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, moin_data, prefix, web_app_dir, 331 common_dir, url_path, superuser, site_name, front_page_name, 332 web_site_dir=None, web_static_dir=None, farm_config=None, 333 site_config=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 * theme_default - optional: the default theme (such as modern) 366 """ 367 368 self.moin_distribution = moin_distribution 369 self.moin_data = moin_data 370 self.superuser = superuser 371 self.site_name = site_name 372 self.front_page_name = front_page_name 373 self.farm_config = farm_config 374 self.site_config = site_config 375 self.theme_default = theme_default 376 377 # NOTE: Support the detection of the Apache sites directory. 378 379 self.prefix, self.web_app_dir, self.web_site_dir, self.web_static_dir, self.common_dir = \ 380 map(self._get_abspath, (prefix, web_app_dir, web_site_dir, web_static_dir, common_dir)) 381 382 # Strip any trailing "/" from the URL path. 383 384 if url_path != "/" and url_path.endswith("/"): 385 self.url_path = url_path[:-1] 386 else: 387 self.url_path = url_path 388 389 # Define and create specific directories. 390 # Here are the configuration and actual Wiki data directories. 391 392 self.conf_dir = join(self.common_dir, "conf") 393 self.instance_dir = join(self.common_dir, "wikidata") 394 395 # Define the place where the MoinMoin package will actually reside. 396 397 self.prefix_site_packages = join(self.prefix, "lib", "python%s.%s" % sys.version_info[:2], "site-packages") 398 399 # Find the version. 400 401 self.moin_version = self.get_moin_version() 402 403 # The static resources reside in different locations depending on the 404 # version of MoinMoin. Moreover, these resources may end up in a 405 # published directory for 1.8 installations where the Web server cannot 406 # be instructed to fetch the content from outside certain designated 407 # locations. 408 409 # 1.9: moin/lib/python2.x/site-packages/MoinMoin/web/static/htdocs 410 411 if self.moin_version.startswith("1.9"): 412 413 # A shared data directory may be in use. 414 415 if self.moin_data: 416 self.htdocs_dir = self.htdocs_dir_source = join(self.moin_data, "htdocs") 417 else: 418 self.htdocs_dir = self.htdocs_dir_source = join(self.prefix_site_packages, "MoinMoin", "web", "static", "htdocs") 419 420 self.static_url_path = self.url_path 421 422 # 1.8: moin/share/moin/htdocs (optionally copied to a Web directory) 423 424 else: 425 426 # A shared data directory may be in use. 427 428 if self.moin_data: 429 self.htdocs_dir_source = join(self.moin_data, "htdocs") 430 else: 431 self.htdocs_dir_source = join(self.instance_dir, "share", "moin", "htdocs") 432 433 # Add the static identifier to the URL path. For example: 434 # / -> /moin_static187 435 # /hgwiki -> /hgwiki-moin_static187 436 437 self.static_url_path = self.url_path + (self.url_path != "/" and "-" or "") + self.get_static_identifier() 438 439 # In limited hosting, the static resources directory is related to 440 # the URL path. 441 442 if self.limited_hosting(): 443 self.htdocs_dir = join(self.web_static_dir or self.web_app_dir, self.static_url_path.lstrip("/")) 444 445 # Otherwise, a mapping is made to the directory. 446 447 else: 448 self.htdocs_dir = self.htdocs_dir_source 449 450 def _get_abspath(self, d): 451 return d and abspath(d) or None 452 453 def get_moin_version(self): 454 455 "Inspect the MoinMoin package information, returning the version." 456 457 if self.moin_distribution: 458 try: 459 this_dir = os.getcwd() 460 try: 461 os.chdir(self.moin_distribution) 462 f = open("PKG-INFO") 463 try: 464 for line in f.xreadlines(): 465 columns = line.split() 466 if columns[0] == "Version:": 467 return columns[1] 468 469 return None 470 471 finally: 472 f.close() 473 474 finally: 475 os.chdir(this_dir) 476 477 except IOError: 478 pass 479 480 # Where no distribution information can be read, try and import an 481 # installed version module. 482 483 f = os.popen("%s -c 'from MoinMoin.version import release; print release'" % sys.executable) 484 try: 485 return f.read().strip() 486 finally: 487 f.close() 488 489 def get_moin_data(self): 490 491 "Return the location of MoinMoin data." 492 493 return self.moin_data or self.moin_distribution and join(self.moin_distribution, "wiki") or None 494 495 def get_static_identifier(self): 496 497 "Return the static URL/directory identifier for the Wiki." 498 499 return "moin_static%s" % self.moin_version.replace(".", "") 500 501 def get_plugin_directory(self, plugin_type): 502 503 "Return the directory for plugins of the given 'plugin_type'." 504 505 data_dir = join(self.conf_dir, "data") 506 return join(data_dir, "plugin", plugin_type) 507 508 def limited_hosting(self): 509 510 "Return whether limited Web hosting is being used." 511 512 return not self.web_site_dir 513 514 def ensure_directories(self): 515 516 "Make sure that all the directories are available." 517 518 for d in (self.conf_dir, self.instance_dir, self.web_app_dir, self.web_static_dir, self.web_site_dir): 519 if d is not None and not exists(d): 520 os.makedirs(d) 521 522 def get_theme_directories(self, theme_name=None): 523 524 """ 525 Return tuples of the form (theme name, theme directory) for all themes, 526 or for a single theme if the optional 'theme_name' is specified. 527 """ 528 529 filenames = theme_name and [theme_name] or os.listdir(self.htdocs_dir) 530 directories = [] 531 532 for filename in filenames: 533 theme_dir = join(self.htdocs_dir, filename) 534 535 if not exists(theme_dir) or not isdir(theme_dir): 536 continue 537 538 directories.append((filename, theme_dir)) 539 540 return directories 541 542 # Main methods. 543 544 def setup(self): 545 546 "Set up the installation." 547 548 self.ensure_directories() 549 self.install_moin() 550 self._setup_wiki() 551 552 def setup_wiki(self): 553 554 "Set up a Wiki without installing MoinMoin." 555 556 self.ensure_directories() 557 558 try: 559 self.install_moin(data_only=1) 560 except SetupException: 561 if not self.moin_data: 562 raise 563 564 self._setup_wiki() 565 566 def _setup_wiki(self): 567 568 "Set up a Wiki without installing MoinMoin." 569 570 self.install_data() 571 self.configure_moin() 572 self.edit_moin_script() 573 self.add_superuser() 574 self.edit_moin_web_script(self.make_site_files()) 575 self.make_post_install_script() 576 577 def install_moin(self, data_only=0): 578 579 "Enter the distribution directory and run the setup script." 580 581 # NOTE: Possibly check for an existing installation and skip repeated 582 # NOTE: installation attempts. 583 584 if not self.moin_distribution: 585 raise SetupException, "Cannot install MoinMoin without a 'moin_distribution' setting being defined." 586 587 this_dir = os.getcwd() 588 os.chdir(self.moin_distribution) 589 590 log_filename = "install-%s.log" % split(self.common_dir)[-1] 591 592 status("Installing MoinMoin%s in %s..." % (data_only and " (data only)" or "", self.prefix)) 593 594 if data_only: 595 install_cmd = "install_data" 596 options = "--install-dir='%s'" % self.instance_dir 597 else: 598 install_cmd = "install" 599 options = "--prefix='%s' --install-data='%s' --record='%s'" % (self.prefix, self.instance_dir, log_filename) 600 601 os.system("python setup.py --quiet %s %s --force" % (install_cmd, options)) 602 603 os.chdir(this_dir) 604 605 def install_data(self): 606 607 "Install Wiki data." 608 609 moin_data = self.get_moin_data() 610 611 if not moin_data: 612 raise SetupException, \ 613 "Cannot install MoinMoin data without either a 'moin_distribution' or a 'moin_data' setting being defined." 614 615 # The default wikiconfig assumes data and underlay in the same directory. 616 617 status("Installing data and underlay in %s..." % self.conf_dir) 618 619 for d in ("data", "underlay"): 620 source = join(moin_data, d) 621 source_tar = source + os.path.extsep + "tar" 622 d_tar = source + os.path.extsep + "tar" 623 624 if os.path.exists(source): 625 shutil.copytree(source, join(self.conf_dir, d)) 626 elif os.path.exists(source_tar): 627 shutil.copy(source_tar, self.conf_dir) 628 os.system("tar xf %s -C %s" % (d_tar, self.conf_dir)) 629 else: 630 status("Could not copy %s into installed Wiki." % d) 631 632 # Copy static Web data if appropriate. 633 634 if not self.moin_version.startswith("1.9") and self.limited_hosting(): 635 636 if not exists(self.htdocs_dir): 637 os.mkdir(self.htdocs_dir) 638 639 for item in os.listdir(self.htdocs_dir_source): 640 path = join(self.htdocs_dir_source, item) 641 if isdir(path): 642 shutil.copytree(path, join(self.htdocs_dir, item)) 643 else: 644 shutil.copy(path, join(self.htdocs_dir, item)) 645 646 def configure_moin(self): 647 648 "Edit the Wiki configuration file." 649 650 moin_data = self.get_moin_data() 651 652 if not moin_data: 653 raise SetupException, \ 654 "Cannot configure MoinMoin without either a 'moin_distribution' or a 'moin_data' setting being defined." 655 656 # NOTE: Single Wiki only so far. 657 658 # Static URLs seem to be different in MoinMoin 1.9.x. 659 # For earlier versions, reserve URL space alongside the Wiki. 660 # NOTE: MoinMoin usually uses an apparently common URL space associated 661 # NOTE: with the version, but more specific locations are probably 662 # NOTE: acceptable if less efficient. 663 664 if self.moin_version.startswith("1.9"): 665 url_prefix_static = "%r + url_prefix_static" % self.static_url_path 666 else: 667 url_prefix_static = "%r" % self.static_url_path 668 669 # Use a farm configuration file. 670 671 if self.farm_config: 672 wikiconfig_py = self.farm_config 673 674 # Or copy the Wiki configuration file from the distribution. 675 676 else: 677 wikiconfig_py = join(self.conf_dir, "wikiconfig.py") 678 shutil.copyfile(join(moin_data, "config", "wikiconfig.py"), wikiconfig_py) 679 680 status("Editing configuration from %s..." % wikiconfig_py) 681 682 # Edit the Wiki configuration file. 683 684 wikiconfig = Configuration(wikiconfig_py) 685 686 try: 687 wikiconfig.set("url_prefix_static", url_prefix_static, raw=1) 688 wikiconfig.set("superuser", [self.superuser]) 689 wikiconfig.set("acl_rights_before", u"%s:read,write,delete,revert,admin" % self.superuser) 690 691 # Edit any created Wiki configuration. 692 693 if not self.site_config: 694 self._configure_moin(wikiconfig) 695 696 finally: 697 wikiconfig.close() 698 699 # Edit any separate site configuration file. 700 701 if self.site_config: 702 status("Editing configuration from %s..." % self.site_config) 703 704 wikiconfig = Configuration(self.site_config) 705 706 try: 707 self._configure_moin(wikiconfig) 708 finally: 709 wikiconfig.close() 710 711 def _configure_moin(self, wikiconfig): 712 713 """ 714 Configure Moin, accessing the configuration file using 'wikiconfig'. 715 """ 716 717 if not self.moin_version.startswith("1.9"): 718 data_dir = join(self.conf_dir, "data") 719 data_underlay_dir = join(self.conf_dir, "underlay") 720 721 wikiconfig.set("data_dir", data_dir) 722 wikiconfig.set("data_underlay_dir", data_underlay_dir) 723 724 wikiconfig.set("site_name", self.site_name) 725 wikiconfig.set("page_front_page", self.front_page_name, count=1) 726 727 if self.theme_default is not None: 728 wikiconfig.set("theme_default", self.theme_default) 729 730 def edit_moin_script(self): 731 732 "Edit the moin script." 733 734 moin_script = join(self.prefix, "bin", "moin") 735 736 status("Editing moin script at %s..." % moin_script) 737 738 s = readfile(moin_script) 739 s = s.replace("#import sys", "import sys\nsys.path.insert(0, %r)" % self.prefix_site_packages) 740 741 writefile(moin_script, s) 742 743 def edit_moin_web_script(self, site_file_configured=1): 744 745 "Edit and install CGI script." 746 747 # NOTE: CGI only so far. 748 # NOTE: Permissions should be checked. 749 750 moin_data = self.get_moin_data() 751 752 if self.moin_version.startswith("1.9"): 753 moin_cgi_script = "moin.fcgi" 754 else: 755 moin_cgi_script = "moin.cgi" 756 757 moin_cgi = join(moin_data, "server", moin_cgi_script) 758 moin_cgi_installed = join(self.web_app_dir, "moin.cgi") 759 760 status("Editing moin.cgi script from %s, writing to %s..." % (moin_cgi, moin_cgi_installed)) 761 762 s = readfile(moin_cgi) 763 s = moin_cgi_prefix.sub("sys.path.insert(0, %r)" % self.prefix_site_packages, s) 764 s = moin_cgi_wikiconfig.sub("sys.path.insert(0, %r)" % self.conf_dir, s) 765 766 # Handle differences in script names when using limited hosting with 767 # URL rewriting. 768 769 if self.limited_hosting(): 770 if not site_file_configured: 771 note("Site file not configured: script name not changed.") 772 else: 773 if self.moin_version.startswith("1.9"): 774 s = moin_cgi_fix_script_name.sub(r"\1\2 %r" % self.url_path, s) 775 else: 776 s = moin_cgi_properties.sub(r"\1\2 %r" % {"script_name" : self.url_path}, s) 777 778 # NOTE: Use CGI for now. 779 780 if self.moin_version.startswith("1.9"): 781 s = moin_cgi_force_cgi.sub(r"\1", s) 782 783 writefile(moin_cgi_installed, s) 784 os.system("chmod a+rx '%s'" % moin_cgi_installed) 785 786 def add_superuser(self): 787 788 "Add the superuser account." 789 790 moin_script = join(self.prefix, "bin", "moin") 791 792 print "Creating superuser", self.superuser, "using..." 793 email = raw_input("E-mail address: ") 794 password = getpass("Password: ") 795 796 path = os.environ.get("PYTHONPATH", "") 797 798 if path: 799 os.environ["PYTHONPATH"] = path + ":" + self.conf_dir 800 else: 801 os.environ["PYTHONPATH"] = self.conf_dir 802 803 cmd = moin_script + " --config-dir='%s' account create --name='%s' --email='%s' --password='%s'" % ( 804 self.conf_dir, self.superuser, email, password) 805 os.system(cmd) 806 807 if path: 808 os.environ["PYTHONPATH"] = path 809 else: 810 del os.environ["PYTHONPATH"] 811 812 def make_site_files(self): 813 814 "Make the Apache site files." 815 816 # NOTE: Using local namespace for substitution. 817 818 # Where the site definitions and applications directories are different, 819 # use a normal site definition. 820 821 if not self.limited_hosting(): 822 823 site_def = join(self.web_site_dir, self.site_name) 824 825 s = apache_site % self.__dict__ 826 827 if not self.moin_version.startswith("1.9"): 828 s += apache_site_extra_moin18 % self.__dict__ 829 830 status("Writing Apache site definitions to %s..." % site_def) 831 writefile(site_def, s) 832 833 note("Copy the site definitions to the appropriate sites directory if appropriate.") 834 note("Then, make sure that the site is enabled by running the appropriate tools (such as a2ensite).") 835 836 return 1 837 838 # Otherwise, use an .htaccess file. 839 840 else: 841 site_def = join(self.web_app_dir, ".htaccess") 842 843 s = apache_htaccess_combined_mod_rewrite % self.__dict__ 844 845 status("Writing .htaccess file to %s..." % site_def) 846 try: 847 writefile(site_def, s) 848 except IOError: 849 note("The .htaccess file could not be written. This will also affect the script name setting.") 850 return 0 851 else: 852 return 1 853 854 def make_post_install_script(self): 855 856 "Write a post-install script with additional actions." 857 858 # Work out whether setfacl works. 859 860 fd, temp_filename = tempfile.mkstemp(dir=self.conf_dir) 861 os.close(fd) 862 863 have_setfacl = os.system("setfacl -m user:%(web_user)s:r %(file)s > /dev/null 2>&1" % { 864 "web_user" : self.web_user, "file" : temp_filename}) == 0 865 866 os.remove(temp_filename) 867 868 # Create the scripts. 869 870 this_user = os.environ["USER"] 871 postinst_scripts = "moinsetup-post-chown.sh", "moinsetup-post-setfacl.sh" 872 873 vars = {} 874 vars.update(Installation.__dict__) 875 vars.update(self.__dict__) 876 vars.update(locals()) 877 878 for postinst_script, start, extra in [ 879 (postinst_scripts[0], postsetup_chown_chmod, postsetup_chown_moin18_extra), 880 (postinst_scripts[1], postsetup_setfacl, postsetup_setfacl_moin18_extra) 881 ]: 882 883 s = start % vars 884 885 if not self.moin_version.startswith("1.9"): 886 s += extra % vars 887 888 writefile(postinst_script, s) 889 os.chmod(postinst_script, 0755) 890 891 if have_setfacl: 892 note("Run %s to set file ownership and permissions." % postinst_scripts[1]) 893 note("If this somehow fails...") 894 895 note("Run %s as root to set file ownership and permissions." % postinst_scripts[0]) 896 897 # Accessory methods. 898 899 def reconfigure_moin(self, name=None, value=None, raw=0): 900 901 """ 902 Edit the installed Wiki configuration file, setting a parameter with any 903 given 'name' to the given 'value', treating the value as a raw 904 expression (not a string) if 'raw' is set to a true value. 905 906 If 'name' and the remaining parameters are omitted, the default 907 configuration activity is performed. 908 909 If the 'site_config' setting is defined, the specific site configuration 910 will be changed. 911 """ 912 913 if self.site_config: 914 wikiconfig_py = self.site_config 915 else: 916 wikiconfig_py = join(self.conf_dir, "wikiconfig.py") 917 918 status("Editing configuration from %s..." % wikiconfig_py) 919 920 wikiconfig = Configuration(wikiconfig_py) 921 922 try: 923 # Perform default configuration. 924 925 if name is None and value is None: 926 self._configure_moin(wikiconfig) 927 else: 928 wikiconfig.set(name, value, raw=raw) 929 930 finally: 931 wikiconfig.close() 932 933 def set_auth_method(self, method_name): 934 935 """ 936 Edit the installed Wiki configuration file, configuring the 937 authentication method having the given 'method_name'. 938 939 If the 'farm_config' setting is defined, the Wiki farm configuration 940 will be changed. 941 """ 942 943 if self.farm_config: 944 wikiconfig_py = self.farm_config 945 else: 946 wikiconfig_py = join(self.conf_dir, "wikiconfig.py") 947 948 status("Editing configuration from %s..." % wikiconfig_py) 949 950 wikiconfig = Configuration(wikiconfig_py) 951 952 try: 953 # OpenID authentication. 954 955 if method_name.lower() == "openid": 956 wikiconfig.set_import("MoinMoin.auth.openidrp", ["OpenIDAuth"]) 957 958 if self.moin_version.startswith("1.9"): 959 if wikiconfig.get("cookie_lifetime"): 960 wikiconfig.replace("cookie_lifetime", "(12, 12)", raw=1) 961 else: 962 wikiconfig.set("cookie_lifetime", "(12, 12)", raw=1) 963 else: 964 if wikiconfig.get("anonymous_session_lifetime"): 965 wikiconfig.replace("anonymous_session_lifetime", "1000", raw=1) 966 else: 967 wikiconfig.set("anonymous_session_lifetime", "1000", raw=1) 968 969 auth_object = "OpenIDAuth()" 970 971 # Default Moin authentication. 972 973 elif method_name.lower() in ("moin", "default"): 974 wikiconfig.set_import("MoinMoin.auth", ["MoinAuth"]) 975 auth_object = "MoinAuth()" 976 977 # REMOTE_USER authentication. 978 979 elif method_name.lower() in ("given", "remote-user"): 980 wikiconfig.set_import("MoinMoin.auth.http", ["HTTPAuth"]) 981 auth_object = "HTTPAuth(autocreate=True)" 982 983 # Other methods are not currently supported. 984 985 else: 986 return 987 988 # Edit the authentication setting. 989 990 auth = wikiconfig.get("auth") 991 if auth: 992 wikiconfig.replace("auth", "%s + [%s]" % (auth, auth_object), raw=1) 993 else: 994 wikiconfig.set("auth", "[%s]" % auth_object, raw=1) 995 996 finally: 997 wikiconfig.close() 998 999 def install_theme(self, theme_dir, theme_name=None): 1000 1001 """ 1002 Install Wiki theme provided in the given 'theme_dir' having the given 1003 optional 'theme_name' (if different from the 'theme_dir' name). 1004 """ 1005 1006 theme_dir = normpath(theme_dir) 1007 theme_name = theme_name or split(theme_dir)[-1] 1008 theme_module = join(theme_dir, theme_name + extsep + "py") 1009 1010 plugin_theme_dir = self.get_plugin_directory("theme") 1011 1012 # Copy the theme module. 1013 1014 status("Copying theme module to %s..." % plugin_theme_dir) 1015 1016 shutil.copy(theme_module, plugin_theme_dir) 1017 1018 # Copy the resources. 1019 1020 resources_dir = join(self.htdocs_dir, theme_name) 1021 1022 if not exists(resources_dir): 1023 os.mkdir(resources_dir) 1024 1025 status("Copying theme resources to %s..." % resources_dir) 1026 1027 for d in ("css", "img"): 1028 target_dir = join(resources_dir, d) 1029 if exists(target_dir): 1030 status("Replacing %s..." % target_dir) 1031 shutil.rmtree(target_dir) 1032 shutil.copytree(join(theme_dir, d), target_dir) 1033 1034 # Copy additional resources from other themes. 1035 1036 resources_source_dir = join(self.htdocs_dir, self.theme_master) 1037 target_dir = join(resources_dir, "css") 1038 1039 status("Copying resources from %s..." % resources_source_dir) 1040 1041 for css_file in self.extra_theme_css_files: 1042 css_file_path = join(resources_source_dir, "css", css_file) 1043 if exists(css_file_path): 1044 shutil.copy(css_file_path, target_dir) 1045 1046 note("Don't forget to add theme resources for extensions for this theme.") 1047 note("Don't forget to edit this theme's stylesheets for extensions.") 1048 1049 def install_extension_package(self, extension_dir): 1050 1051 "Install any libraries from 'extension_dir' using a setup script." 1052 1053 this_dir = os.getcwd() 1054 os.chdir(extension_dir) 1055 os.system("python setup.py install --prefix=%s" % self.prefix) 1056 os.chdir(this_dir) 1057 1058 def install_plugins(self, plugins_dir, plugin_type): 1059 1060 """ 1061 Install Wiki actions provided in the given 'plugins_dir' of the 1062 specified 'plugin_type'. 1063 """ 1064 1065 plugin_target_dir = self.get_plugin_directory(plugin_type) 1066 1067 # Copy the modules. 1068 1069 status("Copying %s modules to %s..." % (plugin_type, plugin_target_dir)) 1070 1071 for module in glob(join(plugins_dir, "*%spy" % extsep)): 1072 shutil.copy(module, plugin_target_dir) 1073 1074 def install_actions(self, actions_dir): 1075 1076 "Install Wiki actions provided in the given 'actions_dir'." 1077 1078 self.install_plugins(actions_dir, "action") 1079 1080 def install_macros(self, macros_dir): 1081 1082 "Install Wiki macros provided in the given 'macros_dir'." 1083 1084 self.install_plugins(macros_dir, "macro") 1085 1086 def install_parsers(self, parsers_dir): 1087 1088 "Install Wiki macros provided in the given 'parsers_dir'." 1089 1090 self.install_plugins(parsers_dir, "parser") 1091 1092 def install_theme_resources(self, theme_resources_dir, theme_name=None): 1093 1094 """ 1095 Install theme resources provided in the given 'theme_resources_dir'. If 1096 a specific 'theme_name' is given, only that theme will be given the 1097 specified resources. 1098 """ 1099 1100 for theme_name, theme_dir in self.get_theme_directories(theme_name): 1101 1102 # Copy the resources. 1103 1104 copied = 0 1105 1106 for d in ("css", "img"): 1107 source_dir = join(theme_resources_dir, d) 1108 target_dir = join(theme_dir, d) 1109 1110 if not exists(target_dir): 1111 continue 1112 1113 for resource in glob(join(source_dir, "*%s*" % extsep)): 1114 shutil.copy(resource, target_dir) 1115 copied = 1 1116 1117 if copied: 1118 status("Copied theme resources into %s..." % theme_dir) 1119 1120 note("Don't forget to edit theme stylesheets for any extensions.") 1121 1122 def edit_theme_stylesheet(self, theme_stylesheet, imported_stylesheet, action="ensure", theme_name=None): 1123 1124 """ 1125 Edit the given 'theme_stylesheet', ensuring (or removing) a reference to 1126 the 'imported_stylesheet' according to the given 'action' (optional, 1127 defaulting to "ensure"). If a specific 'theme_name' is given, only that 1128 theme will be affected. 1129 """ 1130 1131 if action == "ensure": 1132 ensure = 1 1133 elif action == "remove": 1134 ensure = 0 1135 else: 1136 error("Action %s not valid: it must be given as either 'ensure' or 'remove'." % action) 1137 return 1138 1139 for theme_name, theme_dir in self.get_theme_directories(theme_name): 1140 1141 # Locate the resources. 1142 1143 css_dir = join(theme_dir, "css") 1144 1145 if not exists(css_dir): 1146 continue 1147 1148 theme_stylesheet_filename = join(css_dir, theme_stylesheet) 1149 imported_stylesheet_filename = join(css_dir, imported_stylesheet) 1150 1151 if not exists(theme_stylesheet_filename): 1152 error("Stylesheet %s not defined in theme %s." % (theme_stylesheet, theme_name)) 1153 continue 1154 1155 if not exists(imported_stylesheet_filename): 1156 error("Stylesheet %s not defined in theme %s." % (imported_stylesheet, theme_name)) 1157 continue 1158 1159 # Edit the resources. 1160 1161 s = readfile(theme_stylesheet_filename) 1162 after_point = 0 1163 1164 for stylesheet_import in css_import_stylesheet.finditer(s): 1165 before, filename, after = stylesheet_import.groups() 1166 before_point, after_point = stylesheet_import.span() 1167 1168 # Test the import for a reference to the requested imported 1169 # stylesheet. 1170 1171 if filename == imported_stylesheet: 1172 if ensure: 1173 break 1174 else: 1175 if s[after_point:after_point+1] == "\n": 1176 after_point += 1 1177 s = "%s%s" % (s[:before_point], s[after_point:]) 1178 1179 status("Removing %s from %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 1180 writefile(theme_stylesheet_filename, s) 1181 break 1182 1183 # Where no import references the imported stylesheet, insert a 1184 # reference into the theme stylesheet. 1185 1186 else: 1187 if ensure: 1188 1189 # Assume that the stylesheet can follow other imports. 1190 1191 if s[after_point:after_point+1] == "\n": 1192 after_point += 1 1193 s = "%s%s\n%s" % (s[:after_point], '@import "%s";' % imported_stylesheet, s[after_point:]) 1194 1195 status("Adding %s to %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 1196 writefile(theme_stylesheet_filename, s) 1197 1198 def make_page_package(self, page_directory, package_filename): 1199 1200 """ 1201 Make a package containing the pages in 'page_directory', using the 1202 filenames as the page names, and writing the package to a file with the 1203 given 'package_filename'. 1204 """ 1205 1206 package = ZipFile(package_filename, "w") 1207 1208 try: 1209 script = ["MoinMoinPackage|1"] 1210 1211 for filename in os.listdir(page_directory): 1212 pathname = join(page_directory, filename) 1213 1214 # Add files as pages having the filename as page name. 1215 1216 if os.path.isfile(pathname): 1217 package.write(pathname, filename) 1218 script.append("AddRevision|%s|%s" % (filename, filename)) 1219 1220 # Add directories ending with "-attachments" as collections of 1221 # attachments for a particular page. 1222 1223 elif os.path.isdir(pathname) and filename.endswith("-attachments"): 1224 parent = filename[:-len("-attachments")] 1225 1226 # Add each file as an attachment. 1227 1228 for attachment in os.listdir(pathname): 1229 zipname = "%s_%s" % (filename, attachment) 1230 package.write(join(pathname, attachment), zipname) 1231 script.append("AddAttachment|%s|%s|%s||" % (zipname, attachment, parent)) 1232 1233 package.writestr("MOIN_PACKAGE", "\n".join(script)) 1234 1235 finally: 1236 package.close() 1237 1238 def install_page_package(self, package_filename): 1239 1240 """ 1241 Install a package from the file with the given 'package_filename'. 1242 """ 1243 1244 path = os.environ.get("PYTHONPATH", "") 1245 1246 if path: 1247 os.environ["PYTHONPATH"] = path + ":" + self.prefix_site_packages + ":" + self.conf_dir 1248 else: 1249 os.environ["PYTHONPATH"] = self.prefix_site_packages + ":" + self.conf_dir 1250 1251 installer = join(self.prefix_site_packages, "MoinMoin", "packages.py") 1252 os.system("python %s i %s" % (installer, package_filename)) 1253 1254 if path: 1255 os.environ["PYTHONPATH"] = path 1256 else: 1257 del os.environ["PYTHONPATH"] 1258 1259 def show_methods(): 1260 print "Methods:" 1261 print 1262 for method_name in Installation.method_names: 1263 doc = getattr(Installation, method_name).__doc__.strip() 1264 print "%-30s%-s" % (method_name, format(doc, 30)) 1265 print 1266 1267 # Command line option syntax. 1268 1269 syntax_description = "[ -f <config-filename> ] ( <method> | --method=METHOD ) [ <method-argument> ... ]" 1270 1271 # Main program. 1272 1273 if __name__ == "__main__": 1274 from ConfigParser import ConfigParser 1275 import sys, cmdsyntax 1276 1277 # Check the command syntax. 1278 1279 syntax = cmdsyntax.Syntax(syntax_description) 1280 try: 1281 matches = syntax.get_args(sys.argv[1:]) 1282 args = matches[0] 1283 except IndexError: 1284 print "Syntax:" 1285 print sys.argv[0], syntax_description 1286 print 1287 show_methods() 1288 sys.exit(1) 1289 1290 # Obtain configuration details. 1291 1292 try: 1293 config_filename = args.get("config-filename", "moinsetup.cfg") 1294 config = ConfigParser() 1295 config.read(config_filename) 1296 1297 # Obtain as many arguments as needed from the configuration. 1298 1299 config_arguments = dict(config.items("installation") + config.items("site")) 1300 method_arguments = args.get("method-argument", []) 1301 1302 # Attempt to initialise the configuration. 1303 1304 installation = Installation(**config_arguments) 1305 1306 except TypeError: 1307 print "Configuration settings:" 1308 print 1309 print Installation.__init__.__doc__ 1310 print 1311 sys.exit(1) 1312 1313 # Obtain the method. 1314 1315 try: 1316 method = getattr(installation, args["method"]) 1317 except AttributeError: 1318 show_methods() 1319 sys.exit(1) 1320 1321 try: 1322 method(*method_arguments) 1323 except TypeError: 1324 print "Method documentation:" 1325 print 1326 print method.__doc__ 1327 print 1328 raise 1329 1330 # vim: tabstop=4 expandtab shiftwidth=4