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