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