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 if self.moin_distribution: 503 this_dir = os.getcwd() 504 try: 505 chdir(self.moin_distribution) 506 version = self.get_moin_version_from_package_info() or \ 507 self.get_moin_version_from_import() 508 if version: 509 return version 510 511 finally: 512 chdir(this_dir) 513 514 else: 515 return self.get_moin_version_from_import() 516 517 def get_moin_version_from_package_info(self): 518 519 "Inspect the MoinMoin package information, returning the version." 520 521 try: 522 f = open("PKG-INFO") 523 try: 524 for line in f.xreadlines(): 525 columns = line.split() 526 if columns[0] == "Version:": 527 return columns[1] 528 finally: 529 f.close() 530 531 except IOError: 532 pass 533 534 return None 535 536 def get_moin_version_from_import(self): 537 538 "Return the MoinMoin version from an import of the package itself." 539 540 # Where no distribution information can be read, try and import an 541 # installed version module. 542 543 f = os.popen("%s -c 'from MoinMoin.version import release; print release'" % sys.executable) 544 try: 545 return f.read().strip() 546 finally: 547 f.close() 548 549 def get_moin_data(self): 550 551 "Return the exact location of MoinMoin data." 552 553 return self.moin_distribution and join(self.moin_distribution, "wiki") or \ 554 self.prefix and join(self.prefix, "share", "moin") or None 555 556 def get_moin_script(self): 557 558 "Return the location of the general-purpose moin script." 559 560 return join(self.prefix, "bin", "moin") 561 562 def get_wikiconfig_directory(self): 563 564 "Return the location of the Wiki configuration." 565 566 if self.site_config: 567 return split(self.site_config)[0] 568 else: 569 return self.common_dir 570 571 def get_static_identifier(self): 572 573 "Return the static URL/directory identifier for the Wiki." 574 575 return "moin_static%s" % self.moin_version.replace(".", "") 576 577 def get_plugin_directory(self, plugin_type): 578 579 "Return the directory for plugins of the given 'plugin_type'." 580 581 data_dir = join(self.common_dir, "data") 582 return join(data_dir, "plugin", plugin_type) 583 584 def limited_hosting(self): 585 586 "Return whether limited Web hosting is being used." 587 588 return not self.web_site_dir 589 590 def ensure_directories(self): 591 592 "Make sure that all the directories are available." 593 594 for d in (self.common_dir, self.web_app_dir, self.web_static_dir, self.web_site_dir): 595 if d is not None and not exists(d): 596 makedirs(d) 597 598 def get_theme_directories(self, theme_name=None): 599 600 """ 601 Return tuples of the form (theme name, theme directory) for all themes, 602 or for a single theme if the optional 'theme_name' is specified. 603 """ 604 605 filenames = theme_name and [theme_name] or listdir(self.htdocs_dir) 606 directories = [] 607 608 for filename in filenames: 609 theme_dir = join(self.htdocs_dir, filename) 610 611 if not exists(theme_dir) or not isdir(theme_dir): 612 continue 613 614 directories.append((filename, theme_dir)) 615 616 return directories 617 618 # Main methods. 619 620 def setup(self): 621 622 "Set up the installation." 623 624 self.ensure_directories() 625 self.install_moin() 626 self.edit_moin_script() 627 self._setup_wiki() 628 629 def setup_wiki(self): 630 631 "Set up a Wiki without installing MoinMoin." 632 633 self.ensure_directories() 634 self._setup_wiki() 635 636 def _setup_wiki(self): 637 638 "Set up a Wiki without installing MoinMoin." 639 640 self.install_data() 641 self.install_static_data() 642 self.configure_moin() 643 self.add_superuser() 644 self.edit_moin_web_script(self.make_site_files()) 645 self.make_post_install_script() 646 647 if self.moin_version.startswith("1.9"): 648 note("You may need to visit the LanguageSetup page in the Wiki to create the standard set of pages.") 649 650 def install_moin(self): 651 652 "Enter the distribution directory and run the setup script." 653 654 # NOTE: Possibly check for an existing installation and skip repeated 655 # NOTE: installation attempts. 656 657 if not self.moin_distribution: 658 raise SetupException, "Cannot install MoinMoin without a 'moin_distribution' setting being defined." 659 660 this_dir = os.getcwd() 661 chdir(self.moin_distribution) 662 663 log_filename = "install-%s.log" % split(self.common_dir)[-1] 664 665 status("Installing MoinMoin in %s..." % self.prefix) 666 667 install_cmd = "install" 668 options = "--prefix='%s' --record='%s'" % (self.prefix, log_filename) 669 670 os.system("python setup.py --quiet %s %s --force" % (install_cmd, options)) 671 672 chdir(this_dir) 673 674 def install_data(self): 675 676 "Install Wiki data into an instance." 677 678 moin_data = self.get_moin_data() 679 680 if not moin_data: 681 raise SetupException, \ 682 "Cannot install MoinMoin data without either a 'moin_distribution' or a 'prefix' setting being defined." 683 684 # The default wikiconfig assumes data and underlay in the same directory. 685 686 status("Installing data and underlay in %s..." % self.common_dir) 687 688 for d in ("data", "underlay"): 689 source = join(moin_data, d) 690 source_tar = source + extsep + "tar" 691 692 if exists(source): 693 shutil.copytree(source, join(self.common_dir, d)) 694 elif exists(source_tar): 695 696 note("Copying archive %s instead of directory %s. Running...\n" 697 "make pagepacks\n" 698 "in the distribution directory should rectify this." % (source_tar, source)) 699 700 shutil.copy(source_tar, self.common_dir) 701 os.system("tar xf %s -C %s" % (source_tar, self.common_dir)) 702 else: 703 status("Could not copy %s into installed Wiki." % d) 704 705 def install_static_data(self): 706 707 "Install static Web data if appropriate." 708 709 if not exists(self.htdocs_dir): 710 mkdir(self.htdocs_dir) 711 712 for item in listdir(self.htdocs_dir_source): 713 path = join(self.htdocs_dir_source, item) 714 if isdir(path): 715 shutil.copytree(path, join(self.htdocs_dir, item)) 716 else: 717 shutil.copy(path, join(self.htdocs_dir, item)) 718 719 def configure_moin(self, reset=0): 720 721 """ 722 Edit the Wiki configuration file. If the optional 'reset' parameter is 723 specified as a true value, a default configuration will be copied from 724 the distribution if appropriate. 725 """ 726 727 moin_data = self.get_moin_data() 728 729 if not moin_data: 730 raise SetupException, \ 731 "Cannot configure MoinMoin without either a 'moin_distribution' or a 'prefix' setting being defined." 732 733 # NOTE: MoinMoin usually uses an apparently common URL space associated 734 # NOTE: with the version, but more specific locations are probably 735 # NOTE: acceptable if less efficient. 736 737 url_prefix_static = "%r" % self.static_url_path 738 739 # Use a farm configuration file. 740 741 if self.farm_config: 742 wikiconfig_py = self.farm_config 743 744 # Or copy the Wiki configuration file from the distribution. 745 746 else: 747 wikiconfig_py = join(self.common_dir, "wikiconfig.py") 748 749 if not exists(wikiconfig_py) or reset: 750 shutil.copyfile(join(moin_data, "config", "wikiconfig.py"), wikiconfig_py) 751 752 status("Editing configuration from %s..." % wikiconfig_py) 753 754 # Edit the Wiki configuration file. 755 756 wikiconfig = Configuration(wikiconfig_py) 757 758 try: 759 wikiconfig.set("url_prefix_static", url_prefix_static, raw=1) 760 if self.superuser: 761 wikiconfig.set("superuser", [self.superuser]) 762 wikiconfig.set("acl_rights_before", u"%s:read,write,delete,revert,admin" % self.superuser) 763 else: 764 note("Superuser not defined. The ACL rules should be fixed in the configuration.") 765 766 # Edit any created Wiki configuration. 767 768 if not self.site_config: 769 self._configure_moin(wikiconfig) 770 771 finally: 772 wikiconfig.close() 773 774 # Edit any separate site configuration file. 775 776 if self.site_config: 777 status("Editing configuration from %s..." % self.site_config) 778 779 wikiconfig = Configuration(self.site_config) 780 781 try: 782 self._configure_moin(wikiconfig) 783 finally: 784 wikiconfig.close() 785 786 def _configure_moin(self, wikiconfig): 787 788 """ 789 Configure Moin, accessing the configuration file using 'wikiconfig'. 790 """ 791 792 # Specific site configurations also appear to need 'data_dir', even in 793 # 1.9. 794 795 if not self.moin_version.startswith("1.9") or self.site_config: 796 data_dir = join(self.common_dir, "data") 797 data_underlay_dir = join(self.common_dir, "underlay") 798 799 wikiconfig.set("data_dir", data_dir) 800 wikiconfig.set("data_underlay_dir", data_underlay_dir) 801 802 wikiconfig.set("site_name", self.site_name) 803 wikiconfig.set("page_front_page", self.front_page_name, count=1) 804 805 if self.theme_default is not None: 806 wikiconfig.set("theme_default", self.theme_default) 807 808 def edit_moin_script(self): 809 810 "Edit the moin script." 811 812 moin_script = self.get_moin_script() 813 status("Editing moin script at %s..." % moin_script) 814 815 s = readfile(moin_script) 816 s = s.replace("#import sys", "import sys\nsys.path.insert(0, %r)" % self.prefix_site_packages) 817 818 writefile(moin_script, s) 819 820 def edit_moin_web_script(self, site_file_configured=1): 821 822 "Edit and install CGI script." 823 824 # NOTE: CGI only so far. 825 # NOTE: Permissions should be checked. 826 827 moin_data = self.get_moin_data() 828 829 if self.moin_version.startswith("1.9"): 830 moin_cgi_script = "moin.fcgi" 831 else: 832 moin_cgi_script = "moin.cgi" 833 834 moin_cgi = join(moin_data, "server", moin_cgi_script) 835 moin_cgi_installed = join(self.web_app_dir, "moin.cgi") 836 837 status("Editing moin.cgi script from %s, writing to %s..." % (moin_cgi, moin_cgi_installed)) 838 839 s = readfile(moin_cgi) 840 s = moin_cgi_prefix.sub("sys.path.insert(0, %r)" % self.prefix_site_packages, s) 841 s = moin_cgi_wikiconfig.sub("sys.path.insert(0, %r)" % self.common_dir, s) 842 843 # Handle differences in script names when using limited hosting with 844 # URL rewriting. 845 846 if self.limited_hosting(): 847 if not site_file_configured: 848 note("Site file not configured: script name not changed.") 849 else: 850 if self.moin_version.startswith("1.9"): 851 s = moin_cgi_fix_script_name.sub(r"\1\2 %r" % self.url_path, s) 852 else: 853 s = moin_cgi_properties.sub(r"\1\2 %r" % {"script_name" : self.url_path}, s) 854 855 # NOTE: Use CGI for now. 856 857 if self.moin_version.startswith("1.9"): 858 s = moin_cgi_force_cgi.sub(r"\1", s) 859 860 writefile(moin_cgi_installed, s) 861 os.system("chmod a+rx '%s'" % moin_cgi_installed) 862 863 # Fix the cause of opaque errors in some Apache environments. 864 865 os.system("chmod go-w '%s'" % moin_cgi_installed) 866 867 def add_superuser(self): 868 869 "Add the superuser account." 870 871 if not self.superuser: 872 return 873 874 print "Creating superuser", self.superuser, "using..." 875 email = raw_input("E-mail address: ") 876 password = getpass("Password: ") 877 878 path = os.environ.get("PYTHONPATH", "") 879 880 if path: 881 os.environ["PYTHONPATH"] = path + ":" + self.common_dir 882 else: 883 os.environ["PYTHONPATH"] = self.common_dir 884 885 cmd = "%s --config-dir='%s' account create --name='%s' --email='%s' --password='%s'" % ( 886 self.get_moin_script(), self.common_dir, self.superuser, email, password) 887 os.system(cmd) 888 889 if path: 890 os.environ["PYTHONPATH"] = path 891 else: 892 del os.environ["PYTHONPATH"] 893 894 def make_site_files(self): 895 896 "Make the Apache site files." 897 898 # NOTE: Using local namespace for substitution. 899 900 # Where the site definitions and applications directories are different, 901 # use a normal site definition. 902 903 if not self.limited_hosting(): 904 905 site_def = join(self.web_site_dir, self.site_identifier) 906 907 s = apache_site % self.__dict__ 908 s += apache_site_extra % self.__dict__ 909 910 status("Writing Apache site definitions to %s..." % site_def) 911 writefile(site_def, s) 912 913 note("Copy the site definitions to the appropriate sites directory if appropriate.\n" 914 "Then, make sure that the site is enabled by running the appropriate tools (such as a2ensite).") 915 916 return 1 917 918 # Otherwise, use an .htaccess file. 919 920 else: 921 site_def = join(self.web_app_dir, ".htaccess") 922 923 s = apache_htaccess_combined_mod_rewrite % self.__dict__ 924 925 status("Writing .htaccess file to %s..." % site_def) 926 try: 927 writefile(site_def, s) 928 except IOError: 929 note("The .htaccess file could not be written. This will also affect the script name setting.") 930 return 0 931 else: 932 return 1 933 934 def make_post_install_script(self): 935 936 "Write a post-install script with additional actions." 937 938 # Work out whether setfacl works. 939 940 fd, temp_filename = tempfile.mkstemp(dir=self.common_dir) 941 os.close(fd) 942 943 have_setfacl = os.system("setfacl -m user:%(web_user)s:r %(file)s > /dev/null 2>&1" % { 944 "web_user" : self.web_user, "file" : temp_filename}) == 0 945 946 remove(temp_filename) 947 948 # Create the scripts. 949 950 this_user = os.environ["USER"] 951 postinst_scripts = "moinsetup-post-chown.sh", "moinsetup-post-setfacl.sh" 952 953 vars = {} 954 vars.update(Installation.__dict__) 955 vars.update(self.__dict__) 956 vars.update(locals()) 957 958 for postinst_script, start, extra in [ 959 (postinst_scripts[0], postsetup_chown_chmod, postsetup_chown_extra), 960 (postinst_scripts[1], postsetup_setfacl, postsetup_setfacl_extra) 961 ]: 962 963 s = start % vars 964 s += extra % vars 965 966 writefile(postinst_script, s) 967 chmod(postinst_script, 0755) 968 969 if have_setfacl: 970 note("Run %s to set file ownership and permissions.\n" 971 "If this somehow fails..." % postinst_scripts[1]) 972 973 note("Run %s as root to set file ownership and permissions." % postinst_scripts[0]) 974 975 # Accessory methods. 976 977 def reconfigure_moin(self, name=None, value=None, raw=0): 978 979 """ 980 Edit the installed Wiki configuration file, setting a parameter with any 981 given 'name' to the given 'value', treating the value as a raw 982 expression (not a string) if 'raw' is set to a true value. 983 984 If 'name' and the remaining parameters are omitted, the default 985 configuration activity is performed. 986 987 If the 'site_config' setting is defined, the specific site configuration 988 will be changed. 989 """ 990 991 if self.site_config: 992 wikiconfig_py = self.site_config 993 else: 994 wikiconfig_py = join(self.common_dir, "wikiconfig.py") 995 996 status("Editing configuration from %s..." % wikiconfig_py) 997 998 wikiconfig = Configuration(wikiconfig_py) 999 1000 try: 1001 # Perform default configuration. 1002 1003 if name is None and value is None: 1004 self._configure_moin(wikiconfig) 1005 else: 1006 wikiconfig.set(name, value, raw=raw) 1007 1008 finally: 1009 wikiconfig.close() 1010 1011 def set_auth_method(self, method_name): 1012 1013 """ 1014 Edit the installed Wiki configuration file, configuring the 1015 authentication method having the given 'method_name'. 1016 1017 If the 'farm_config' setting is defined, the Wiki farm configuration 1018 will be changed. 1019 """ 1020 1021 if self.farm_config: 1022 wikiconfig_py = self.farm_config 1023 else: 1024 wikiconfig_py = join(self.common_dir, "wikiconfig.py") 1025 1026 status("Editing configuration from %s..." % wikiconfig_py) 1027 1028 wikiconfig = Configuration(wikiconfig_py) 1029 1030 try: 1031 # OpenID authentication. 1032 1033 if method_name.lower() == "openid": 1034 wikiconfig.set_import("MoinMoin.auth.openidrp", ["OpenIDAuth"]) 1035 1036 if self.moin_version.startswith("1.9"): 1037 if wikiconfig.get("cookie_lifetime"): 1038 wikiconfig.replace("cookie_lifetime", "(12, 12)", raw=1) 1039 else: 1040 wikiconfig.set("cookie_lifetime", "(12, 12)", raw=1) 1041 else: 1042 if wikiconfig.get("anonymous_session_lifetime"): 1043 wikiconfig.replace("anonymous_session_lifetime", "1000", raw=1) 1044 else: 1045 wikiconfig.set("anonymous_session_lifetime", "1000", raw=1) 1046 1047 auth_object = "OpenIDAuth()" 1048 1049 # Default Moin authentication. 1050 1051 elif method_name.lower() in ("moin", "default"): 1052 wikiconfig.set_import("MoinMoin.auth", ["MoinAuth"]) 1053 auth_object = "MoinAuth()" 1054 1055 # REMOTE_USER authentication. 1056 1057 elif method_name.lower() in ("given", "remote-user"): 1058 wikiconfig.set_import("MoinMoin.auth.http", ["HTTPAuth"]) 1059 auth_object = "HTTPAuth(autocreate=True)" 1060 1061 # Other methods are not currently supported. 1062 1063 else: 1064 return 1065 1066 # Edit the authentication setting. 1067 1068 auth = wikiconfig.get("auth") 1069 if auth: 1070 wikiconfig.replace("auth", "%s + [%s]" % (auth, auth_object), raw=1) 1071 else: 1072 wikiconfig.set("auth", "[%s]" % auth_object, raw=1) 1073 1074 finally: 1075 wikiconfig.close() 1076 1077 def migrate_instance(self, test=0, change_site=0): 1078 1079 """ 1080 Migrate the Wiki to the currently supported layout. If 'test' is 1081 specified and set to a non-empty or true value, only print whether the 1082 migration can be performed. 1083 1084 If 'change_site' is specified and set to a non-empty or true value, the 1085 site definitions will be updated; this will overwrite any changes made 1086 to the site definitions after they were last produced by moinsetup, and 1087 care must be taken to ensure that things like access controls are 1088 re-added to the definitions after this action is performed. 1089 """ 1090 1091 conf_dir = join(self.common_dir, "conf") 1092 if exists(conf_dir): 1093 for filename in listdir(conf_dir): 1094 pathname = join(conf_dir, filename) 1095 target = join(self.common_dir, filename) 1096 if not exists(target): 1097 print "Move", filename, "from conf directory." 1098 if not test: 1099 rename(pathname, target) 1100 else: 1101 print "No conf directory." 1102 1103 wikidata = join(self.common_dir, "wikidata") 1104 if exists(wikidata): 1105 htdocs = join(wikidata, "share", "moin", "htdocs") 1106 if exists(htdocs): 1107 target = join(self.common_dir, "htdocs") 1108 if not exists(target): 1109 print "Move htdocs from wikidata directory." 1110 if not test: 1111 rename(htdocs, target) 1112 else: 1113 print "No wikidata directory." 1114 1115 # Remove links and directories. 1116 1117 for name in ("conf", "wikidata"): 1118 d = join(self.common_dir, name) 1119 if islink(d): 1120 print "Remove %s symbolic link." % name 1121 if not test: 1122 remove(d) 1123 1124 if isdir(conf_dir): 1125 print "Remove conf directory." 1126 if not test: 1127 rmdir(conf_dir) 1128 1129 # Add any missing htdocs directory. 1130 1131 if not exists(self.htdocs_dir): 1132 print "Copy htdocs into the instance." 1133 if not test: 1134 self.install_static_data() 1135 1136 # Now attempt to reconfigure the Wiki. 1137 1138 print "Reconfigure the Wiki, the Web script%s." % (change_site and " and the site files" or "") 1139 if not test: 1140 self.configure_moin() 1141 self.edit_moin_web_script() 1142 if change_site: 1143 self.make_site_files() 1144 1145 def install_theme(self, theme_dir, theme_name=None): 1146 1147 """ 1148 Install Wiki theme provided in the given 'theme_dir' having the given 1149 optional 'theme_name' (if different from the 'theme_dir' name). 1150 """ 1151 1152 theme_dir = normpath(theme_dir) 1153 theme_name = theme_name or split(theme_dir)[-1] 1154 theme_module = join(theme_dir, theme_name + extsep + "py") 1155 1156 plugin_theme_dir = self.get_plugin_directory("theme") 1157 1158 # Copy the theme module. 1159 1160 status("Copying theme module to %s..." % plugin_theme_dir) 1161 1162 shutil.copy(theme_module, plugin_theme_dir) 1163 1164 # Copy the resources. 1165 1166 resources_dir = join(self.htdocs_dir, theme_name) 1167 1168 if not exists(resources_dir): 1169 mkdir(resources_dir) 1170 1171 status("Copying theme resources to %s..." % resources_dir) 1172 1173 for d in ("css", "img"): 1174 target_dir = join(resources_dir, d) 1175 if exists(target_dir): 1176 status("Replacing %s..." % target_dir) 1177 shutil.rmtree(target_dir) 1178 shutil.copytree(join(theme_dir, d), target_dir) 1179 1180 # Copy additional resources from other themes. 1181 1182 resources_source_dir = join(self.htdocs_dir, self.theme_master) 1183 target_dir = join(resources_dir, "css") 1184 1185 status("Copying resources from %s..." % resources_source_dir) 1186 1187 for css_file in self.extra_theme_css_files: 1188 css_file_path = join(resources_source_dir, "css", css_file) 1189 if exists(css_file_path): 1190 shutil.copy(css_file_path, target_dir) 1191 1192 note("Don't forget to add theme resources for extensions for this theme.\n" 1193 "Don't forget to edit this theme's stylesheets for extensions.") 1194 1195 def install_extension_package(self, extension_dir): 1196 1197 "Install any libraries from 'extension_dir' using a setup script." 1198 1199 this_dir = os.getcwd() 1200 chdir(extension_dir) 1201 1202 options = "install --install-lib=%s" % self.prefix_site_packages 1203 1204 os.system("python setup.py %s" % options) 1205 chdir(this_dir) 1206 1207 def install_plugins(self, plugins_dir, plugin_type): 1208 1209 """ 1210 Install Wiki actions provided in the given 'plugins_dir' of the 1211 specified 'plugin_type'. 1212 """ 1213 1214 plugin_target_dir = self.get_plugin_directory(plugin_type) 1215 1216 # Copy the modules. 1217 1218 status("Copying %s modules to %s..." % (plugin_type, plugin_target_dir)) 1219 1220 for module in glob(join(plugins_dir, "*%spy" % extsep)): 1221 shutil.copy(module, plugin_target_dir) 1222 1223 def install_actions(self, actions_dir): 1224 1225 "Install Wiki actions provided in the given 'actions_dir'." 1226 1227 self.install_plugins(actions_dir, "action") 1228 1229 def install_macros(self, macros_dir): 1230 1231 "Install Wiki macros provided in the given 'macros_dir'." 1232 1233 self.install_plugins(macros_dir, "macro") 1234 1235 def install_parsers(self, parsers_dir): 1236 1237 "Install Wiki parsers provided in the given 'parsers_dir'." 1238 1239 self.install_plugins(parsers_dir, "parser") 1240 1241 def install_event_handlers(self, events_dir): 1242 1243 "Install Wiki event handlers provided in the given 'events_dir'." 1244 1245 self.install_plugins(events_dir, "events") 1246 1247 def install_theme_resources(self, theme_resources_dir, theme_name=None): 1248 1249 """ 1250 Install theme resources provided in the given 'theme_resources_dir'. If 1251 a specific 'theme_name' is given, only that theme will be given the 1252 specified resources. 1253 """ 1254 1255 for theme_name, theme_dir in self.get_theme_directories(theme_name): 1256 1257 # Copy the resources. 1258 1259 copied = 0 1260 1261 for d in ("css", "img"): 1262 source_dir = join(theme_resources_dir, d) 1263 target_dir = join(theme_dir, d) 1264 1265 if not exists(target_dir): 1266 continue 1267 1268 for resource in glob(join(source_dir, "*%s*" % extsep)): 1269 shutil.copy(resource, target_dir) 1270 copied = 1 1271 1272 if copied: 1273 status("Copied theme resources into %s..." % theme_dir) 1274 1275 note("Don't forget to edit theme stylesheets for any extensions.") 1276 1277 def edit_theme_stylesheet(self, theme_stylesheet, imported_stylesheet, action="ensure", theme_name=None): 1278 1279 """ 1280 Edit the given 'theme_stylesheet', ensuring (or removing) a reference to 1281 the 'imported_stylesheet' according to the given 'action' (optional, 1282 defaulting to "ensure"). If a specific 'theme_name' is given, only that 1283 theme will be affected. 1284 """ 1285 1286 if action == "ensure": 1287 ensure = 1 1288 elif action == "remove": 1289 ensure = 0 1290 else: 1291 error("Action %s not valid: it must be given as either 'ensure' or 'remove'." % action) 1292 return 1293 1294 for theme_name, theme_dir in self.get_theme_directories(theme_name): 1295 1296 # Locate the resources. 1297 1298 css_dir = join(theme_dir, "css") 1299 1300 if not exists(css_dir): 1301 continue 1302 1303 theme_stylesheet_filename = join(css_dir, theme_stylesheet) 1304 imported_stylesheet_filename = join(css_dir, imported_stylesheet) 1305 1306 if not exists(theme_stylesheet_filename): 1307 error("Stylesheet %s not defined in theme %s." % (theme_stylesheet, theme_name)) 1308 continue 1309 1310 if not exists(imported_stylesheet_filename): 1311 error("Stylesheet %s not defined in theme %s." % (imported_stylesheet, theme_name)) 1312 continue 1313 1314 # Edit the resources. 1315 1316 s = readfile(theme_stylesheet_filename) 1317 after_point = 0 1318 1319 for stylesheet_import in css_import_stylesheet.finditer(s): 1320 before, filename, after = stylesheet_import.groups() 1321 before_point, after_point = stylesheet_import.span() 1322 1323 # Test the import for a reference to the requested imported 1324 # stylesheet. 1325 1326 if filename == imported_stylesheet: 1327 if ensure: 1328 break 1329 else: 1330 if s[after_point:after_point+1] == "\n": 1331 after_point += 1 1332 s = "%s%s" % (s[:before_point], s[after_point:]) 1333 1334 status("Removing %s from %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 1335 writefile(theme_stylesheet_filename, s) 1336 break 1337 1338 # Where no import references the imported stylesheet, insert a 1339 # reference into the theme stylesheet. 1340 1341 else: 1342 if ensure: 1343 1344 # Assume that the stylesheet can follow other imports. 1345 1346 if s[after_point:after_point+1] == "\n": 1347 after_point += 1 1348 s = "%s%s\n%s" % (s[:after_point], '@import "%s";' % imported_stylesheet, s[after_point:]) 1349 1350 status("Adding %s to %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 1351 writefile(theme_stylesheet_filename, s) 1352 1353 def make_page_package(self, page_directory, package_filename): 1354 1355 """ 1356 Make a package containing the pages in 'page_directory', using the 1357 filenames as the page names, and writing the package to a file with the 1358 given 'package_filename'. 1359 """ 1360 1361 package = ZipFile(package_filename, "w") 1362 1363 try: 1364 script = ["MoinMoinPackage|1"] 1365 1366 for filename in listdir(page_directory): 1367 pathname = join(page_directory, filename) 1368 1369 # Add files as pages having the filename as page name. 1370 1371 if os.path.isfile(pathname): 1372 package.write(pathname, filename) 1373 script.append("AddRevision|%s|%s" % (filename, filename)) 1374 1375 # Add directories ending with "-attachments" as collections of 1376 # attachments for a particular page. 1377 1378 elif os.path.isdir(pathname) and filename.endswith("-attachments"): 1379 parent = filename[:-len("-attachments")] 1380 1381 # Add each file as an attachment. 1382 1383 for attachment in listdir(pathname): 1384 zipname = "%s_%s" % (filename, attachment) 1385 package.write(join(pathname, attachment), zipname) 1386 script.append("AddAttachment|%s|%s|%s||" % (zipname, attachment, parent)) 1387 1388 package.writestr("MOIN_PACKAGE", "\n".join(script)) 1389 1390 finally: 1391 package.close() 1392 1393 def install_page_package(self, package_filename): 1394 1395 """ 1396 Install a package from the file with the given 'package_filename'. 1397 """ 1398 1399 path = os.environ.get("PYTHONPATH", "") 1400 1401 conf_dir = self.get_wikiconfig_directory() 1402 1403 if path: 1404 os.environ["PYTHONPATH"] = path + ":" + self.prefix_site_packages + ":" + conf_dir 1405 else: 1406 os.environ["PYTHONPATH"] = self.prefix_site_packages + ":" + conf_dir 1407 1408 installer = join(self.prefix_site_packages, "MoinMoin", "packages.py") 1409 cmd = "python %s i %s" % (installer, package_filename) 1410 os.system(cmd) 1411 1412 if path: 1413 os.environ["PYTHONPATH"] = path 1414 else: 1415 del os.environ["PYTHONPATH"] 1416 1417 def show_methods(): 1418 print "Methods:" 1419 print 1420 for method_name in Installation.method_names: 1421 doc = getattr(Installation, method_name).__doc__.strip() 1422 print "%-30s%-s" % (method_name, format(doc, 30)) 1423 print 1424 1425 # Command line option syntax. 1426 1427 syntax_description = "[ -f <config-filename> ] ( -m <method> | --method=METHOD ) [ <method-argument> ... ]" 1428 1429 # Main program. 1430 1431 if __name__ == "__main__": 1432 from ConfigParser import ConfigParser 1433 import sys, cmdsyntax 1434 1435 # Check the command syntax. 1436 1437 syntax = cmdsyntax.Syntax(syntax_description) 1438 try: 1439 matches = syntax.get_args(sys.argv[1:]) 1440 args = matches[0] 1441 except IndexError: 1442 print "Syntax:" 1443 print sys.argv[0], syntax_description 1444 print 1445 show_methods() 1446 sys.exit(1) 1447 1448 # Obtain configuration details. 1449 1450 try: 1451 config_filename = args.get("config-filename", "moinsetup.cfg") 1452 1453 if not exists(config_filename): 1454 print "Configuration", config_filename, "not found." 1455 sys.exit(1) 1456 1457 config = ConfigParser() 1458 config.read(config_filename) 1459 1460 # Obtain as many arguments as needed from the configuration. 1461 1462 config_arguments = dict(config.items("installation") + config.items("site")) 1463 method_arguments = args.get("method-argument", []) 1464 1465 # Attempt to initialise the configuration. 1466 1467 installation = Installation(**config_arguments) 1468 1469 except TypeError, exc: 1470 print "Error:" 1471 print 1472 print exc.args[0] 1473 print 1474 print "Configuration settings:" 1475 print 1476 print Installation.__init__.__doc__ 1477 print 1478 sys.exit(1) 1479 1480 # Obtain the method. 1481 1482 try: 1483 method = getattr(installation, args["method"]) 1484 except AttributeError: 1485 show_methods() 1486 sys.exit(1) 1487 1488 try: 1489 method(*method_arguments) 1490 except TypeError, exc: 1491 print "Error:" 1492 print 1493 print exc.args[0] 1494 print 1495 print "Method documentation:" 1496 print 1497 print method.__doc__ 1498 print 1499 sys.exit(1) 1500 1501 # vim: tabstop=4 expandtab shiftwidth=4