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