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