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