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