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