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