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