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