moinsetup

moinsetup.py

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