moinsetup

moinsetup.py

54:6813feee7e68
2011-10-15 Paul Boddie Attempt to use both the distribution and installed Moin to identify the Moin version.
     1 #!/usr/bin/env python     2      3 """     4 A setup and configuration script for MoinMoin.     5      6 Copyright (C) 2010, 2011 Paul Boddie <paul@boddie.org.uk>     7      8 This program is free software; you can redistribute it and/or modify it under     9 the terms of the GNU General Public License as published by the Free Software    10 Foundation; either version 3 of the License, or (at your option) any later    11 version.    12     13 This program is distributed in the hope that it will be useful, but WITHOUT    14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS    15 FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more    16 details.    17     18 You should have received a copy of the GNU General Public License along with    19 this program.  If not, see <http://www.gnu.org/licenses/>.    20 """    21     22 from os.path import abspath, exists, extsep, isdir, islink, join, normpath, split    23 from os import chdir, chmod, listdir, mkdir, makedirs, remove, rename, rmdir    24 from getpass import getpass    25 from glob import glob    26 from zipfile import ZipFile    27 import os    28 import sys    29 import shutil    30 import re    31 import tempfile    32     33 __version__ = "0.3"    34     35 # Regular expressions for editing MoinMoin scripts and configuration files.    36     37 def compile_definition(name):    38     return re.compile(r"^(\s*)#*\s*(%s =)\s*(.*)$" % name, re.MULTILINE)    39     40 wikiconfig_import        = re.compile(r"^(\s*)#*\s*"    41                             r"(from\s+)(?P<module>\S+)"    42                             r"(\s+import\s+)(?P<names>(?:\S|[^,\s])+(?:\s*,\s*(?:\S|[^,\s])+)*)"    43                             r"(\s*)$", re.MULTILINE)    44     45 moin_cgi_prefix          = re.compile(r"^#sys\.path\.insert\(0, 'PREFIX.*$", re.MULTILINE)    46 moin_cgi_wikiconfig      = re.compile(r"^#sys\.path\.insert\(0, '/path/to/wikiconfigdir.*$", re.MULTILINE)    47 moin_cgi_properties      = compile_definition("properties")    48 moin_cgi_fix_script_name = compile_definition("fix_script_name")    49 moin_cgi_force_cgi       = re.compile(r"^#(os.environ\['FCGI_FORCE_CGI'\].*)$", re.MULTILINE)    50     51 css_import_stylesheet    = re.compile(r"(\s*@import\s+[\"'])(.*?)([\"']\s*;)")    52     53 # Templates for Apache site definitions.    54     55 apache_site = """    56 ScriptAlias %(url_path)s "%(web_app_dir)s/moin.cgi"    57 """    58     59 apache_site_extra = """    60 Alias %(static_url_path)s "%(htdocs_dir)s/"    61 """    62     63 # Limited hosting .htaccess definitions require the following settings to be    64 # configured in the main Apache configuration files:    65 #    66 # Options ExecCGI FollowSymLinks Indexes SymLinksIfOwnerMatch    67 # AllowOverride FileInfo Limit    68 # AddHandler cgi-script .cgi    69     70 apache_htaccess_combined_mod_rewrite = """    71 DirectoryIndex moin.cgi    72 RewriteEngine On    73 RewriteBase %(url_path)s    74 RewriteCond %%{REQUEST_FILENAME} !-f    75 RewriteCond %%{REQUEST_FILENAME} !-d    76 RewriteRule ^(.*) moin.cgi/$1 [PT,L,QSA]    77 """    78     79 # Post-setup templates.    80     81 postsetup_setfacl = """#!/bin/sh    82     83 find '%(common_dir)s/data' -type f | xargs setfacl -m u:%(web_user)s:rw    84 find '%(common_dir)s/data' -type d | xargs setfacl -m u:%(web_user)s:rwx    85 find '%(common_dir)s/underlay' -type f | xargs setfacl -m u:%(web_user)s:rw    86 find '%(common_dir)s/underlay' -type d | xargs setfacl -m u:%(web_user)s:rwx    87 """    88     89 postsetup_setfacl_extra = """    90 find '%(htdocs_dir)s' -type f | xargs setfacl -m u:%(web_user)s:r    91 find '%(htdocs_dir)s' -type d | xargs setfacl -m u:%(web_user)s:rx    92 """    93     94 postsetup_chown_chmod = """#!/bin/sh    95     96 chown -R %(this_user)s.%(web_group)s '%(common_dir)s/data'    97 chown -R %(this_user)s.%(web_group)s '%(common_dir)s/underlay'    98 chmod -R g+w '%(common_dir)s/data'    99 chmod -R g+w '%(common_dir)s/underlay'   100 """   101    102 postsetup_chown_extra = """   103 chown -R %(this_user)s.%(web_group)s '%(htdocs_dir)s'   104 """   105    106 # Utility functions.   107    108 def readfile(filename):   109     f = open(filename)   110     try:   111         return f.read()   112     finally:   113         f.close()   114    115 def writefile(filename, s):   116     f = open(filename, "w")   117     try:   118         f.write(s)   119     finally:   120         f.close()   121    122 def status(message):   123     print message   124    125 note = status   126 error = status   127    128 def format(s, indent):   129     return re.sub("\n\s+", "\n" + " " * indent, s)   130    131 # Classes.   132    133 class SetupException(Exception):   134    135     "An exception indicating a problem with a setup action."   136    137     pass   138    139 class Configuration:   140    141     "A class representing the configuration."   142    143     special_names = ["site_name"]   144    145     def __init__(self, filename):   146         self.content = readfile(filename)   147         self.filename = filename   148    149     def get_pattern(self, name):   150    151         # Make underscores optional for certain names.   152    153         if name in self.special_names:   154             name = name.replace("_", "_?")   155    156         return compile_definition(name)   157    158     def get(self, name):   159    160         """   161         Return the raw value of the last definition having the given 'name'.   162         """   163    164         pattern = self.get_pattern(name)   165         results = [match.group(3) for match in pattern.finditer(self.content)]   166         if results:   167             return results[-1]   168         else:   169             return None   170    171     def set(self, name, value, count=None, raw=0):   172    173         """   174         Set the configuration parameter having the given 'name' with the given   175         'value', limiting the number of appropriately named parameters changed   176         to 'count', if specified.   177    178         If the configuration parameter of the given 'name' does not exist,   179         insert such a parameter at the end of the file.   180    181         If the optional 'raw' parameter is specified and set to a true value,   182         the provided 'value' is inserted directly into the configuration file.   183         """   184    185         if not self.replace(name, value, count, raw):   186             self.insert(name, value, raw)   187    188     def replace(self, name, value, count=None, raw=0):   189    190         """   191         Replace configuration parameters having the given 'name' with the given   192         'value', limiting the number of appropriately named parameters changed   193         to 'count', if specified.   194    195         If the optional 'raw' parameter is specified and set to a true value,   196         the provided 'value' is inserted directly into the configuration file.   197    198         Return the number of substitutions made.   199         """   200    201         if raw:   202             substitution = r"\1\2 %s" % value   203         else:   204             substitution = r"\1\2 %r" % value   205    206         pattern = self.get_pattern(name)   207    208         if count is None:   209             self.content, n = pattern.subn(substitution, self.content)   210         else:   211             self.content, n = pattern.subn(substitution, self.content, count=count)   212    213         return n   214    215     def insert(self, name, value, raw=0):   216    217         """   218         Insert the configuration parameter having the given 'name' and 'value'.   219    220         If the optional 'raw' parameter is specified and set to a true value,   221         the provided 'value' is inserted directly into the configuration file.   222         """   223    224         if raw:   225             insertion = "%s = %s"   226         else:   227             insertion = "%s = %r"   228    229         self.insert_text(insertion % (name, value))   230    231     def insert_text(self, text):   232    233         "Insert the given 'text' at the end of the configuration."   234    235         if not self.content.endswith("\n"):   236             self.content += "\n"   237         self.content += "    %s\n" % text   238    239     def set_import(self, imported_module, imported_names):   240    241         """   242         Set up an import of the given 'imported_module' exposing the given   243         'imported_names'.   244         """   245    246         s = self.content   247         after_point = 0   248         first_point = None   249    250         for module_import in wikiconfig_import.finditer(s):   251             before, from_keyword, module, import_keyword, names, after = module_import.groups()   252             before_point, after_point = module_import.span()   253    254             if first_point is None:   255                 first_point = after_point   256    257             names = [name.strip() for name in names.split(",")]   258    259             # Test the import for a reference to the requested imported module.   260    261             if imported_module == module:   262                 for name in imported_names:   263                     if name not in names:   264                         names.append(name)   265    266                 self.content = s[:before_point] + (   267                     "%s%s%s%s%s%s" % (before, from_keyword, module, import_keyword, ", ".join(names), after)   268                     ) + s[after_point:]   269                 break   270    271         # Where no import references the imported module, insert a reference   272         # into the configuration.   273    274         else:   275             # Add the import after the first one.   276    277             if first_point is not None:   278                 self.content = s[:first_point] + ("\nfrom %s import %s" % (imported_module, ", ".join(imported_names))) + s[first_point:]   279    280     def close(self):   281    282         "Close the file, writing the content."   283    284         writefile(self.filename, self.content)   285    286 class Installation:   287    288     "A class for installing and initialising MoinMoin."   289    290     method_names = (   291         "show_config",   292         "setup",   293         "setup_wiki",   294         "install_moin",   295         "install_data",   296         "install_static_data",   297         "configure_moin",   298         "edit_moin_script",   299         "edit_moin_web_script",   300         "add_superuser",   301         "make_site_files",   302         "make_post_install_script",   303    304         # Post-installation activities.   305    306         "reconfigure_moin",   307         "set_auth_method",   308         "migrate_instance",   309         "install_theme",   310         "install_extension_package",   311         "install_plugins",   312         "install_actions",   313         "install_macros",   314         "install_parsers",   315         "install_event_handlers",   316         "install_theme_resources",   317         "edit_theme_stylesheet",   318    319         # Other activities.   320    321         "make_page_package",   322         "install_page_package",   323         )   324    325     source_config_names = (   326         "moin_distribution", "prefix", "site_packages", "prefix_site_packages",   327         "htdocs_dir_source"   328         )   329    330     instance_config_names = (   331         "common_dir", "farm_config", "site_config",   332         "site_name", "site_identifier",   333         "front_page_name", "superuser", "theme_default", "htdocs_dir"   334         )   335    336     site_config_names = (   337         "web_app_dir", "web_site_dir", "web_static_dir",   338         "url_path", "static_url_path"   339         )   340    341     # NOTE: Need to detect Web server user.   342    343     web_user = "www-data"   344     web_group = "www-data"   345    346     # MoinMoin resources.   347    348     theme_master = "modernized"   349     extra_theme_css_files = ["SlideShow.css"]   350    351     def __init__(self, moin_distribution=None, prefix=None,   352         site_packages=None, web_app_dir=None, web_static_dir=None, web_site_dir=None,   353         common_dir=None, farm_config=None, site_config=None, url_path=None,   354         superuser=None, site_name=None, site_identifier=None, front_page_name=None,   355         theme_default=None):   356    357         """   358         Initialise a Wiki installation using the following installation   359         settings:   360    361           * moin_distribution - the directory containing a MoinMoin source   362                                 distribution (can be omitted)   363           * prefix            - the installation prefix (equivalent to /usr)   364           * site_packages     - optional: the location of the Python   365                                 site-packages directory if outside the 'prefix'   366                                 (overrides the path calculated using 'prefix')   367           * web_app_dir       - the directory where Web applications and scripts   368                                 reside (such as /home/www-user/cgi-bin)   369           * web_static_dir    - optional: the directory where static Web   370                                 resources reside (such as /home/www-user/htdocs)   371           * web_site_dir      - optional: the directory where Web site   372                                 definitions reside (such as   373                                 /etc/apache2/sites-available)   374    375         The following site settings are also applicable:   376    377           * common_dir        - the directory where the Wiki configuration,   378                                 resources and instance will reside (such as   379                                 /home/www-user/mywiki)   380           * farm_config       - optional: any Wiki farm configuration file for   381                                 multiple Wiki deployments (overrides the   382                                 'common_dir' setting)   383           * site_config       - optional: a specific configuration file location   384                                 (overrides the 'common_dir' setting)   385           * url_path          - the URL path at which the Wiki will be made   386                                 available (such as / or /mywiki)   387           * superuser         - the name of the site's superuser (such as   388                                 "AdminUser", can be omitted)   389           * site_name         - the name of the site (such as "My Wiki")   390           * site_identifier   - optional: an identifier used to refer to the   391                                 site, typically derived from 'site_name'   392           * front_page_name   - the front page name for the site (such as   393                                 "FrontPage" or a specific name for the site)   394           * theme_default     - optional: the default theme (such as modern)   395         """   396    397         self.moin_distribution = moin_distribution   398         self.superuser = superuser   399         self.site_name = site_name   400         self.site_identifier = site_identifier or site_name.replace(" ", "").lower()   401         self.front_page_name = front_page_name   402         self.farm_config = farm_config   403         self.site_config = site_config   404         self.theme_default = theme_default   405    406         # NOTE: Support the detection of the Apache sites directory.   407    408         self.prefix, self.site_packages, self.web_app_dir, self.web_site_dir, self.web_static_dir, self.common_dir = \   409             map(self._get_abspath, (prefix, site_packages, web_app_dir, web_site_dir, web_static_dir, common_dir))   410    411         if not self.web_app_dir:   412             raise TypeError, "The 'web_app_dir' setting must be specified."   413    414         # Strip any trailing "/" from the URL path.   415    416         if not url_path:   417             raise TypeError, "The 'url_path' setting must be specified."   418    419         if url_path != "/" and url_path.endswith("/"):   420             self.url_path = url_path[:-1]   421         else:   422             self.url_path = url_path   423    424         # Define and create specific directories.   425         # Here are the configuration and actual Wiki data directories.   426    427         if not self.common_dir:   428             raise TypeError, "The 'common_dir' setting must be specified."   429    430         # Define the place where the MoinMoin package will actually reside.   431    432         if not self.prefix and not self.site_packages:   433             raise TypeError, "Either the 'prefix' or the 'site_packages' setting must be specified."   434    435         self.prefix_site_packages = self.site_packages or \   436             join(self.prefix, "lib", "python%s.%s" % sys.version_info[:2], "site-packages")   437    438         # Find the version.   439    440         self.moin_version = self.get_moin_version()   441    442         # The static resources reside in different locations depending on the   443         # version of MoinMoin, but the Web server is used to serve static   444         # resources in both cases, even though MoinMoin 1.9 can serve static   445         # files itself.   446    447         # A shared data directory may be in use.   448    449         self.htdocs_dir_source = join(self.get_moin_data(), "htdocs")   450    451         if self.htdocs_dir_source is None or not exists(self.htdocs_dir_source):   452    453             # 1.9: moin/lib/python2.x/site-packages/MoinMoin/web/static/htdocs   454    455             if self.moin_version.startswith("1.9"):   456                 self.htdocs_dir_source = join(self.prefix_site_packages, "MoinMoin", "web", "static", "htdocs")   457             else:   458                 raise SetupException, "The static resources could not be found."   459    460         # Add the static identifier to the URL path. For example:   461         #   462         # /         -> /moin_static187   463         # /hgwiki   -> /hgwiki-moin_static187   464         #   465         # This allows multiple Wiki instances to have their own static resources   466         # in the same hosting area.   467    468         self.static_url_path = self.url_path + (self.url_path != "/" and "-" or "") + self.get_static_identifier()   469    470         # In limited hosting, the static resources directory is related to   471         # the URL path.   472    473         if self.limited_hosting():   474             self.htdocs_dir = join(self.web_static_dir or self.web_app_dir, self.static_url_path.lstrip("/"))   475    476         # Otherwise, a mapping is made to the directory.   477         # This may be placed in a special static directory if desired.   478    479         else:   480             self.htdocs_dir = join(self.web_static_dir or self.common_dir, "htdocs")   481    482     def show_config(self):   483    484         "Show the configuration."   485    486         print   487         for section in ("source", "instance", "site"):   488             print section.title()   489             print "-" * len(section)   490             print   491             for setting in getattr(self, "%s_config_names" % section):   492                 print "%-24s%s" % (setting, getattr(self, setting))   493             print   494    495     def _get_abspath(self, d):   496         return d and abspath(d) or None   497    498     def get_moin_version(self):   499    500         "Return the MoinMoin version."   501    502         this_dir = os.getcwd()   503    504         for dir in [self.moin_distribution, self.prefix_site_packages]:   505             if dir:   506                 try:   507                     chdir(dir)   508                     version = self.get_moin_version_from_package_info() or \   509                         self.get_moin_version_from_import()   510                     if version:   511                         return version   512    513                 finally:   514                     chdir(this_dir)   515         else:   516             return self.get_moin_version_from_import()   517    518     def get_moin_version_from_package_info(self):   519    520         "Inspect the MoinMoin package information, returning the version."   521    522         try:   523             f = open("PKG-INFO")   524             try:   525                 for line in f.xreadlines():   526                     columns = line.split()   527                     if columns[0] == "Version:":   528                         return columns[1]   529             finally:   530                 f.close()   531    532         except IOError:   533             pass   534    535         return None   536    537     def get_moin_version_from_import(self):   538    539         "Return the MoinMoin version from an import of the package itself."   540    541         # Where no distribution information can be read, try and import an   542         # installed version module.   543    544         f = os.popen("%s -c 'from MoinMoin.version import release; print release'" % sys.executable)   545         try:   546             return f.read().strip()   547         finally:   548             f.close()   549    550     def get_moin_data(self):   551    552         "Return the exact location of MoinMoin data."   553    554         return self.moin_distribution and join(self.moin_distribution, "wiki") or \   555             self.prefix and join(self.prefix, "share", "moin") or None   556    557     def get_moin_script(self):   558    559         "Return the location of the general-purpose moin script."   560    561         return join(self.prefix, "bin", "moin")   562    563     def get_wikiconfig_directory(self):   564    565         "Return the location of the Wiki configuration."   566    567         if self.site_config:   568             return split(self.site_config)[0]   569         else:   570             return self.common_dir   571    572     def get_static_identifier(self):   573    574         "Return the static URL/directory identifier for the Wiki."   575    576         return "moin_static%s" % self.moin_version.replace(".", "")   577    578     def get_plugin_directory(self, plugin_type):   579    580         "Return the directory for plugins of the given 'plugin_type'."   581    582         data_dir = join(self.common_dir, "data")   583         return join(data_dir, "plugin", plugin_type)   584    585     def limited_hosting(self):   586    587         "Return whether limited Web hosting is being used."   588    589         return not self.web_site_dir   590    591     def ensure_directories(self):   592    593         "Make sure that all the directories are available."   594    595         for d in (self.common_dir, self.web_app_dir, self.web_static_dir, self.web_site_dir):   596             if d is not None and not exists(d):   597                 makedirs(d)   598    599     def get_theme_directories(self, theme_name=None):   600    601         """   602         Return tuples of the form (theme name, theme directory) for all themes,   603         or for a single theme if the optional 'theme_name' is specified.   604         """   605    606         filenames = theme_name and [theme_name] or listdir(self.htdocs_dir)   607         directories = []   608    609         for filename in filenames:   610             theme_dir = join(self.htdocs_dir, filename)   611    612             if not exists(theme_dir) or not isdir(theme_dir):   613                 continue   614    615             directories.append((filename, theme_dir))   616    617         return directories   618    619     # Main methods.   620    621     def setup(self):   622    623         "Set up the installation."   624    625         self.ensure_directories()   626         self.install_moin()   627         self.edit_moin_script()   628         self._setup_wiki()   629    630     def setup_wiki(self):   631    632         "Set up a Wiki without installing MoinMoin."   633    634         self.ensure_directories()   635         self._setup_wiki()   636    637     def _setup_wiki(self):   638    639         "Set up a Wiki without installing MoinMoin."   640    641         self.install_data()   642         self.install_static_data()   643         self.configure_moin()   644         self.add_superuser()   645         self.edit_moin_web_script(self.make_site_files())   646         self.make_post_install_script()   647    648         if self.moin_version.startswith("1.9"):   649             note("You may need to visit the LanguageSetup page in the Wiki to create the standard set of pages.")   650    651     def install_moin(self):   652    653         "Enter the distribution directory and run the setup script."   654    655         # NOTE: Possibly check for an existing installation and skip repeated   656         # NOTE: installation attempts.   657    658         if not self.moin_distribution:   659             raise SetupException, "Cannot install MoinMoin without a 'moin_distribution' setting being defined."   660    661         this_dir = os.getcwd()   662         chdir(self.moin_distribution)   663    664         log_filename = "install-%s.log" % split(self.common_dir)[-1]   665    666         status("Installing MoinMoin in %s..." % self.prefix)   667    668         install_cmd = "install"   669         options = "--prefix='%s' --record='%s'" % (self.prefix, log_filename)   670    671         os.system("python setup.py --quiet %s %s --force" % (install_cmd, options))   672    673         chdir(this_dir)   674    675     def install_data(self):   676    677         "Install Wiki data into an instance."   678    679         moin_data = self.get_moin_data()   680    681         if not moin_data:   682             raise SetupException, \   683                 "Cannot install MoinMoin data without either a 'moin_distribution' or a 'prefix' setting being defined."   684    685         # The default wikiconfig assumes data and underlay in the same directory.   686    687         status("Installing data and underlay in %s..." % self.common_dir)   688    689         for d in ("data", "underlay"):   690             source = join(moin_data, d)   691             source_tar = source + extsep + "tar"   692    693             if exists(source):   694                 shutil.copytree(source, join(self.common_dir, d))   695             elif exists(source_tar):   696    697                 note("Copying archive %s instead of directory %s. Running...\n"   698                     "make pagepacks\n"   699                     "in the distribution directory should rectify this." % (source_tar, source))   700    701                 shutil.copy(source_tar, self.common_dir)   702                 os.system("tar xf %s -C %s" % (source_tar, self.common_dir))   703             else:   704                 status("Could not copy %s into installed Wiki." % d)   705    706     def install_static_data(self):   707    708         "Install static Web data if appropriate."   709    710         if not exists(self.htdocs_dir):   711             mkdir(self.htdocs_dir)   712    713         for item in listdir(self.htdocs_dir_source):   714             path = join(self.htdocs_dir_source, item)   715             if isdir(path):   716                 shutil.copytree(path, join(self.htdocs_dir, item))   717             else:   718                 shutil.copy(path, join(self.htdocs_dir, item))   719    720     def configure_moin(self, reset=0):   721    722         """   723         Edit the Wiki configuration file. If the optional 'reset' parameter is   724         specified as a true value, a default configuration will be copied from   725         the distribution if appropriate.   726         """   727    728         moin_data = self.get_moin_data()   729    730         if not moin_data:   731             raise SetupException, \   732                 "Cannot configure MoinMoin without either a 'moin_distribution' or a 'prefix' setting being defined."   733    734         # NOTE: MoinMoin usually uses an apparently common URL space associated   735         # NOTE: with the version, but more specific locations are probably   736         # NOTE: acceptable if less efficient.   737    738         url_prefix_static = "%r" % self.static_url_path   739    740         # Use a farm configuration file.   741    742         if self.farm_config:   743             wikiconfig_py = self.farm_config   744    745         # Or copy the Wiki configuration file from the distribution.   746    747         else:   748             wikiconfig_py = join(self.common_dir, "wikiconfig.py")   749    750             if not exists(wikiconfig_py) or reset:   751                 shutil.copyfile(join(moin_data, "config", "wikiconfig.py"), wikiconfig_py)   752    753         status("Editing configuration from %s..." % wikiconfig_py)   754    755         # Edit the Wiki configuration file.   756    757         wikiconfig = Configuration(wikiconfig_py)   758    759         try:   760             wikiconfig.set("url_prefix_static", url_prefix_static, raw=1)   761             if self.superuser:   762                 wikiconfig.set("superuser", [self.superuser])   763                 wikiconfig.set("acl_rights_before", u"%s:read,write,delete,revert,admin" % self.superuser)   764             else:   765                 note("Superuser not defined. The ACL rules should be fixed in the configuration.")   766    767             # Edit any created Wiki configuration.   768    769             if not self.site_config:   770                 self._configure_moin(wikiconfig)   771    772         finally:   773             wikiconfig.close()   774    775         # Edit any separate site configuration file.   776    777         if self.site_config:   778             status("Editing configuration from %s..." % self.site_config)   779    780             wikiconfig = Configuration(self.site_config)   781    782             try:   783                 self._configure_moin(wikiconfig)   784             finally:   785                 wikiconfig.close()   786    787     def _configure_moin(self, wikiconfig):   788    789         """   790         Configure Moin, accessing the configuration file using 'wikiconfig'.   791         """   792    793         # Specific site configurations also appear to need 'data_dir', even in   794         # 1.9.   795    796         if not self.moin_version.startswith("1.9") or self.site_config:   797             data_dir = join(self.common_dir, "data")   798             data_underlay_dir = join(self.common_dir, "underlay")   799    800             wikiconfig.set("data_dir", data_dir)   801             wikiconfig.set("data_underlay_dir", data_underlay_dir)   802    803         wikiconfig.set("site_name", self.site_name)   804         wikiconfig.set("page_front_page", self.front_page_name, count=1)   805    806         if self.theme_default is not None:   807             wikiconfig.set("theme_default", self.theme_default)   808    809     def edit_moin_script(self):   810    811         "Edit the moin script."   812    813         moin_script = self.get_moin_script()   814         status("Editing moin script at %s..." % moin_script)   815    816         s = readfile(moin_script)   817         s = s.replace("#import sys", "import sys\nsys.path.insert(0, %r)" % self.prefix_site_packages)   818    819         writefile(moin_script, s)   820    821     def edit_moin_web_script(self, site_file_configured=1):   822    823         "Edit and install CGI script."   824    825         # NOTE: CGI only so far.   826         # NOTE: Permissions should be checked.   827    828         moin_data = self.get_moin_data()   829    830         if self.moin_version.startswith("1.9"):   831             moin_cgi_script = "moin.fcgi"   832         else:   833             moin_cgi_script = "moin.cgi"   834    835         moin_cgi = join(moin_data, "server", moin_cgi_script)   836         moin_cgi_installed = join(self.web_app_dir, "moin.cgi")   837    838         status("Editing moin.cgi script from %s, writing to %s..." % (moin_cgi, moin_cgi_installed))   839    840         s = readfile(moin_cgi)   841         s = moin_cgi_prefix.sub("sys.path.insert(0, %r)" % self.prefix_site_packages, s)   842         s = moin_cgi_wikiconfig.sub("sys.path.insert(0, %r)" % self.common_dir, s)   843    844         # Handle differences in script names when using limited hosting with   845         # URL rewriting.   846    847         if self.limited_hosting():   848             if not site_file_configured:   849                 note("Site file not configured: script name not changed.")   850             else:   851                 if self.moin_version.startswith("1.9"):   852                     s = moin_cgi_fix_script_name.sub(r"\1\2 %r" % self.url_path, s)   853                 else:   854                     s = moin_cgi_properties.sub(r"\1\2 %r" % {"script_name" : self.url_path}, s)   855    856         # NOTE: Use CGI for now.   857    858         if self.moin_version.startswith("1.9"):   859             s = moin_cgi_force_cgi.sub(r"\1", s)   860    861         writefile(moin_cgi_installed, s)   862         os.system("chmod a+rx '%s'" % moin_cgi_installed)   863    864         # Fix the cause of opaque errors in some Apache environments.   865    866         os.system("chmod go-w '%s'" % moin_cgi_installed)   867    868     def add_superuser(self):   869    870         "Add the superuser account."   871    872         if not self.superuser:   873             return   874    875         print "Creating superuser", self.superuser, "using..."   876         email = raw_input("E-mail address: ")   877         password = getpass("Password: ")   878    879         path = os.environ.get("PYTHONPATH", "")   880    881         if path:   882             os.environ["PYTHONPATH"] = path + ":" + self.common_dir   883         else:   884             os.environ["PYTHONPATH"] = self.common_dir   885    886         cmd = "%s --config-dir='%s' account create --name='%s' --email='%s' --password='%s'" % (   887             self.get_moin_script(), self.common_dir, self.superuser, email, password)   888         os.system(cmd)   889    890         if path:   891             os.environ["PYTHONPATH"] = path   892         else:   893             del os.environ["PYTHONPATH"]   894    895     def make_site_files(self):   896    897         "Make the Apache site files."   898    899         # NOTE: Using local namespace for substitution.   900    901         # Where the site definitions and applications directories are different,   902         # use a normal site definition.   903    904         if not self.limited_hosting():   905    906             site_def = join(self.web_site_dir, self.site_identifier)   907    908             s = apache_site % self.__dict__   909             s += apache_site_extra % self.__dict__   910    911             status("Writing Apache site definitions to %s..." % site_def)   912             writefile(site_def, s)   913    914             note("Copy the site definitions to the appropriate sites directory if appropriate.\n"   915                 "Then, make sure that the site is enabled by running the appropriate tools (such as a2ensite).")   916    917             return 1   918    919         # Otherwise, use an .htaccess file.   920    921         else:   922             site_def = join(self.web_app_dir, ".htaccess")   923    924             s = apache_htaccess_combined_mod_rewrite % self.__dict__   925    926             status("Writing .htaccess file to %s..." % site_def)   927             try:   928                 writefile(site_def, s)   929             except IOError:   930                 note("The .htaccess file could not be written. This will also affect the script name setting.")   931                 return 0   932             else:   933                 return 1   934    935     def make_post_install_script(self):   936    937         "Write a post-install script with additional actions."   938    939         # Work out whether setfacl works.   940    941         fd, temp_filename = tempfile.mkstemp(dir=self.common_dir)   942         os.close(fd)   943    944         have_setfacl = os.system("setfacl -m user:%(web_user)s:r %(file)s > /dev/null 2>&1" % {   945             "web_user" : self.web_user, "file" : temp_filename}) == 0   946    947         remove(temp_filename)   948    949         # Create the scripts.   950    951         this_user = os.environ["USER"]   952         postinst_scripts = "moinsetup-post-chown.sh", "moinsetup-post-setfacl.sh"   953    954         vars = {}   955         vars.update(Installation.__dict__)   956         vars.update(self.__dict__)   957         vars.update(locals())   958    959         for postinst_script, start, extra in [   960             (postinst_scripts[0], postsetup_chown_chmod, postsetup_chown_extra),   961             (postinst_scripts[1], postsetup_setfacl, postsetup_setfacl_extra)   962             ]:   963    964             s = start % vars   965             s += extra % vars   966    967             writefile(postinst_script, s)   968             chmod(postinst_script, 0755)   969    970         if have_setfacl:   971             note("Run %s to set file ownership and permissions.\n"   972                 "If this somehow fails..." % postinst_scripts[1])   973    974         note("Run %s as root to set file ownership and permissions." % postinst_scripts[0])   975    976     # Accessory methods.   977    978     def reconfigure_moin(self, name=None, value=None, raw=0):   979    980         """   981         Edit the installed Wiki configuration file, setting a parameter with any   982         given 'name' to the given 'value', treating the value as a raw   983         expression (not a string) if 'raw' is set to a true value.   984    985         If 'name' and the remaining parameters are omitted, the default   986         configuration activity is performed.   987    988         If the 'site_config' setting is defined, the specific site configuration   989         will be changed.   990         """   991    992         if self.site_config:   993             wikiconfig_py = self.site_config   994         else:   995             wikiconfig_py = join(self.common_dir, "wikiconfig.py")   996    997         status("Editing configuration from %s..." % wikiconfig_py)   998    999         wikiconfig = Configuration(wikiconfig_py)  1000   1001         try:  1002             # Perform default configuration.  1003   1004             if name is None and value is None:  1005                 self._configure_moin(wikiconfig)  1006             else:  1007                 wikiconfig.set(name, value, raw=raw)  1008   1009         finally:  1010             wikiconfig.close()  1011   1012     def set_auth_method(self, method_name):  1013   1014         """  1015         Edit the installed Wiki configuration file, configuring the  1016         authentication method having the given 'method_name'.  1017   1018         If the 'farm_config' setting is defined, the Wiki farm configuration  1019         will be changed.  1020         """  1021   1022         if self.farm_config:  1023             wikiconfig_py = self.farm_config  1024         else:  1025             wikiconfig_py = join(self.common_dir, "wikiconfig.py")  1026   1027         status("Editing configuration from %s..." % wikiconfig_py)  1028   1029         wikiconfig = Configuration(wikiconfig_py)  1030   1031         try:  1032             # OpenID authentication.  1033   1034             if method_name.lower() == "openid":  1035                 wikiconfig.set_import("MoinMoin.auth.openidrp", ["OpenIDAuth"])  1036   1037                 if self.moin_version.startswith("1.9"):  1038                     if wikiconfig.get("cookie_lifetime"):  1039                         wikiconfig.replace("cookie_lifetime", "(12, 12)", raw=1)  1040                     else:  1041                         wikiconfig.set("cookie_lifetime", "(12, 12)", raw=1)  1042                 else:  1043                     if wikiconfig.get("anonymous_session_lifetime"):  1044                         wikiconfig.replace("anonymous_session_lifetime", "1000", raw=1)  1045                     else:  1046                         wikiconfig.set("anonymous_session_lifetime", "1000", raw=1)  1047   1048                 auth_object = "OpenIDAuth()"  1049   1050             # Default Moin authentication.  1051   1052             elif method_name.lower() in ("moin", "default"):  1053                 wikiconfig.set_import("MoinMoin.auth", ["MoinAuth"])  1054                 auth_object = "MoinAuth()"  1055   1056             # REMOTE_USER authentication.  1057   1058             elif method_name.lower() in ("given", "remote-user"):  1059                 wikiconfig.set_import("MoinMoin.auth.http", ["HTTPAuth"])  1060                 auth_object = "HTTPAuth(autocreate=True)"  1061   1062             # Other methods are not currently supported.  1063   1064             else:  1065                 return  1066   1067             # Edit the authentication setting.  1068   1069             auth = wikiconfig.get("auth")  1070             if auth:  1071                 wikiconfig.replace("auth", "%s + [%s]" % (auth, auth_object), raw=1)  1072             else:  1073                 wikiconfig.set("auth", "[%s]" % auth_object, raw=1)  1074   1075         finally:  1076             wikiconfig.close()  1077   1078     def migrate_instance(self, test=0, change_site=0):  1079   1080         """  1081         Migrate the Wiki to the currently supported layout. If 'test' is  1082         specified and set to a non-empty or true value, only print whether the  1083         migration can be performed.  1084   1085         If 'change_site' is specified and set to a non-empty or true value, the  1086         site definitions will be updated; this will overwrite any changes made  1087         to the site definitions after they were last produced by moinsetup, and  1088         care must be taken to ensure that things like access controls are  1089         re-added to the definitions after this action is performed.  1090         """  1091   1092         conf_dir = join(self.common_dir, "conf")  1093         if exists(conf_dir):  1094             for filename in listdir(conf_dir):  1095                 pathname = join(conf_dir, filename)  1096                 target = join(self.common_dir, filename)  1097                 if not exists(target):  1098                     print "Move", filename, "from conf directory."  1099                     if not test:  1100                         rename(pathname, target)  1101         else:  1102             print "No conf directory."  1103   1104         wikidata = join(self.common_dir, "wikidata")  1105         if exists(wikidata):  1106             htdocs = join(wikidata, "share", "moin", "htdocs")  1107             if exists(htdocs):  1108                 target = join(self.common_dir, "htdocs")  1109                 if not exists(target):  1110                     print "Move htdocs from wikidata directory."  1111                     if not test:  1112                         rename(htdocs, target)  1113         else:  1114             print "No wikidata directory."  1115   1116         # Remove links and directories.  1117   1118         for name in ("conf", "wikidata"):  1119             d = join(self.common_dir, name)  1120             if islink(d):  1121                 print "Remove %s symbolic link." % name  1122                 if not test:  1123                     remove(d)  1124   1125         if isdir(conf_dir):  1126             print "Remove conf directory."  1127             if not test:  1128                 rmdir(conf_dir)  1129   1130         # Add any missing htdocs directory.  1131   1132         if not exists(self.htdocs_dir):  1133             print "Copy htdocs into the instance."  1134             if not test:  1135                 self.install_static_data()  1136   1137         # Now attempt to reconfigure the Wiki.  1138   1139         print "Reconfigure the Wiki, the Web script%s." % (change_site and " and the site files" or "")  1140         if not test:  1141             self.configure_moin()  1142             self.edit_moin_web_script()  1143             if change_site:  1144                 self.make_site_files()  1145   1146     def install_theme(self, theme_dir, theme_name=None):  1147   1148         """  1149         Install Wiki theme provided in the given 'theme_dir' having the given  1150         optional 'theme_name' (if different from the 'theme_dir' name).  1151         """  1152   1153         theme_dir = normpath(theme_dir)  1154         theme_name = theme_name or split(theme_dir)[-1]  1155         theme_module = join(theme_dir, theme_name + extsep + "py")  1156   1157         plugin_theme_dir = self.get_plugin_directory("theme")  1158   1159         # Copy the theme module.  1160   1161         status("Copying theme module to %s..." % plugin_theme_dir)  1162   1163         shutil.copy(theme_module, plugin_theme_dir)  1164   1165         # Copy the resources.  1166   1167         resources_dir = join(self.htdocs_dir, theme_name)  1168   1169         if not exists(resources_dir):  1170             mkdir(resources_dir)  1171   1172         status("Copying theme resources to %s..." % resources_dir)  1173   1174         for d in ("css", "img"):  1175             target_dir = join(resources_dir, d)  1176             if exists(target_dir):  1177                 status("Replacing %s..." % target_dir)  1178                 shutil.rmtree(target_dir)  1179             shutil.copytree(join(theme_dir, d), target_dir)  1180   1181         # Copy additional resources from other themes.  1182   1183         resources_source_dir = join(self.htdocs_dir, self.theme_master)  1184         target_dir = join(resources_dir, "css")  1185   1186         status("Copying resources from %s..." % resources_source_dir)  1187   1188         for css_file in self.extra_theme_css_files:  1189             css_file_path = join(resources_source_dir, "css", css_file)  1190             if exists(css_file_path):  1191                 shutil.copy(css_file_path, target_dir)  1192   1193         note("Don't forget to add theme resources for extensions for this theme.\n"  1194             "Don't forget to edit this theme's stylesheets for extensions.")  1195   1196     def install_extension_package(self, extension_dir):  1197   1198         "Install any libraries from 'extension_dir' using a setup script."  1199   1200         this_dir = os.getcwd()  1201         chdir(extension_dir)  1202   1203         options = "install --install-lib=%s" % self.prefix_site_packages  1204   1205         os.system("python setup.py %s" % options)  1206         chdir(this_dir)  1207   1208     def install_plugins(self, plugins_dir, plugin_type):  1209   1210         """  1211         Install Wiki actions provided in the given 'plugins_dir' of the  1212         specified 'plugin_type'.  1213         """  1214   1215         plugin_target_dir = self.get_plugin_directory(plugin_type)  1216   1217         # Copy the modules.  1218   1219         status("Copying %s modules to %s..." % (plugin_type, plugin_target_dir))  1220   1221         for module in glob(join(plugins_dir, "*%spy" % extsep)):  1222             shutil.copy(module, plugin_target_dir)  1223   1224     def install_actions(self, actions_dir):  1225   1226         "Install Wiki actions provided in the given 'actions_dir'."  1227   1228         self.install_plugins(actions_dir, "action")  1229   1230     def install_macros(self, macros_dir):  1231   1232         "Install Wiki macros provided in the given 'macros_dir'."  1233   1234         self.install_plugins(macros_dir, "macro")  1235   1236     def install_parsers(self, parsers_dir):  1237   1238         "Install Wiki parsers provided in the given 'parsers_dir'."  1239   1240         self.install_plugins(parsers_dir, "parser")  1241   1242     def install_event_handlers(self, events_dir):  1243   1244         "Install Wiki event handlers provided in the given 'events_dir'."  1245   1246         self.install_plugins(events_dir, "events")  1247   1248     def install_theme_resources(self, theme_resources_dir, theme_name=None):  1249   1250         """  1251         Install theme resources provided in the given 'theme_resources_dir'. If  1252         a specific 'theme_name' is given, only that theme will be given the  1253         specified resources.  1254         """  1255   1256         for theme_name, theme_dir in self.get_theme_directories(theme_name):  1257   1258             # Copy the resources.  1259   1260             copied = 0  1261   1262             for d in ("css", "img"):  1263                 source_dir = join(theme_resources_dir, d)  1264                 target_dir = join(theme_dir, d)  1265   1266                 if not exists(target_dir):  1267                     continue  1268   1269                 for resource in glob(join(source_dir, "*%s*" % extsep)):  1270                     shutil.copy(resource, target_dir)  1271                     copied = 1  1272   1273             if copied:  1274                 status("Copied theme resources into %s..." % theme_dir)  1275   1276         note("Don't forget to edit theme stylesheets for any extensions.")  1277   1278     def edit_theme_stylesheet(self, theme_stylesheet, imported_stylesheet, action="ensure", theme_name=None):  1279   1280         """  1281         Edit the given 'theme_stylesheet', ensuring (or removing) a reference to  1282         the 'imported_stylesheet' according to the given 'action' (optional,  1283         defaulting to "ensure"). If a specific 'theme_name' is given, only that  1284         theme will be affected.  1285         """  1286   1287         if action == "ensure":  1288             ensure = 1  1289         elif action == "remove":  1290             ensure = 0  1291         else:  1292             error("Action %s not valid: it must be given as either 'ensure' or 'remove'." % action)  1293             return  1294   1295         for theme_name, theme_dir in self.get_theme_directories(theme_name):  1296   1297             # Locate the resources.  1298   1299             css_dir = join(theme_dir, "css")  1300   1301             if not exists(css_dir):  1302                 continue  1303   1304             theme_stylesheet_filename = join(css_dir, theme_stylesheet)  1305             imported_stylesheet_filename = join(css_dir, imported_stylesheet)  1306   1307             if not exists(theme_stylesheet_filename):  1308                 error("Stylesheet %s not defined in theme %s." % (theme_stylesheet, theme_name))  1309                 continue  1310   1311             if not exists(imported_stylesheet_filename):  1312                 error("Stylesheet %s not defined in theme %s." % (imported_stylesheet, theme_name))  1313                 continue  1314   1315             # Edit the resources.  1316   1317             s = readfile(theme_stylesheet_filename)  1318             after_point = 0  1319   1320             for stylesheet_import in css_import_stylesheet.finditer(s):  1321                 before, filename, after = stylesheet_import.groups()  1322                 before_point, after_point = stylesheet_import.span()  1323   1324                 # Test the import for a reference to the requested imported  1325                 # stylesheet.  1326   1327                 if filename == imported_stylesheet:  1328                     if ensure:  1329                         break  1330                     else:  1331                         if s[after_point:after_point+1] == "\n":  1332                             after_point += 1  1333                         s = "%s%s" % (s[:before_point], s[after_point:])  1334   1335                         status("Removing %s from %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name))  1336                         writefile(theme_stylesheet_filename, s)  1337                         break  1338   1339             # Where no import references the imported stylesheet, insert a  1340             # reference into the theme stylesheet.  1341   1342             else:  1343                 if ensure:  1344   1345                     # Assume that the stylesheet can follow other imports.  1346   1347                     if s[after_point:after_point+1] == "\n":  1348                         after_point += 1  1349                     s = "%s%s\n%s" % (s[:after_point], '@import "%s";' % imported_stylesheet, s[after_point:])  1350   1351                     status("Adding %s to %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name))  1352                     writefile(theme_stylesheet_filename, s)  1353   1354     def make_page_package(self, page_directory, package_filename):  1355   1356         """  1357         Make a package containing the pages in 'page_directory', using the  1358         filenames as the page names, and writing the package to a file with the  1359         given 'package_filename'.  1360         """  1361   1362         package = ZipFile(package_filename, "w")  1363   1364         try:  1365             script = ["MoinMoinPackage|1"]  1366   1367             for filename in listdir(page_directory):  1368                 pathname = join(page_directory, filename)  1369   1370                 # Add files as pages having the filename as page name.  1371   1372                 if os.path.isfile(pathname):  1373                     package.write(pathname, filename)  1374                     script.append("AddRevision|%s|%s" % (filename, filename))  1375   1376                 # Add directories ending with "-attachments" as collections of  1377                 # attachments for a particular page.  1378   1379                 elif os.path.isdir(pathname) and filename.endswith("-attachments"):  1380                     parent = filename[:-len("-attachments")]  1381   1382                     # Add each file as an attachment.  1383   1384                     for attachment in listdir(pathname):  1385                         zipname = "%s_%s" % (filename, attachment)  1386                         package.write(join(pathname, attachment), zipname)  1387                         script.append("AddAttachment|%s|%s|%s||" % (zipname, attachment, parent))  1388   1389             package.writestr("MOIN_PACKAGE", "\n".join(script))  1390   1391         finally:  1392             package.close()  1393   1394     def install_page_package(self, package_filename):  1395   1396         """  1397         Install a package from the file with the given 'package_filename'.  1398         """  1399   1400         path = os.environ.get("PYTHONPATH", "")  1401   1402         conf_dir = self.get_wikiconfig_directory()  1403   1404         if path:  1405             os.environ["PYTHONPATH"] = path + ":" + self.prefix_site_packages + ":" + conf_dir  1406         else:  1407             os.environ["PYTHONPATH"] = self.prefix_site_packages + ":" + conf_dir  1408   1409         installer = join(self.prefix_site_packages, "MoinMoin", "packages.py")  1410         cmd = "python %s i %s" % (installer, package_filename)  1411         os.system(cmd)  1412   1413         if path:  1414             os.environ["PYTHONPATH"] = path  1415         else:  1416             del os.environ["PYTHONPATH"]  1417   1418 def show_methods():  1419     print "Methods:"  1420     print  1421     for method_name in Installation.method_names:  1422         doc = getattr(Installation, method_name).__doc__.strip()  1423         print "%-30s%-s" % (method_name, format(doc, 30))  1424     print  1425   1426 # Command line option syntax.  1427   1428 syntax_description = "[ -f <config-filename> ] ( -m <method> | --method=METHOD ) [ <method-argument> ... ]"  1429   1430 # Main program.  1431   1432 if __name__ == "__main__":  1433     from ConfigParser import ConfigParser  1434     import sys, cmdsyntax  1435   1436     # Check the command syntax.  1437   1438     syntax = cmdsyntax.Syntax(syntax_description)  1439     try:  1440         matches = syntax.get_args(sys.argv[1:])  1441         args = matches[0]  1442     except IndexError:  1443         print "Syntax:"  1444         print sys.argv[0], syntax_description  1445         print  1446         show_methods()  1447         sys.exit(1)  1448   1449     # Obtain configuration details.  1450   1451     try:  1452         config_filename = args.get("config-filename", "moinsetup.cfg")  1453   1454         if not exists(config_filename):  1455             print "Configuration", config_filename, "not found."  1456             sys.exit(1)  1457   1458         config = ConfigParser()  1459         config.read(config_filename)  1460   1461         # Obtain as many arguments as needed from the configuration.  1462   1463         config_arguments = dict(config.items("installation") + config.items("site"))  1464         method_arguments = args.get("method-argument", [])  1465   1466         # Attempt to initialise the configuration.  1467   1468         installation = Installation(**config_arguments)  1469   1470     except TypeError, exc:  1471         print "Error:"  1472         print  1473         print exc.args[0]  1474         print  1475         print "Configuration settings:"  1476         print  1477         print Installation.__init__.__doc__  1478         print  1479         sys.exit(1)  1480   1481     # Obtain the method.  1482   1483     try:  1484         method = getattr(installation, args["method"])  1485     except AttributeError:  1486         show_methods()  1487         sys.exit(1)  1488   1489     try:  1490         method(*method_arguments)  1491     except TypeError, exc:  1492         print "Error:"  1493         print  1494         print exc.args[0]  1495         print  1496         print "Method documentation:"  1497         print  1498         print method.__doc__  1499         print  1500         sys.exit(1)  1501   1502 # vim: tabstop=4 expandtab shiftwidth=4