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