1 #!/usr/bin/env python 2 3 """ 4 A setup and configuration script for MoinMoin. 5 6 Copyright (C) 2010, 2011, 2012, 2013 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from os.path import abspath, exists, extsep, isdir, islink, join, normpath, split 23 from os import chdir, chmod, listdir, mkdir, makedirs, remove, rename, rmdir 24 from getpass import getpass 25 from glob import glob 26 from zipfile import ZipFile 27 import os 28 import py_compile 29 import re 30 import shutil 31 import sys 32 import tempfile 33 34 __version__ = "0.4" 35 36 # Regular expressions for editing MoinMoin scripts and configuration files. 37 38 def compile_definition(name): 39 return re.compile(r"^(\s*)#*\s*(%s =)\s*(.*)$" % name, re.MULTILINE) 40 41 wikiconfig_import = re.compile(r"^(\s*)#*\s*" 42 r"(from\s+)(?P<module>\S+)" 43 r"(\s+import\s+)(?P<names>(?:\S|[^,\s])+(?:\s*,\s*(?:\S|[^,\s])+)*)" 44 r"(\s*)$", re.MULTILINE) 45 46 moin_cgi_prefix = re.compile(r"^#sys\.path\.insert\(0, 'PREFIX.*$", re.MULTILINE) 47 moin_cgi_wikiconfig = re.compile(r"^#sys\.path\.insert\(0, '/path/to/wikiconfigdir.*$", re.MULTILINE) 48 moin_cgi_properties = compile_definition("properties") 49 moin_cgi_fix_script_name = compile_definition("fix_script_name") 50 moin_cgi_force_cgi = re.compile(r"^#(os.environ\['FCGI_FORCE_CGI'\].*)$", re.MULTILINE) 51 52 css_import_stylesheet = re.compile(r"(\s*@import\s+[\"'])(.*?)([\"']\s*;)") 53 54 # Templates for Apache site definitions. 55 56 apache_site = """\ 57 ScriptAlias %(url_path)s "%(web_app_dir)s/moin.cgi" 58 """ 59 60 apache_site_extra = """\ 61 Alias %(static_url_resources_path)s "%(htdocs_dir)s/" 62 """ 63 64 # Limited hosting .htaccess definitions require the following settings to be 65 # configured in the main Apache configuration files for the directory where 66 # .htaccess is to be deployed: 67 # 68 # AllowOverride FileInfo Indexes 69 # 70 # The following settings are required for the directory where the moin.cgi 71 # script is to be deployed: 72 # 73 # Options ExecCGI FollowSymLinks SymLinksIfOwnerMatch 74 # AddHandler cgi-script .cgi 75 # 76 # If a DirectoryIndex directive is also desired, the Indexes option must be set. 77 # Such a directive is not desirable where the static and dynamic resources are 78 # in different places, however. 79 80 apache_htaccess_combined_mod_rewrite = """\ 81 DirectoryIndex %(url_path_tr)s/moin.cgi/ 82 RewriteEngine On 83 RewriteBase %(final_url_path)s 84 RewriteCond %%{REQUEST_FILENAME} !-f 85 RewriteCond %%{REQUEST_FILENAME} !-d 86 RewriteRule ^(.*) %(url_path_tr)s/moin.cgi/$1 [PT,L,QSA] 87 """ 88 89 90 91 # Post-setup templates. 92 93 # ACL-capable filesystem modifications. 94 95 postsetup_setfacl = """\ 96 #!/bin/sh 97 98 find '%(common_dir)s/data' -type f | xargs setfacl -m u:%(web_user)s:rw 99 find '%(common_dir)s/data' -type d | xargs setfacl -m u:%(web_user)s:rwx 100 find '%(common_dir)s/data/plugin' -type f | xargs setfacl -m u:%(web_user)s:r 101 find '%(common_dir)s/data/plugin' -type d | xargs setfacl -m u:%(web_user)s:rx 102 find '%(common_dir)s/underlay' -type f | xargs setfacl -m u:%(web_user)s:rw 103 find '%(common_dir)s/underlay' -type d | xargs setfacl -m u:%(web_user)s:rwx 104 if [ -e "%(common_dir)s/wikiconfig.py" ]; then 105 setfacl -m u:%(web_user)s:r '%(common_dir)s/wikiconfig.py' 106 fi 107 """ 108 109 postsetup_setfacl_extra = """ 110 find '%(htdocs_dir)s' -type f | xargs setfacl -m u:%(web_user)s:r 111 find '%(htdocs_dir)s' -type d | xargs setfacl -m u:%(web_user)s:rx 112 """ 113 114 postsetup_setfacl_logs = """ 115 if [ -e "%(common_dir)s/data/*-log" ]; then 116 setfacl -m u:%(web_user)s:rw %(common_dir)s/data/*-log 117 fi 118 """ 119 120 # Conventional owner/group permissions. 121 122 postsetup_chown_chmod = """\ 123 #!/bin/sh 124 125 chown -R %(this_user)s.%(web_group)s '%(common_dir)s/data' 126 chown -R %(this_user)s.%(web_group)s '%(common_dir)s/underlay' 127 find '%(common_dir)s/data' -type f | xargs chmod g=rw 128 find '%(common_dir)s/data' -type d | xargs chmod g=rwx 129 find '%(common_dir)s/data/plugin' -type f | xargs chmod g=r 130 find '%(common_dir)s/data/plugin' -type d | xargs chmod g=rx 131 find '%(common_dir)s/underlay' -type f | xargs chmod g=rw 132 find '%(common_dir)s/underlay' -type d | xargs chmod g=rwx 133 if [ -e "%(common_dir)s/wikiconfig.py" ]; then 134 chown %(this_user)s.%(web_group)s '%(common_dir)s/wikiconfig.py' 135 chmod g=r '%(common_dir)s/wikiconfig.py' 136 fi 137 """ 138 139 postsetup_chown_extra = """ 140 chown -R %(this_user)s.%(web_group)s '%(htdocs_dir)s' 141 """ 142 143 postsetup_chown_logs = """ 144 if [ -e "%(common_dir)s/data/*-log" ]; then 145 chown %(this_user)s.%(web_group)s %(common_dir)s/data/*-log 146 fi 147 """ 148 149 # SELinux file type modifications. 150 151 postsetup_semanage = """\ 152 #!/bin/sh 153 154 semanage fcontext -a -t httpd_sys_content_t "%(common_dir)s(/.*)?" 155 """ 156 157 postsetup_semanage_extra = """\ 158 semanage fcontext -a -t httpd_sys_content_t "%(htdocs_dir)s(/.*)?" 159 """ 160 161 postsetup_semanage_logs = """\ 162 """ 163 164 postinst_scripts = { 165 "chown" : ("moinsetup-post-chown.sh", postsetup_chown_chmod, postsetup_chown_extra, postsetup_chown_logs), 166 "setfacl" : ("moinsetup-post-setfacl.sh", postsetup_setfacl, postsetup_setfacl_extra, postsetup_setfacl_logs), 167 "semanage" : ("moinsetup-post-semanage.sh", postsetup_semanage, postsetup_semanage_extra, postsetup_semanage_logs), 168 } 169 170 # Utility functions. 171 172 def readfile(filename): 173 f = open(filename) 174 try: 175 return f.read() 176 finally: 177 f.close() 178 179 def writefile(filename, s): 180 f = open(filename, "w") 181 try: 182 f.write(s) 183 finally: 184 f.close() 185 186 def status(message): 187 print message 188 189 note = status 190 error = status 191 192 def format(s, indent): 193 return re.sub("\n\s+", "\n" + " " * indent, s) 194 195 # Classes. 196 197 class SetupException(Exception): 198 199 "An exception indicating a problem with a setup action." 200 201 pass 202 203 class Configuration: 204 205 "A class representing the configuration." 206 207 special_names = ["site_name"] 208 209 def __init__(self, filename): 210 self.content = readfile(filename) 211 self.filename = filename 212 213 def get_pattern(self, name): 214 215 # Make underscores optional for certain names. 216 217 if name in self.special_names: 218 name = name.replace("_", "_?") 219 220 return compile_definition(name) 221 222 def get(self, name): 223 224 """ 225 Return the raw value of the last definition having the given 'name'. 226 """ 227 228 pattern = self.get_pattern(name) 229 results = [match.group(3) for match in pattern.finditer(self.content)] 230 if results: 231 return results[-1] 232 else: 233 return None 234 235 def set(self, name, value, count=None, raw=0): 236 237 """ 238 Set the configuration parameter having the given 'name' with the given 239 'value', limiting the number of appropriately named parameters changed 240 to 'count', if specified. 241 242 If the configuration parameter of the given 'name' does not exist, 243 insert such a parameter at the end of the file. 244 245 If the optional 'raw' parameter is specified and set to a true value, 246 the provided 'value' is inserted directly into the configuration file. 247 """ 248 249 if not self.replace(name, value, count, raw): 250 self.insert(name, value, raw) 251 252 def replace(self, name, value, count=None, raw=0): 253 254 """ 255 Replace configuration parameters having the given 'name' with the given 256 'value', limiting the number of appropriately named parameters changed 257 to 'count', if specified. 258 259 If the optional 'raw' parameter is specified and set to a true value, 260 the provided 'value' is inserted directly into the configuration file. 261 262 Return the number of substitutions made. 263 """ 264 265 if raw: 266 substitution = r"\1\2 %s" % value 267 else: 268 substitution = r"\1\2 %r" % value 269 270 pattern = self.get_pattern(name) 271 272 if count is None: 273 self.content, n = pattern.subn(substitution, self.content) 274 else: 275 self.content, n = pattern.subn(substitution, self.content, count=count) 276 277 return n 278 279 def insert(self, name, value, raw=0): 280 281 """ 282 Insert the configuration parameter having the given 'name' and 'value'. 283 284 If the optional 'raw' parameter is specified and set to a true value, 285 the provided 'value' is inserted directly into the configuration file. 286 """ 287 288 if raw: 289 insertion = "%s = %s" 290 else: 291 insertion = "%s = %r" 292 293 self.insert_text(insertion % (name, value)) 294 295 def insert_text(self, text): 296 297 "Insert the given 'text' at the end of the configuration." 298 299 if not self.content.endswith("\n"): 300 self.content += "\n" 301 self.content += " %s\n" % text 302 303 def set_import(self, imported_module, imported_names): 304 305 """ 306 Set up an import of the given 'imported_module' exposing the given 307 'imported_names'. 308 """ 309 310 s = self.content 311 after_point = 0 312 first_point = None 313 314 for module_import in wikiconfig_import.finditer(s): 315 before, from_keyword, module, import_keyword, names, after = module_import.groups() 316 before_point, after_point = module_import.span() 317 318 if first_point is None: 319 first_point = after_point 320 321 names = [name.strip() for name in names.split(",")] 322 323 # Test the import for a reference to the requested imported module. 324 325 if imported_module == module: 326 for name in imported_names: 327 if name not in names: 328 names.append(name) 329 330 self.content = s[:before_point] + ( 331 "%s%s%s%s%s%s" % (before, from_keyword, module, import_keyword, ", ".join(names), after) 332 ) + s[after_point:] 333 break 334 335 # Where no import references the imported module, insert a reference 336 # into the configuration. 337 338 else: 339 # Add the import after the first one. 340 341 if first_point is not None: 342 self.content = s[:first_point] + ("\nfrom %s import %s" % (imported_module, ", ".join(imported_names))) + s[first_point:] 343 344 def close(self): 345 346 "Close the file, writing the content." 347 348 writefile(self.filename, self.content) 349 350 class Installation: 351 352 "A class for installing and initialising MoinMoin." 353 354 method_names = ( 355 "show_config", 356 "setup", 357 "setup_wiki", 358 "install_moin", 359 "install_data", 360 "install_static_data", 361 "configure_moin", 362 "edit_moin_script", 363 "edit_moin_web_script", 364 "add_superuser", 365 "make_site_files", 366 "make_post_install_script", 367 368 # Post-installation activities. 369 370 "reconfigure_moin", 371 "set_auth_method", 372 "migrate_instance", 373 "install_theme", 374 "install_extension_package", 375 "install_plugins", 376 "install_actions", 377 "install_macros", 378 "install_parsers", 379 "install_event_handlers", 380 "install_theme_resources", 381 "edit_theme_stylesheet", 382 383 # Other activities. 384 385 "make_page_package", 386 "install_page_package", 387 ) 388 389 source_config_names = ( 390 "moin_distribution", "prefix", "site_packages", "prefix_site_packages", 391 "htdocs_dir_source" 392 ) 393 394 instance_config_names = ( 395 "common_dir", "farm_config", "site_config", 396 "site_name", "site_identifier", 397 "front_page_name", "superuser", "theme_default", "htdocs_dir" 398 ) 399 400 site_config_names = ( 401 "web_app_dir", "web_site_dir", "web_static_dir", 402 "url_path", "static_url_path" 403 ) 404 405 # NOTE: Potentially detect Web server credentials. 406 407 web_user_default = "www-data" 408 web_group_default = "www-data" 409 410 # MoinMoin resources. 411 412 theme_master = "modernized" 413 extra_theme_css_files = ["SlideShow.css"] 414 415 def __init__(self, 416 moin_distribution=None, 417 prefix=None, site_packages=None, 418 web_app_dir=None, web_static_dir=None, web_site_dir=None, 419 web_user=None, web_group=None, 420 common_dir=None, farm_config=None, site_config=None, 421 url_path=None, static_url_path=None, 422 superuser=None, site_name=None, site_identifier=None, front_page_name=None, 423 theme_default=None): 424 425 """ 426 Initialise a Wiki installation using the following installation 427 settings: 428 429 * moin_distribution - the directory containing a MoinMoin source 430 distribution (can be omitted) 431 * prefix - the installation prefix (equivalent to /usr) 432 * site_packages - optional: the location of the Python 433 site-packages directory if outside the 'prefix' 434 (overrides the path calculated using 'prefix') 435 * web_app_dir - the directory where Web applications and scripts 436 reside (such as /home/www-user/cgi-bin) 437 * web_static_dir - optional: the directory where static Web 438 resources reside (such as /home/www-user/htdocs) 439 * web_site_dir - optional: the directory where Web site 440 definitions reside (such as 441 /etc/apache2/sites-available) 442 * web_user - optional: the user operating the Web server 443 * web_group - optional: the group of the Web server user 444 445 The following site settings are also applicable: 446 447 * common_dir - the directory where the Wiki configuration, 448 resources and instance will reside (such as 449 /home/www-user/mywiki) 450 * farm_config - optional: any Wiki farm configuration file for 451 multiple Wiki deployments (overrides the 452 'common_dir' setting) 453 * site_config - optional: a specific configuration file location 454 (overrides the 'common_dir' setting) 455 * url_path - the URL path at which the Wiki will be made 456 available (such as / or /mywiki) 457 * static_url_path - optional: the URL path at which static resources 458 will be made available (such as / or /mywiki) 459 * superuser - the name of the site's superuser (such as 460 "AdminUser", can be omitted) 461 * site_name - the name of the site (such as "My Wiki") 462 * site_identifier - optional: an identifier used to refer to the 463 site, typically derived from 'site_name' 464 * front_page_name - the front page name for the site (such as 465 "FrontPage" or a specific name for the site) 466 * theme_default - optional: the default theme (such as modern) 467 """ 468 469 self.moin_distribution = moin_distribution 470 self.superuser = superuser 471 self.site_name = site_name 472 self.site_identifier = site_identifier or site_name.replace(" ", "").lower() 473 self.front_page_name = front_page_name 474 self.farm_config = farm_config 475 self.site_config = site_config 476 self.theme_default = theme_default 477 478 self.web_user = web_user or self.web_user_default 479 self.web_group = web_group or self.web_group_default 480 481 # NOTE: Support the detection of the Apache sites directory. 482 483 self.prefix, self.site_packages, self.web_app_dir, self.web_site_dir, self.web_static_dir, self.common_dir = \ 484 map(self._get_abspath, (prefix, site_packages, web_app_dir, web_site_dir, web_static_dir, common_dir)) 485 486 if not self.web_app_dir: 487 raise TypeError, "The 'web_app_dir' setting must be specified." 488 489 # Strip any trailing "/" from the URL path. 490 491 if not url_path: 492 raise TypeError, "The 'url_path' setting must be specified." 493 494 self.url_path = self._tidy_url_path(url_path) 495 self.static_url_path = static_url_path and self._tidy_url_path(static_url_path) 496 497 self.url_path_tr = self._truncate_url_path(url_path) 498 499 # Define and create specific directories. 500 # Here are the configuration and actual Wiki data directories. 501 502 if not self.common_dir: 503 raise TypeError, "The 'common_dir' setting must be specified." 504 505 # Define the place where the MoinMoin package will actually reside. 506 507 if not self.prefix and not self.site_packages: 508 raise TypeError, "Either the 'prefix' or the 'site_packages' setting must be specified." 509 510 self.prefix_site_packages = self.site_packages or \ 511 join(self.prefix, "lib", "python%s.%s" % sys.version_info[:2], "site-packages") 512 513 # Find the version. 514 515 self.moin_version = self.get_moin_version() 516 517 # The static resources reside in different locations depending on the 518 # version of MoinMoin, but the Web server is used to serve static 519 # resources in both cases, even though MoinMoin 1.9 can serve static 520 # files itself. 521 522 # A shared data directory may be in use. 523 524 self.htdocs_dir_source = join(self.get_moin_data(), "htdocs") 525 526 if self.htdocs_dir_source is None or not exists(self.htdocs_dir_source): 527 528 # 1.9: moin/lib/python2.x/site-packages/MoinMoin/web/static/htdocs 529 530 if self.moin_version.startswith("1.9"): 531 self.htdocs_dir_source = join(self.prefix_site_packages, "MoinMoin", "web", "static", "htdocs") 532 else: 533 raise SetupException, "The static resources could not be found." 534 535 # Add the static identifier to the URL path. For example: 536 # 537 # / -> /moin_static187 538 # /hgwiki -> /hgwiki-moin_static187 539 # 540 # This allows multiple Wiki instances to have their own static resources 541 # in the same hosting area. 542 # 543 # Where a separate static URL path has been given, the resources are 544 # located under that path: 545 # 546 # / -> /moin_static187 547 # /hgwiki -> /hgwiki/moin_static187 548 549 # The final URL path is the principal location of the Wiki. 550 551 self.final_url_path = self.static_url_path or self.url_path 552 553 # The static URL resources path is the specific location of static 554 # resources. 555 556 self.static_url_resources_path = \ 557 self.final_url_path + \ 558 (self.static_url_path 559 and (self.static_url_path != "/" 560 and "/" 561 or "") 562 or (self.final_url_path != "/" 563 and "-" 564 or "")) + \ 565 self.get_static_identifier() 566 567 self.static_dir_name = self.static_url_resources_path.split("/")[-1] 568 569 # In limited hosting, the static resources directory is related to 570 # the URL path, either a specified static URL path or the general path. 571 572 if self.limited_hosting(): 573 self.htdocs_dir = join(self.web_static_dir or self.web_app_dir, self.static_dir_name) 574 575 # Otherwise, a mapping is made to the directory. 576 # This may be placed in a special static directory if desired. 577 578 else: 579 self.htdocs_dir = join(self.web_static_dir or self.common_dir, "htdocs") 580 581 def show_config(self): 582 583 "Show the configuration." 584 585 print 586 if self.limited_hosting(): 587 print "Limited hosting configuration detected." 588 print "Published resources will be configured using .htaccess." 589 else: 590 print "Privileged hosting configuration detected..." 591 print "Published resources will be configured using site definition files." 592 print 593 for section in ("source", "instance", "site"): 594 print section.title() 595 print "-" * len(section) 596 print 597 for setting in getattr(self, "%s_config_names" % section): 598 print "%-24s%s" % (setting, getattr(self, setting)) 599 print 600 601 print "Configuration locations" 602 print "-----------------------" 603 print 604 print "%-24s%s" % ("site-level", self.get_site_config()) 605 print "%-24s%s" % ("global", self.get_global_config()) 606 print 607 print "Theme directories" 608 print "-----------------" 609 print 610 611 try: 612 for theme in self.get_theme_directories(): 613 print "%-24s%s" % theme 614 except OSError, exc: 615 print "Not shown:", str(exc) 616 617 def _get_abspath(self, d): 618 return d and abspath(d) or None 619 620 def _tidy_url_path(self, url_path): 621 if url_path != "/" and url_path.endswith("/"): 622 return url_path[:-1] 623 else: 624 return url_path 625 626 def _truncate_url_path(self, url_path): 627 if url_path.endswith("/"): 628 return url_path[:-1] 629 else: 630 return url_path 631 632 def _set_pythonpath(self): 633 path = os.environ.get("PYTHONPATH", "") 634 635 conf_dir = self.get_wikiconfig_directory() 636 637 if path: 638 os.environ["PYTHONPATH"] = path + ":" + self.prefix_site_packages + ":" + conf_dir 639 else: 640 os.environ["PYTHONPATH"] = self.prefix_site_packages + ":" + conf_dir 641 642 return path 643 644 def _reset_pythonpath(self, path): 645 if path: 646 os.environ["PYTHONPATH"] = path 647 else: 648 del os.environ["PYTHONPATH"] 649 650 def get_moin_version(self): 651 652 "Return the MoinMoin version." 653 654 this_dir = os.getcwd() 655 656 for dir in [self.moin_distribution, self.prefix_site_packages]: 657 if dir: 658 try: 659 chdir(dir) 660 version = self.get_moin_version_from_package_info() or \ 661 self.get_moin_version_from_import() 662 if version: 663 return version 664 665 finally: 666 chdir(this_dir) 667 else: 668 return self.get_moin_version_from_import() 669 670 def get_moin_version_from_package_info(self): 671 672 "Inspect the MoinMoin package information, returning the version." 673 674 try: 675 f = open("PKG-INFO") 676 try: 677 for line in f.xreadlines(): 678 columns = line.split() 679 if columns[0] == "Version:": 680 return columns[1] 681 finally: 682 f.close() 683 684 except IOError: 685 pass 686 687 return None 688 689 def get_moin_version_from_import(self): 690 691 "Return the MoinMoin version from an import of the package itself." 692 693 # Where no distribution information can be read, try and import an 694 # installed version module. 695 696 f = os.popen("%s -c 'from MoinMoin.version import release; print release'" % sys.executable) 697 try: 698 return f.read().strip() 699 finally: 700 f.close() 701 702 def get_moin_data(self): 703 704 "Return the exact location of MoinMoin data." 705 706 return self.moin_distribution and join(self.moin_distribution, "wiki") or \ 707 self.prefix and join(self.prefix, "share", "moin") or None 708 709 def get_moin_script(self): 710 711 "Return the location of the general-purpose moin script." 712 713 return join(self.prefix, "bin", "moin") 714 715 def get_wikiconfig_directory(self): 716 717 "Return the location of the Wiki configuration." 718 719 if self.site_config: 720 return split(self.site_config)[0] 721 else: 722 return self.common_dir 723 724 def get_site_config(self): 725 726 "Return the file providing the site-level configuration." 727 728 if self.site_config: 729 return self.site_config 730 else: 731 return join(self.common_dir, "wikiconfig.py") 732 733 def get_global_config(self): 734 735 "Return the file providing the global MoinMoin configuration." 736 737 if self.farm_config: 738 return self.farm_config 739 else: 740 return join(self.common_dir, "wikiconfig.py") 741 742 def get_static_identifier(self): 743 744 "Return the static URL/directory identifier for the Wiki." 745 746 return "moin_static%s" % self.moin_version.replace(".", "") 747 748 def get_plugin_directory(self, plugin_type): 749 750 "Return the directory for plugins of the given 'plugin_type'." 751 752 data_dir = join(self.common_dir, "data") 753 return join(data_dir, "plugin", plugin_type) 754 755 def limited_hosting(self): 756 757 "Return whether limited Web hosting is being used." 758 759 return not self.web_site_dir 760 761 def ensure_directories(self): 762 763 "Make sure that all the directories are available." 764 765 for d in (self.common_dir, self.web_app_dir, self.web_static_dir, self.web_site_dir): 766 if d is not None and not exists(d): 767 makedirs(d) 768 769 def get_theme_directories(self, theme_name=None): 770 771 """ 772 Return tuples of the form (theme name, theme directory) for all themes, 773 or for a single theme if the optional 'theme_name' is specified. 774 """ 775 776 filenames = theme_name and [theme_name] or listdir(self.htdocs_dir) 777 directories = [] 778 779 for filename in filenames: 780 theme_dir = join(self.htdocs_dir, filename) 781 782 if not exists(theme_dir) or not isdir(theme_dir): 783 continue 784 785 directories.append((filename, theme_dir)) 786 787 return directories 788 789 def _get_temp_filename(self): 790 fd, temp_filename = tempfile.mkstemp(dir=self.common_dir) 791 os.close(fd) 792 return temp_filename 793 794 def have_setfacl(self): 795 796 "Work out whether setfacl works." 797 798 temp_filename = self._get_temp_filename() 799 800 try: 801 return os.system("setfacl -m user:%(web_user)s:r %(file)s > /dev/null 2>&1" % { 802 "web_user" : self.web_user, "file" : temp_filename}) == 0 803 finally: 804 remove(temp_filename) 805 806 # Main methods. 807 808 def setup(self): 809 810 "Set up the installation." 811 812 self.ensure_directories() 813 self.install_moin() 814 self.edit_moin_script() 815 self._setup_wiki() 816 817 def setup_wiki(self): 818 819 "Set up a Wiki without installing MoinMoin." 820 821 self.ensure_directories() 822 self._setup_wiki() 823 824 def _setup_wiki(self): 825 826 "Set up a Wiki without installing MoinMoin." 827 828 self.install_data() 829 self.install_static_data() 830 self.configure_moin() 831 self.add_superuser() 832 self.edit_moin_web_script(self.make_site_files()) 833 self.make_post_install_script() 834 835 if self.moin_version.startswith("1.9"): 836 note("You may need to visit the LanguageSetup page in the Wiki to create the standard set of pages.") 837 838 def install_moin(self): 839 840 "Enter the distribution directory and run the setup script." 841 842 # NOTE: Possibly check for an existing installation and skip repeated 843 # NOTE: installation attempts. 844 845 if not self.moin_distribution: 846 raise SetupException, "Cannot install MoinMoin without a 'moin_distribution' setting being defined." 847 848 this_dir = os.getcwd() 849 chdir(self.moin_distribution) 850 851 log_filename = "install-%s.log" % split(self.common_dir)[-1] 852 853 status("Installing MoinMoin in %s..." % self.prefix) 854 855 install_cmd = "install" 856 options = "--prefix='%s' --record='%s'" % (self.prefix, log_filename) 857 858 os.system("%s setup.py --quiet %s %s --force" % (sys.executable, install_cmd, options)) 859 860 chdir(this_dir) 861 862 def install_data(self): 863 864 "Install Wiki data into an instance." 865 866 moin_data = self.get_moin_data() 867 868 if not moin_data: 869 raise SetupException, \ 870 "Cannot install MoinMoin data without either a 'moin_distribution' or a 'prefix' setting being defined." 871 872 # The default wikiconfig assumes data and underlay in the same directory. 873 874 status("Installing data and underlay in %s..." % self.common_dir) 875 876 for d in ("data", "underlay"): 877 source = join(moin_data, d) 878 source_tar = source + extsep + "tar" 879 880 if exists(source): 881 shutil.copytree(source, join(self.common_dir, d)) 882 elif exists(source_tar): 883 884 note("Copying archive %s instead of directory %s. Running...\n" 885 "make pagepacks\n" 886 "in the distribution directory should rectify this." % (source_tar, source)) 887 888 shutil.copy(source_tar, self.common_dir) 889 os.system("tar xf %s -C %s" % (source_tar, self.common_dir)) 890 else: 891 status("Could not copy %s into installed Wiki." % d) 892 893 def install_static_data(self): 894 895 "Install static Web data if appropriate." 896 897 if not exists(self.htdocs_dir): 898 mkdir(self.htdocs_dir) 899 900 for item in listdir(self.htdocs_dir_source): 901 path = join(self.htdocs_dir_source, item) 902 if isdir(path): 903 shutil.copytree(path, join(self.htdocs_dir, item)) 904 else: 905 shutil.copy(path, join(self.htdocs_dir, item)) 906 907 def configure_moin(self, reset=0): 908 909 """ 910 Edit the Wiki configuration file. If the optional 'reset' parameter is 911 specified as a true value, a default configuration will be copied from 912 the distribution if appropriate. 913 """ 914 915 moin_data = self.get_moin_data() 916 917 if not moin_data: 918 raise SetupException, \ 919 "Cannot configure MoinMoin without either a 'moin_distribution' or a 'prefix' setting being defined." 920 921 # NOTE: MoinMoin usually uses an apparently common URL space associated 922 # NOTE: with the version, but more specific locations are probably 923 # NOTE: acceptable if less efficient. 924 925 url_prefix_static = "%r" % self.static_url_resources_path 926 927 # Use a farm configuration file. 928 929 if self.farm_config: 930 wikiconfig_py = self.farm_config 931 932 # Or copy the Wiki configuration file from the distribution. 933 934 else: 935 wikiconfig_py = join(self.common_dir, "wikiconfig.py") 936 937 if not exists(wikiconfig_py) or reset: 938 shutil.copyfile(join(moin_data, "config", "wikiconfig.py"), wikiconfig_py) 939 940 status("Editing configuration from %s..." % wikiconfig_py) 941 942 # Edit the Wiki configuration file. 943 944 wikiconfig = Configuration(wikiconfig_py) 945 946 try: 947 wikiconfig.set("url_prefix_static", url_prefix_static, raw=1) 948 if self.superuser: 949 wikiconfig.set("superuser", [self.superuser]) 950 wikiconfig.set("acl_rights_before", u"%s:read,write,delete,revert,admin" % self.superuser) 951 else: 952 note("Superuser not defined. The ACL rules should be fixed in the configuration.") 953 954 # Edit any created Wiki configuration. 955 956 if not self.site_config: 957 self._configure_moin(wikiconfig) 958 959 finally: 960 wikiconfig.close() 961 962 # Edit any separate site configuration file. 963 964 if self.site_config: 965 status("Editing configuration from %s..." % self.site_config) 966 967 wikiconfig = Configuration(self.site_config) 968 969 try: 970 self._configure_moin(wikiconfig) 971 finally: 972 wikiconfig.close() 973 974 def _configure_moin(self, wikiconfig): 975 976 """ 977 Configure Moin, accessing the configuration file using 'wikiconfig'. 978 """ 979 980 # Specific site configurations also appear to need 'data_dir', even in 981 # 1.9. 982 983 if not self.moin_version.startswith("1.9") or self.site_config: 984 data_dir = join(self.common_dir, "data") 985 data_underlay_dir = join(self.common_dir, "underlay") 986 987 wikiconfig.set("data_dir", data_dir) 988 wikiconfig.set("data_underlay_dir", data_underlay_dir) 989 990 wikiconfig.set("site_name", self.site_name) 991 wikiconfig.set("page_front_page", self.front_page_name, count=1) 992 993 if self.theme_default is not None: 994 wikiconfig.set("theme_default", self.theme_default) 995 996 def edit_moin_script(self): 997 998 "Edit the moin script." 999 1000 moin_script = self.get_moin_script() 1001 status("Editing moin script at %s..." % moin_script) 1002 1003 s = readfile(moin_script) 1004 s = s.replace("#!/usr/bin/env python", "#!%s" % sys.executable) 1005 s = s.replace("#import sys", "import sys\nsys.path.insert(0, %r)" % self.prefix_site_packages) 1006 1007 writefile(moin_script, s) 1008 1009 def edit_moin_web_script(self, site_file_configured=1): 1010 1011 "Edit and install CGI script." 1012 1013 # NOTE: CGI only so far. 1014 # NOTE: Permissions should be checked. 1015 1016 moin_data = self.get_moin_data() 1017 1018 if self.moin_version.startswith("1.9"): 1019 moin_cgi_script = "moin.fcgi" 1020 else: 1021 moin_cgi_script = "moin.cgi" 1022 1023 moin_cgi = join(moin_data, "server", moin_cgi_script) 1024 moin_cgi_installed = join(self.web_app_dir, "moin.cgi") 1025 1026 status("Editing moin.cgi script from %s, writing to %s..." % (moin_cgi, moin_cgi_installed)) 1027 1028 s = readfile(moin_cgi) 1029 s = s.replace("#!/usr/bin/env python", "#!%s" % sys.executable) 1030 s = moin_cgi_prefix.sub("sys.path.insert(0, %r)" % self.prefix_site_packages, s) 1031 s = moin_cgi_wikiconfig.sub("sys.path.insert(0, %r)" % self.common_dir, s) 1032 1033 # Handle differences in script names when using limited hosting with 1034 # URL rewriting. 1035 1036 if self.limited_hosting(): 1037 if not site_file_configured: 1038 note("Site file not configured: script name not changed.") 1039 else: 1040 url_path = self.final_url_path 1041 1042 if self.moin_version.startswith("1.9"): 1043 s = moin_cgi_fix_script_name.sub(r"\1\2 %r" % url_path, s) 1044 else: 1045 s = moin_cgi_properties.sub(r"\1\2 %r" % {"script_name" : url_path}, s) 1046 1047 # NOTE: Use CGI for now. 1048 1049 if self.moin_version.startswith("1.9"): 1050 s = moin_cgi_force_cgi.sub(r"\1", s) 1051 1052 writefile(moin_cgi_installed, s) 1053 os.system("chmod a+rx '%s'" % moin_cgi_installed) 1054 1055 # Fix the cause of opaque errors in some Apache environments. 1056 1057 os.system("chmod go-w '%s'" % moin_cgi_installed) 1058 1059 def add_superuser(self): 1060 1061 "Add the superuser account." 1062 1063 if not self.superuser: 1064 return 1065 1066 print "Creating superuser", self.superuser, "using..." 1067 email = raw_input("E-mail address: ") 1068 password = getpass("Password: ") 1069 1070 path = self._set_pythonpath() 1071 1072 cmd = "%s --config-dir='%s' account create --name='%s' --email='%s' --password='%s'" % ( 1073 self.get_moin_script(), self.common_dir, self.superuser, email, password) 1074 os.system(cmd) 1075 1076 self._reset_pythonpath(path) 1077 1078 def make_site_files(self): 1079 1080 "Make the Apache site files." 1081 1082 # NOTE: Using local namespace for substitution. 1083 1084 # Where the site definitions and applications directories are different, 1085 # use a normal site definition. 1086 1087 if not self.limited_hosting(): 1088 1089 site_def = join(self.web_site_dir, self.site_identifier) 1090 1091 s = apache_site % self.__dict__ 1092 s += apache_site_extra % self.__dict__ 1093 1094 status("Writing Apache site definitions to %s..." % site_def) 1095 writefile(site_def, s) 1096 1097 note("Copy the site definitions to the appropriate sites directory if appropriate.\n" 1098 "Then, make sure that the site is enabled by running the appropriate tools (such as a2ensite).") 1099 1100 return 1 1101 1102 # Otherwise, use an .htaccess file. 1103 1104 else: 1105 site_def = join(self.web_static_dir or self.web_app_dir, ".htaccess") 1106 1107 s = apache_htaccess_combined_mod_rewrite % self.__dict__ 1108 1109 status("Writing .htaccess file to %s..." % site_def) 1110 try: 1111 writefile(site_def, s) 1112 except IOError: 1113 note("The .htaccess file could not be written. This will also affect the script name setting.") 1114 return 0 1115 else: 1116 return 1 1117 1118 def make_post_install_script(self): 1119 1120 "Write a post-install script with additional actions." 1121 1122 # Create the scripts. 1123 1124 this_user = os.environ["USER"] 1125 1126 vars = {} 1127 vars.update(Installation.__dict__) 1128 vars.update(self.__dict__) 1129 vars.update(locals()) 1130 1131 for postinst_script, start, extra, logs in postinst_scripts.values(): 1132 1133 s = start % vars 1134 s += extra % vars 1135 s += logs % vars 1136 1137 writefile(postinst_script, s) 1138 chmod(postinst_script, 0755) 1139 1140 if self.have_setfacl(): 1141 note("Run %s to set file access permissions.\n" 1142 "If this somehow fails..." % postinst_scripts["setfacl"][0]) 1143 1144 note("Run %s as root to set file ownership and permissions." % postinst_scripts["chown"][0]) 1145 1146 note("Run %s as root to set SELinux permissions, if applicable." % postinst_scripts["semanage"][0]) 1147 1148 # Accessory methods. 1149 1150 def reconfigure_moin(self, name=None, value=None, raw=0): 1151 1152 """ 1153 Edit the installed Wiki configuration file, setting a parameter with any 1154 given 'name' to the given 'value', treating the value as a raw 1155 expression (not a string) if 'raw' is set to a true value. 1156 1157 If 'name' and the remaining parameters are omitted, the default 1158 configuration activity is performed. 1159 1160 If the 'site_config' setting is defined, the specific site configuration 1161 will be changed. 1162 """ 1163 1164 wikiconfig_py = self.get_site_config() 1165 1166 status("Editing configuration from %s..." % wikiconfig_py) 1167 1168 wikiconfig = Configuration(wikiconfig_py) 1169 1170 try: 1171 # Perform default configuration. 1172 1173 if name is None and value is None: 1174 self._configure_moin(wikiconfig) 1175 else: 1176 wikiconfig.set(name, value, raw=raw) 1177 1178 finally: 1179 wikiconfig.close() 1180 1181 def set_auth_method(self, method_name): 1182 1183 """ 1184 Edit the installed Wiki configuration file, configuring the 1185 authentication method having the given 'method_name'. 1186 1187 Currently recognised authentication methods are: 1188 1189 * openid (uses OpenIDAuth to access OpenID providers) 1190 * moin, default (use MoinAuth to provide a login form) 1191 * given, remote-user (use HTTPAuth to obtain Web server credentials) 1192 1193 If the 'farm_config' setting is defined, the Wiki farm configuration 1194 will be changed. 1195 """ 1196 1197 wikiconfig_py = self.get_global_config() 1198 1199 status("Editing configuration from %s..." % wikiconfig_py) 1200 1201 wikiconfig = Configuration(wikiconfig_py) 1202 1203 try: 1204 # OpenID authentication. 1205 1206 if method_name.lower() == "openid": 1207 wikiconfig.set_import("MoinMoin.auth.openidrp", ["OpenIDAuth"]) 1208 1209 if self.moin_version.startswith("1.9"): 1210 if wikiconfig.get("cookie_lifetime"): 1211 wikiconfig.replace("cookie_lifetime", "(12, 12)", raw=1) 1212 else: 1213 wikiconfig.set("cookie_lifetime", "(12, 12)", raw=1) 1214 else: 1215 if wikiconfig.get("anonymous_session_lifetime"): 1216 wikiconfig.replace("anonymous_session_lifetime", "1000", raw=1) 1217 else: 1218 wikiconfig.set("anonymous_session_lifetime", "1000", raw=1) 1219 1220 auth_object = "OpenIDAuth()" 1221 1222 # Default Moin authentication. 1223 1224 elif method_name.lower() in ("moin", "default"): 1225 wikiconfig.set_import("MoinMoin.auth", ["MoinAuth"]) 1226 auth_object = "MoinAuth()" 1227 1228 # REMOTE_USER authentication. 1229 1230 elif method_name.lower() in ("given", "remote-user"): 1231 wikiconfig.set_import("MoinMoin.auth.http", ["HTTPAuth"]) 1232 auth_object = "HTTPAuth(autocreate=True)" 1233 1234 # Other methods are not currently supported. 1235 1236 else: 1237 return 1238 1239 # Edit the authentication setting. 1240 1241 auth = wikiconfig.get("auth") 1242 if auth: 1243 wikiconfig.replace("auth", "%s + [%s]" % (auth, auth_object), raw=1) 1244 else: 1245 wikiconfig.set("auth", "[%s]" % auth_object, raw=1) 1246 1247 finally: 1248 wikiconfig.close() 1249 1250 def migrate_instance(self, test=0, change_site=0): 1251 1252 """ 1253 Migrate the Wiki to the currently supported layout. If 'test' is 1254 specified and set to a non-empty or true value, only print whether the 1255 migration can be performed. 1256 1257 If 'change_site' is specified and set to a non-empty or true value, the 1258 site definitions will be updated; this will overwrite any changes made 1259 to the site definitions after they were last produced by moinsetup, and 1260 care must be taken to ensure that things like access controls are 1261 re-added to the definitions after this action is performed. 1262 """ 1263 1264 conf_dir = join(self.common_dir, "conf") 1265 if exists(conf_dir): 1266 for filename in listdir(conf_dir): 1267 pathname = join(conf_dir, filename) 1268 target = join(self.common_dir, filename) 1269 if not exists(target): 1270 print "Move", filename, "from conf directory." 1271 if not test: 1272 rename(pathname, target) 1273 else: 1274 print "No conf directory." 1275 1276 wikidata = join(self.common_dir, "wikidata") 1277 if exists(wikidata): 1278 htdocs = join(wikidata, "share", "moin", "htdocs") 1279 if exists(htdocs): 1280 target = join(self.common_dir, "htdocs") 1281 if not exists(target): 1282 print "Move htdocs from wikidata directory." 1283 if not test: 1284 rename(htdocs, target) 1285 else: 1286 print "No wikidata directory." 1287 1288 # Remove links and directories. 1289 1290 for name in ("conf", "wikidata"): 1291 d = join(self.common_dir, name) 1292 if islink(d): 1293 print "Remove %s symbolic link." % name 1294 if not test: 1295 remove(d) 1296 1297 if isdir(conf_dir): 1298 print "Remove conf directory." 1299 if not test: 1300 rmdir(conf_dir) 1301 1302 # Add any missing htdocs directory. 1303 1304 if not exists(self.htdocs_dir): 1305 print "Copy htdocs into the instance." 1306 if not test: 1307 self.install_static_data() 1308 1309 # Now attempt to reconfigure the Wiki. 1310 1311 print "Reconfigure the Wiki, the Web script%s." % (change_site and " and the site files" or "") 1312 if not test: 1313 self.configure_moin() 1314 self.edit_moin_web_script() 1315 if change_site: 1316 self.make_site_files() 1317 1318 def install_theme(self, theme_dir, theme_name=None): 1319 1320 """ 1321 Install Wiki theme provided in the given 'theme_dir' having the given 1322 optional 'theme_name' (if different from the 'theme_dir' name). 1323 """ 1324 1325 theme_dir = normpath(theme_dir) 1326 theme_name = theme_name or split(theme_dir)[-1] 1327 theme_module = join(theme_dir, theme_name + extsep + "py") 1328 1329 plugin_theme_dir = self.get_plugin_directory("theme") 1330 1331 # Copy the theme module. 1332 1333 status("Copying theme module to %s..." % plugin_theme_dir) 1334 1335 shutil.copy(theme_module, plugin_theme_dir) 1336 1337 # Copy the resources. 1338 1339 resources_dir = join(self.htdocs_dir, theme_name) 1340 1341 if not exists(resources_dir): 1342 mkdir(resources_dir) 1343 1344 status("Copying theme resources to %s..." % resources_dir) 1345 1346 for d in ("css", "img"): 1347 target_dir = join(resources_dir, d) 1348 if exists(target_dir): 1349 status("Replacing %s..." % target_dir) 1350 shutil.rmtree(target_dir) 1351 shutil.copytree(join(theme_dir, d), target_dir) 1352 1353 # Copy additional resources from other themes. 1354 1355 resources_source_dir = join(self.htdocs_dir, self.theme_master) 1356 target_dir = join(resources_dir, "css") 1357 1358 status("Copying resources from %s..." % resources_source_dir) 1359 1360 for css_file in self.extra_theme_css_files: 1361 css_file_path = join(resources_source_dir, "css", css_file) 1362 if exists(css_file_path): 1363 shutil.copy(css_file_path, target_dir) 1364 1365 note("Don't forget to add theme resources for extensions for this theme.\n" 1366 "Don't forget to edit this theme's stylesheets for extensions.") 1367 1368 def install_extension_package(self, extension_dir): 1369 1370 "Install any libraries from 'extension_dir' using a setup script." 1371 1372 this_dir = os.getcwd() 1373 chdir(extension_dir) 1374 1375 try: 1376 options = "install --install-lib=%s" % self.prefix_site_packages 1377 os.system("%s setup.py %s" % (sys.executable, options)) 1378 finally: 1379 chdir(this_dir) 1380 1381 def install_plugins(self, plugins_dir, plugin_type): 1382 1383 """ 1384 Install Wiki actions provided in the given 'plugins_dir' of the 1385 specified 'plugin_type'. 1386 """ 1387 1388 plugin_target_dir = self.get_plugin_directory(plugin_type) 1389 1390 # Copy the modules. 1391 1392 status("Copying %s modules to %s..." % (plugin_type, plugin_target_dir)) 1393 1394 # If a setup file is detected, abandon the installation. 1395 1396 if exists(join(plugins_dir, "setup%spy" % extsep)): 1397 print "Plugins not installed: setup%spy was detected." % extsep 1398 return 1399 1400 for module in glob(join(plugins_dir, "*%spy" % extsep)): 1401 shutil.copy(module, plugin_target_dir) 1402 1403 # Compile the plugin. 1404 1405 plugin = join(plugins_dir, split(module)[-1]) 1406 py_compile.compile(plugin) 1407 1408 def install_actions(self, actions_dir): 1409 1410 "Install Wiki actions provided in the given 'actions_dir'." 1411 1412 self.install_plugins(actions_dir, "action") 1413 1414 def install_macros(self, macros_dir): 1415 1416 "Install Wiki macros provided in the given 'macros_dir'." 1417 1418 self.install_plugins(macros_dir, "macro") 1419 1420 def install_parsers(self, parsers_dir): 1421 1422 "Install Wiki parsers provided in the given 'parsers_dir'." 1423 1424 self.install_plugins(parsers_dir, "parser") 1425 1426 def install_event_handlers(self, events_dir): 1427 1428 "Install Wiki event handlers provided in the given 'events_dir'." 1429 1430 self.install_plugins(events_dir, "events") 1431 1432 def install_theme_resources(self, theme_resources_dir, theme_name=None): 1433 1434 """ 1435 Install theme resources provided in the given 'theme_resources_dir'. If 1436 a specific 'theme_name' is given, only that theme will be given the 1437 specified resources. 1438 """ 1439 1440 for theme_name, theme_dir in self.get_theme_directories(theme_name): 1441 1442 # Copy the resources. 1443 1444 copied = 0 1445 1446 for d in ("css", "img"): 1447 source_dir = join(theme_resources_dir, d) 1448 target_dir = join(theme_dir, d) 1449 1450 if not exists(target_dir): 1451 continue 1452 1453 for resource in glob(join(source_dir, "*%s*" % extsep)): 1454 shutil.copy(resource, target_dir) 1455 copied = 1 1456 1457 if copied: 1458 status("Copied theme resources into %s..." % theme_dir) 1459 1460 note("Don't forget to edit theme stylesheets for any extensions.") 1461 1462 def edit_theme_stylesheet(self, theme_stylesheet, imported_stylesheet, action="ensure", theme_name=None): 1463 1464 """ 1465 Edit the given 'theme_stylesheet', ensuring (or removing) a reference to 1466 the 'imported_stylesheet' according to the given 'action' (optional, 1467 defaulting to "ensure"). If a specific 'theme_name' is given, only that 1468 theme will be affected. 1469 """ 1470 1471 if action == "ensure": 1472 ensure = 1 1473 elif action == "remove": 1474 ensure = 0 1475 else: 1476 error("Action %s not valid: it must be given as either 'ensure' or 'remove'." % action) 1477 return 1478 1479 for theme_name, theme_dir in self.get_theme_directories(theme_name): 1480 1481 # Locate the resources. 1482 1483 css_dir = join(theme_dir, "css") 1484 1485 if not exists(css_dir): 1486 continue 1487 1488 theme_stylesheet_filename = join(css_dir, theme_stylesheet) 1489 imported_stylesheet_filename = join(css_dir, imported_stylesheet) 1490 1491 if not exists(theme_stylesheet_filename): 1492 error("Stylesheet %s not defined in theme %s." % (theme_stylesheet, theme_name)) 1493 continue 1494 1495 if not exists(imported_stylesheet_filename): 1496 error("Stylesheet %s not defined in theme %s." % (imported_stylesheet, theme_name)) 1497 continue 1498 1499 # Edit the resources. 1500 1501 s = readfile(theme_stylesheet_filename) 1502 after_point = 0 1503 1504 for stylesheet_import in css_import_stylesheet.finditer(s): 1505 before, filename, after = stylesheet_import.groups() 1506 before_point, after_point = stylesheet_import.span() 1507 1508 # Test the import for a reference to the requested imported 1509 # stylesheet. 1510 1511 if filename == imported_stylesheet: 1512 if ensure: 1513 break 1514 else: 1515 if s[after_point:after_point+1] == "\n": 1516 after_point += 1 1517 s = "%s%s" % (s[:before_point], s[after_point:]) 1518 1519 status("Removing %s from %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 1520 writefile(theme_stylesheet_filename, s) 1521 break 1522 1523 # Where no import references the imported stylesheet, insert a 1524 # reference into the theme stylesheet. 1525 1526 else: 1527 if ensure: 1528 1529 # Assume that the stylesheet can follow other imports. 1530 1531 if s[after_point:after_point+1] == "\n": 1532 after_point += 1 1533 s = "%s%s\n%s" % (s[:after_point], '@import "%s";' % imported_stylesheet, s[after_point:]) 1534 1535 status("Adding %s to %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 1536 writefile(theme_stylesheet_filename, s) 1537 1538 def make_page_package(self, page_directory, package_filename): 1539 1540 """ 1541 Make a package containing the pages in 'page_directory', using the 1542 filenames as the page names, and writing the package to a file with the 1543 given 'package_filename'. 1544 """ 1545 1546 package = ZipFile(package_filename, "w") 1547 1548 try: 1549 script = ["MoinMoinPackage|1"] 1550 self._add_page_package_files(page_directory, "", package, script) 1551 package.writestr("MOIN_PACKAGE", "\n".join(script) + "\n") 1552 1553 finally: 1554 package.close() 1555 1556 def _add_page_package_files(self, page_directory, prefix, package, script): 1557 1558 """ 1559 Add files in the given 'page_directory' as pages with the given 'prefix' 1560 to the given 'package', recording them in the given 'script'. 1561 """ 1562 1563 filenames = listdir(page_directory) 1564 filenames.sort() 1565 1566 for filename in filenames: 1567 pathname = join(page_directory, filename) 1568 1569 # Add files as pages having the filename as page name. 1570 1571 if os.path.isfile(pathname): 1572 pagename = prefix + filename 1573 zipname = pagename.replace("/", "__") 1574 package.write(pathname, zipname) 1575 script.append("AddRevision|%s|%s" % (zipname, pagename)) 1576 1577 elif os.path.isdir(pathname): 1578 1579 # Add directories ending with "-attachments" as collections of 1580 # attachments for a particular page. 1581 1582 if filename.endswith("-attachments"): 1583 pagename = prefix + filename[:-len("-attachments")] 1584 zipname = pagename.replace("/", "__") 1585 1586 # Add each file as an attachment. 1587 1588 for attachment in listdir(pathname): 1589 azipname = "%s_%s" % (zipname, attachment) 1590 package.write(join(pathname, attachment), azipname) 1591 script.append("AddAttachment|%s|%s|%s||" % ( 1592 azipname, attachment, pagename)) 1593 1594 # Descend into other directories. 1595 1596 else: 1597 pagename = prefix + filename 1598 self._add_page_package_files(pathname, "%s/" % pagename, package, script) 1599 1600 def install_page_package(self, package_filename): 1601 1602 """ 1603 Install a package from the file with the given 'package_filename'. 1604 """ 1605 1606 path = self._set_pythonpath() 1607 installer = join(self.prefix_site_packages, "MoinMoin", "packages.py") 1608 cmd = "%s %s i %s" % (sys.executable, installer, package_filename) 1609 os.system(cmd) 1610 self._reset_pythonpath(path) 1611 1612 def show_methods(): 1613 print "Methods:" 1614 print 1615 for method_name in Installation.method_names: 1616 doc = getattr(Installation, method_name).__doc__.strip() 1617 print "%-30s%-s" % (method_name, format(doc, 30)) 1618 print 1619 1620 # Command line option syntax. 1621 1622 syntax_description = "[ -f <config-filename> ] ( -m <method> | --method=METHOD ) [ <method-argument> ... ]" 1623 1624 # Main program. 1625 1626 if __name__ == "__main__": 1627 from ConfigParser import ConfigParser 1628 import sys, cmdsyntax 1629 1630 # Check the command syntax. 1631 1632 syntax = cmdsyntax.Syntax(syntax_description) 1633 try: 1634 matches = syntax.get_args(sys.argv[1:]) 1635 args = matches[0] 1636 except IndexError: 1637 print "Syntax:" 1638 print sys.argv[0], syntax_description 1639 print 1640 show_methods() 1641 sys.exit(1) 1642 1643 # Obtain configuration details. 1644 1645 try: 1646 config_filename = args.get("config-filename", "moinsetup.cfg") 1647 1648 if not exists(config_filename): 1649 print "Configuration", config_filename, "not found." 1650 sys.exit(1) 1651 1652 config = ConfigParser() 1653 config.read(config_filename) 1654 1655 # Obtain as many arguments as needed from the configuration. 1656 1657 config_arguments = dict(config.items("installation") + config.items("site")) 1658 method_arguments = args.get("method-argument", []) 1659 1660 # Attempt to initialise the configuration. 1661 1662 installation = Installation(**config_arguments) 1663 1664 except TypeError, exc: 1665 print "Error:" 1666 print 1667 print exc.args[0] 1668 print 1669 print "Configuration settings:" 1670 print 1671 print Installation.__init__.__doc__ 1672 print 1673 sys.exit(1) 1674 1675 # Obtain the method. 1676 1677 try: 1678 method = getattr(installation, args["method"]) 1679 except AttributeError: 1680 show_methods() 1681 sys.exit(1) 1682 1683 try: 1684 method(*method_arguments) 1685 except TypeError, exc: 1686 print "Error:" 1687 print 1688 print exc.args[0] 1689 print 1690 print "Method documentation:" 1691 print 1692 print method.__doc__ 1693 print 1694 sys.exit(1) 1695 1696 # vim: tabstop=4 expandtab shiftwidth=4