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