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, change_site=0): 1066 1067 """ 1068 Migrate the Wiki to the currently supported layout. If 'test' is 1069 specified and set to a non-empty or true value, only print whether the 1070 migration can be performed. 1071 1072 If 'change_site' is specified and set to a non-empty or true value, the 1073 site definitions will be updated; this will overwrite any changes made 1074 to the site definitions after they were last produced by moinsetup, and 1075 care must be taken to ensure that things like access controls are 1076 re-added to the definitions after this action is performed. 1077 """ 1078 1079 conf_dir = join(self.common_dir, "conf") 1080 if exists(conf_dir): 1081 for filename in listdir(conf_dir): 1082 pathname = join(conf_dir, filename) 1083 target = join(self.common_dir, filename) 1084 if not exists(target): 1085 print "Move", filename, "from conf directory." 1086 if not test: 1087 rename(pathname, target) 1088 else: 1089 print "No conf directory." 1090 1091 wikidata = join(self.common_dir, "wikidata") 1092 if exists(wikidata): 1093 htdocs = join(wikidata, "share", "moin", "htdocs") 1094 if exists(htdocs): 1095 target = join(self.common_dir, "htdocs") 1096 if not exists(target): 1097 print "Move htdocs from wikidata directory." 1098 if not test: 1099 rename(htdocs, target) 1100 else: 1101 print "No wikidata directory." 1102 1103 # Remove links and directories. 1104 1105 for name in ("conf", "wikidata"): 1106 d = join(self.common_dir, name) 1107 if islink(d): 1108 print "Remove %s symbolic link." % name 1109 if not test: 1110 remove(d) 1111 1112 if isdir(conf_dir): 1113 print "Remove conf directory." 1114 if not test: 1115 rmdir(conf_dir) 1116 1117 # Add any missing htdocs directory. 1118 1119 if not exists(self.htdocs_dir): 1120 print "Copy htdocs into the instance." 1121 if not test: 1122 self.install_static_data() 1123 1124 # Now attempt to reconfigure the Wiki. 1125 1126 print "Reconfigure the Wiki, the Web script%s." % (change_site and " and the site files" or "") 1127 if not test: 1128 self.configure_moin() 1129 self.edit_moin_web_script() 1130 if change_site: 1131 self.make_site_files() 1132 1133 def install_theme(self, theme_dir, theme_name=None): 1134 1135 """ 1136 Install Wiki theme provided in the given 'theme_dir' having the given 1137 optional 'theme_name' (if different from the 'theme_dir' name). 1138 """ 1139 1140 theme_dir = normpath(theme_dir) 1141 theme_name = theme_name or split(theme_dir)[-1] 1142 theme_module = join(theme_dir, theme_name + extsep + "py") 1143 1144 plugin_theme_dir = self.get_plugin_directory("theme") 1145 1146 # Copy the theme module. 1147 1148 status("Copying theme module to %s..." % plugin_theme_dir) 1149 1150 shutil.copy(theme_module, plugin_theme_dir) 1151 1152 # Copy the resources. 1153 1154 resources_dir = join(self.htdocs_dir, theme_name) 1155 1156 if not exists(resources_dir): 1157 mkdir(resources_dir) 1158 1159 status("Copying theme resources to %s..." % resources_dir) 1160 1161 for d in ("css", "img"): 1162 target_dir = join(resources_dir, d) 1163 if exists(target_dir): 1164 status("Replacing %s..." % target_dir) 1165 shutil.rmtree(target_dir) 1166 shutil.copytree(join(theme_dir, d), target_dir) 1167 1168 # Copy additional resources from other themes. 1169 1170 resources_source_dir = join(self.htdocs_dir, self.theme_master) 1171 target_dir = join(resources_dir, "css") 1172 1173 status("Copying resources from %s..." % resources_source_dir) 1174 1175 for css_file in self.extra_theme_css_files: 1176 css_file_path = join(resources_source_dir, "css", css_file) 1177 if exists(css_file_path): 1178 shutil.copy(css_file_path, target_dir) 1179 1180 note("Don't forget to add theme resources for extensions for this theme.\n" 1181 "Don't forget to edit this theme's stylesheets for extensions.") 1182 1183 def install_extension_package(self, extension_dir): 1184 1185 "Install any libraries from 'extension_dir' using a setup script." 1186 1187 this_dir = os.getcwd() 1188 chdir(extension_dir) 1189 1190 options = "install --install-lib=%s" % self.prefix_site_packages 1191 1192 os.system("python setup.py %s" % options) 1193 chdir(this_dir) 1194 1195 def install_plugins(self, plugins_dir, plugin_type): 1196 1197 """ 1198 Install Wiki actions provided in the given 'plugins_dir' of the 1199 specified 'plugin_type'. 1200 """ 1201 1202 plugin_target_dir = self.get_plugin_directory(plugin_type) 1203 1204 # Copy the modules. 1205 1206 status("Copying %s modules to %s..." % (plugin_type, plugin_target_dir)) 1207 1208 for module in glob(join(plugins_dir, "*%spy" % extsep)): 1209 shutil.copy(module, plugin_target_dir) 1210 1211 def install_actions(self, actions_dir): 1212 1213 "Install Wiki actions provided in the given 'actions_dir'." 1214 1215 self.install_plugins(actions_dir, "action") 1216 1217 def install_macros(self, macros_dir): 1218 1219 "Install Wiki macros provided in the given 'macros_dir'." 1220 1221 self.install_plugins(macros_dir, "macro") 1222 1223 def install_parsers(self, parsers_dir): 1224 1225 "Install Wiki macros provided in the given 'parsers_dir'." 1226 1227 self.install_plugins(parsers_dir, "parser") 1228 1229 def install_theme_resources(self, theme_resources_dir, theme_name=None): 1230 1231 """ 1232 Install theme resources provided in the given 'theme_resources_dir'. If 1233 a specific 'theme_name' is given, only that theme will be given the 1234 specified resources. 1235 """ 1236 1237 for theme_name, theme_dir in self.get_theme_directories(theme_name): 1238 1239 # Copy the resources. 1240 1241 copied = 0 1242 1243 for d in ("css", "img"): 1244 source_dir = join(theme_resources_dir, d) 1245 target_dir = join(theme_dir, d) 1246 1247 if not exists(target_dir): 1248 continue 1249 1250 for resource in glob(join(source_dir, "*%s*" % extsep)): 1251 shutil.copy(resource, target_dir) 1252 copied = 1 1253 1254 if copied: 1255 status("Copied theme resources into %s..." % theme_dir) 1256 1257 note("Don't forget to edit theme stylesheets for any extensions.") 1258 1259 def edit_theme_stylesheet(self, theme_stylesheet, imported_stylesheet, action="ensure", theme_name=None): 1260 1261 """ 1262 Edit the given 'theme_stylesheet', ensuring (or removing) a reference to 1263 the 'imported_stylesheet' according to the given 'action' (optional, 1264 defaulting to "ensure"). If a specific 'theme_name' is given, only that 1265 theme will be affected. 1266 """ 1267 1268 if action == "ensure": 1269 ensure = 1 1270 elif action == "remove": 1271 ensure = 0 1272 else: 1273 error("Action %s not valid: it must be given as either 'ensure' or 'remove'." % action) 1274 return 1275 1276 for theme_name, theme_dir in self.get_theme_directories(theme_name): 1277 1278 # Locate the resources. 1279 1280 css_dir = join(theme_dir, "css") 1281 1282 if not exists(css_dir): 1283 continue 1284 1285 theme_stylesheet_filename = join(css_dir, theme_stylesheet) 1286 imported_stylesheet_filename = join(css_dir, imported_stylesheet) 1287 1288 if not exists(theme_stylesheet_filename): 1289 error("Stylesheet %s not defined in theme %s." % (theme_stylesheet, theme_name)) 1290 continue 1291 1292 if not exists(imported_stylesheet_filename): 1293 error("Stylesheet %s not defined in theme %s." % (imported_stylesheet, theme_name)) 1294 continue 1295 1296 # Edit the resources. 1297 1298 s = readfile(theme_stylesheet_filename) 1299 after_point = 0 1300 1301 for stylesheet_import in css_import_stylesheet.finditer(s): 1302 before, filename, after = stylesheet_import.groups() 1303 before_point, after_point = stylesheet_import.span() 1304 1305 # Test the import for a reference to the requested imported 1306 # stylesheet. 1307 1308 if filename == imported_stylesheet: 1309 if ensure: 1310 break 1311 else: 1312 if s[after_point:after_point+1] == "\n": 1313 after_point += 1 1314 s = "%s%s" % (s[:before_point], s[after_point:]) 1315 1316 status("Removing %s from %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 1317 writefile(theme_stylesheet_filename, s) 1318 break 1319 1320 # Where no import references the imported stylesheet, insert a 1321 # reference into the theme stylesheet. 1322 1323 else: 1324 if ensure: 1325 1326 # Assume that the stylesheet can follow other imports. 1327 1328 if s[after_point:after_point+1] == "\n": 1329 after_point += 1 1330 s = "%s%s\n%s" % (s[:after_point], '@import "%s";' % imported_stylesheet, s[after_point:]) 1331 1332 status("Adding %s to %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 1333 writefile(theme_stylesheet_filename, s) 1334 1335 def make_page_package(self, page_directory, package_filename): 1336 1337 """ 1338 Make a package containing the pages in 'page_directory', using the 1339 filenames as the page names, and writing the package to a file with the 1340 given 'package_filename'. 1341 """ 1342 1343 package = ZipFile(package_filename, "w") 1344 1345 try: 1346 script = ["MoinMoinPackage|1"] 1347 1348 for filename in listdir(page_directory): 1349 pathname = join(page_directory, filename) 1350 1351 # Add files as pages having the filename as page name. 1352 1353 if os.path.isfile(pathname): 1354 package.write(pathname, filename) 1355 script.append("AddRevision|%s|%s" % (filename, filename)) 1356 1357 # Add directories ending with "-attachments" as collections of 1358 # attachments for a particular page. 1359 1360 elif os.path.isdir(pathname) and filename.endswith("-attachments"): 1361 parent = filename[:-len("-attachments")] 1362 1363 # Add each file as an attachment. 1364 1365 for attachment in listdir(pathname): 1366 zipname = "%s_%s" % (filename, attachment) 1367 package.write(join(pathname, attachment), zipname) 1368 script.append("AddAttachment|%s|%s|%s||" % (zipname, attachment, parent)) 1369 1370 package.writestr("MOIN_PACKAGE", "\n".join(script)) 1371 1372 finally: 1373 package.close() 1374 1375 def install_page_package(self, package_filename): 1376 1377 """ 1378 Install a package from the file with the given 'package_filename'. 1379 """ 1380 1381 path = os.environ.get("PYTHONPATH", "") 1382 1383 conf_dir = self.get_wikiconfig_directory() 1384 1385 if path: 1386 os.environ["PYTHONPATH"] = path + ":" + self.prefix_site_packages + ":" + conf_dir 1387 else: 1388 os.environ["PYTHONPATH"] = self.prefix_site_packages + ":" + conf_dir 1389 1390 installer = join(self.prefix_site_packages, "MoinMoin", "packages.py") 1391 cmd = "python %s i %s" % (installer, package_filename) 1392 os.system(cmd) 1393 1394 if path: 1395 os.environ["PYTHONPATH"] = path 1396 else: 1397 del os.environ["PYTHONPATH"] 1398 1399 def show_methods(): 1400 print "Methods:" 1401 print 1402 for method_name in Installation.method_names: 1403 doc = getattr(Installation, method_name).__doc__.strip() 1404 print "%-30s%-s" % (method_name, format(doc, 30)) 1405 print 1406 1407 # Command line option syntax. 1408 1409 syntax_description = "[ -f <config-filename> ] ( -m <method> | --method=METHOD ) [ <method-argument> ... ]" 1410 1411 # Main program. 1412 1413 if __name__ == "__main__": 1414 from ConfigParser import ConfigParser 1415 import sys, cmdsyntax 1416 1417 # Check the command syntax. 1418 1419 syntax = cmdsyntax.Syntax(syntax_description) 1420 try: 1421 matches = syntax.get_args(sys.argv[1:]) 1422 args = matches[0] 1423 except IndexError: 1424 print "Syntax:" 1425 print sys.argv[0], syntax_description 1426 print 1427 show_methods() 1428 sys.exit(1) 1429 1430 # Obtain configuration details. 1431 1432 try: 1433 config_filename = args.get("config-filename", "moinsetup.cfg") 1434 1435 if not exists(config_filename): 1436 print "Configuration", config_filename, "not found." 1437 sys.exit(1) 1438 1439 config = ConfigParser() 1440 config.read(config_filename) 1441 1442 # Obtain as many arguments as needed from the configuration. 1443 1444 config_arguments = dict(config.items("installation") + config.items("site")) 1445 method_arguments = args.get("method-argument", []) 1446 1447 # Attempt to initialise the configuration. 1448 1449 installation = Installation(**config_arguments) 1450 1451 except TypeError, exc: 1452 print "Error:" 1453 print 1454 print exc.args[0] 1455 print 1456 print "Configuration settings:" 1457 print 1458 print Installation.__init__.__doc__ 1459 print 1460 sys.exit(1) 1461 1462 # Obtain the method. 1463 1464 try: 1465 method = getattr(installation, args["method"]) 1466 except AttributeError: 1467 show_methods() 1468 sys.exit(1) 1469 1470 try: 1471 method(*method_arguments) 1472 except TypeError, exc: 1473 print "Error:" 1474 print 1475 print exc.args[0] 1476 print 1477 print "Method documentation:" 1478 print 1479 print method.__doc__ 1480 print 1481 sys.exit(1) 1482 1483 # vim: tabstop=4 expandtab shiftwidth=4