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