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.1" 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: 526 raise TypeError, "The 'prefix' 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 = "--install-lib='%s' --install-data='%s' --install-scripts='%s' --record='%s'" % ( 875 self.prefix_site_packages, 876 self.prefix, 877 join(self.prefix, "bin"), 878 log_filename) 879 880 os.system("%s setup.py --quiet %s %s --force" % (sys.executable, install_cmd, options)) 881 882 chdir(this_dir) 883 884 def install_data(self): 885 886 "Install Wiki data into an instance." 887 888 moin_data = self.get_moin_data() 889 890 if not moin_data: 891 raise SetupException, \ 892 "Cannot install MoinMoin data without either a 'moin_distribution' or a 'prefix' setting being defined." 893 894 # The default wikiconfig assumes data and underlay in the same directory. 895 896 status("Installing data and underlay in %s..." % self.common_dir) 897 898 for d in ("data", "underlay"): 899 source = join(moin_data, d) 900 source_tar = source + extsep + "tar" 901 902 if exists(source): 903 shutil.copytree(source, join(self.common_dir, d)) 904 elif exists(source_tar): 905 906 note("Copying archive %s instead of directory %s. Running...\n" 907 "make pagepacks\n" 908 "in the distribution directory should rectify this." % (source_tar, source)) 909 910 shutil.copy(source_tar, self.common_dir) 911 os.system("tar xf %s -C %s" % (source_tar, self.common_dir)) 912 else: 913 status("Could not copy %s into installed Wiki." % d) 914 915 def install_static_data(self): 916 917 "Install static Web data if appropriate." 918 919 if not exists(self.htdocs_dir): 920 mkdir(self.htdocs_dir) 921 922 for item in listdir(self.htdocs_dir_source): 923 path = join(self.htdocs_dir_source, item) 924 if isdir(path): 925 shutil.copytree(path, join(self.htdocs_dir, item)) 926 else: 927 shutil.copy(path, join(self.htdocs_dir, item)) 928 929 def configure_moin(self, reset=0): 930 931 """ 932 Edit the Wiki configuration file. If the optional 'reset' parameter is 933 specified as a true value, a default configuration will be copied from 934 the distribution if appropriate. 935 """ 936 937 moin_data = self.get_moin_data() 938 939 if not moin_data: 940 raise SetupException, \ 941 "Cannot configure MoinMoin without either a 'moin_distribution' or a 'prefix' setting being defined." 942 943 # NOTE: MoinMoin usually uses an apparently common URL space associated 944 # NOTE: with the version, but more specific locations are probably 945 # NOTE: acceptable if less efficient. 946 947 url_prefix_static = "%r" % self.static_url_resources_path 948 949 # Use a farm configuration file. 950 951 if self.farm_config: 952 wikiconfig_py = self.farm_config 953 954 # Or copy the Wiki configuration file from the distribution. 955 956 else: 957 wikiconfig_py = join(self.common_dir, "wikiconfig.py") 958 959 if not exists(wikiconfig_py) or reset: 960 shutil.copyfile(join(moin_data, "config", "wikiconfig.py"), wikiconfig_py) 961 962 status("Editing configuration from %s..." % wikiconfig_py) 963 964 # Edit the Wiki configuration file. 965 966 wikiconfig = Configuration(wikiconfig_py) 967 968 try: 969 wikiconfig.set("url_prefix_static", url_prefix_static, raw=1) 970 if self.superuser: 971 wikiconfig.set("superuser", [self.superuser]) 972 wikiconfig.set("acl_rights_before", u"%s:read,write,delete,revert,admin" % self.superuser) 973 else: 974 note("Superuser not defined. The ACL rules should be fixed in the configuration.") 975 976 # Edit any created Wiki configuration. 977 978 if not self.site_config: 979 self._configure_moin(wikiconfig) 980 981 finally: 982 wikiconfig.close() 983 984 # Edit any separate site configuration file. 985 986 if self.site_config: 987 status("Editing configuration from %s..." % self.site_config) 988 989 wikiconfig = Configuration(self.site_config) 990 991 try: 992 self._configure_moin(wikiconfig) 993 finally: 994 wikiconfig.close() 995 996 def _configure_moin(self, wikiconfig): 997 998 """ 999 Configure Moin, accessing the configuration file using 'wikiconfig'. 1000 """ 1001 1002 # Specific site configurations also appear to need 'data_dir', even in 1003 # 1.9. 1004 1005 if not self.moin_version.startswith("1.9") or self.site_config: 1006 data_dir = join(self.common_dir, "data") 1007 data_underlay_dir = join(self.common_dir, "underlay") 1008 1009 wikiconfig.set("data_dir", data_dir) 1010 wikiconfig.set("data_underlay_dir", data_underlay_dir) 1011 1012 wikiconfig.set("site_name", self.site_name) 1013 wikiconfig.set("page_front_page", self.front_page_name, count=1) 1014 1015 if self.theme_default is not None: 1016 wikiconfig.set("theme_default", self.theme_default) 1017 1018 def edit_moin_script(self): 1019 1020 "Edit the moin script." 1021 1022 moin_script = self.get_moin_script() 1023 status("Editing moin script at %s..." % moin_script) 1024 1025 s = readfile(moin_script) 1026 s = s.replace("#!/usr/bin/env python", "#!%s" % sys.executable) 1027 s = s.replace("#import sys", "import sys\nsys.path.insert(0, %r)" % self.prefix_site_packages) 1028 1029 writefile(moin_script, s) 1030 1031 def edit_moin_web_script(self, site_file_configured=1): 1032 1033 "Edit and install CGI script." 1034 1035 # NOTE: CGI only so far. 1036 # NOTE: Permissions should be checked. 1037 1038 moin_data = self.get_moin_data() 1039 1040 if self.moin_version.startswith("1.9"): 1041 moin_cgi_script = "moin.fcgi" 1042 else: 1043 moin_cgi_script = "moin.cgi" 1044 1045 moin_cgi = join(moin_data, "server", moin_cgi_script) 1046 moin_cgi_installed = join(self.web_app_dir, "moin.cgi") 1047 1048 status("Editing moin.cgi script from %s, writing to %s..." % (moin_cgi, moin_cgi_installed)) 1049 1050 s = readfile(moin_cgi) 1051 s = s.replace("#!/usr/bin/env python", "#!%s" % sys.executable) 1052 s = moin_cgi_prefix.sub("sys.path.insert(0, %r)" % self.prefix_site_packages, s) 1053 s = moin_cgi_wikiconfig.sub("sys.path.insert(0, %r)" % self.common_dir, s) 1054 1055 # Handle differences in script names when using limited hosting with 1056 # URL rewriting. 1057 1058 if self.limited_hosting(): 1059 if not site_file_configured: 1060 note("Site file not configured: script name not changed.") 1061 else: 1062 url_path = self.final_url_path 1063 1064 if self.moin_version.startswith("1.9"): 1065 s = moin_cgi_fix_script_name.sub(r"\1\2 %r" % url_path, s) 1066 else: 1067 s = moin_cgi_properties.sub(r"\1\2 %r" % {"script_name" : url_path}, s) 1068 1069 # NOTE: Use CGI for now. 1070 1071 if self.moin_version.startswith("1.9"): 1072 s = moin_cgi_force_cgi.sub(r"\1", s) 1073 1074 writefile(moin_cgi_installed, s) 1075 os.system("chmod a+rx '%s'" % moin_cgi_installed) 1076 1077 # Fix the cause of opaque errors in some Apache environments. 1078 1079 os.system("chmod go-w '%s'" % moin_cgi_installed) 1080 1081 def add_superuser(self): 1082 1083 "Add the superuser account." 1084 1085 if not self.superuser: 1086 return 1087 1088 print "Creating superuser", self.superuser, "using..." 1089 email = raw_input("E-mail address: ") 1090 password = getpass("Password: ") 1091 1092 path = self._set_pythonpath() 1093 1094 cmd = "%s --config-dir='%s' account create --name='%s' --email='%s' --password='%s'" % ( 1095 self.get_moin_script(), self.common_dir, self.superuser, email, password) 1096 os.system(cmd) 1097 1098 self._reset_pythonpath(path) 1099 1100 def make_site_files(self): 1101 1102 "Make the Apache site files." 1103 1104 # NOTE: Using local namespace for substitution. 1105 1106 # Where the site definitions and applications directories are different, 1107 # use a normal site definition. 1108 1109 if not self.limited_hosting(): 1110 1111 site_def = join(self.web_site_dir, self.site_identifier) 1112 1113 s = apache_site % self.__dict__ 1114 s += apache_site_extra % self.__dict__ 1115 1116 status("Writing Apache site definitions to %s..." % site_def) 1117 writefile(site_def, s) 1118 1119 note("Copy the site definitions to the appropriate sites directory if appropriate.\n" 1120 "Then, make sure that the site is enabled by running the appropriate tools (such as a2ensite).") 1121 1122 return 1 1123 1124 # Otherwise, use an .htaccess file. 1125 1126 else: 1127 site_def = join(self.web_static_dir or self.web_app_dir, ".htaccess") 1128 1129 s = apache_htaccess_combined_mod_rewrite % self.__dict__ 1130 1131 status("Writing .htaccess file to %s..." % site_def) 1132 try: 1133 writefile(site_def, s) 1134 except IOError: 1135 note("The .htaccess file could not be written. This will also affect the script name setting.") 1136 return 0 1137 else: 1138 return 1 1139 1140 def make_post_install_script(self): 1141 1142 "Write a post-install script with additional actions." 1143 1144 # Create the scripts. 1145 1146 this_user = os.environ["USER"] 1147 1148 vars = {} 1149 vars.update(Installation.__dict__) 1150 vars.update(self.__dict__) 1151 vars.update(locals()) 1152 1153 for postinst_script, start, extra, logs in postinst_scripts.values(): 1154 1155 s = start % vars 1156 s += extra % vars 1157 s += logs % vars 1158 1159 writefile(postinst_script, s) 1160 chmod(postinst_script, 0755) 1161 1162 if self.have_setfacl(): 1163 note("Run %s to set file access permissions.\n" 1164 "If this somehow fails..." % postinst_scripts["setfacl"][0]) 1165 1166 note("Run %s as root to set file ownership and permissions." % postinst_scripts["chown"][0]) 1167 1168 note("Run %s as root to set SELinux permissions, if applicable." % postinst_scripts["semanage"][0]) 1169 1170 # Accessory methods. 1171 1172 def reconfigure_moin(self, name=None, value=None, raw=0): 1173 1174 """ 1175 Edit the installed Wiki configuration file, setting a parameter with any 1176 given 'name' to the given 'value', treating the value as a raw 1177 expression (not a string) if 'raw' is set to a true value. 1178 1179 If 'name' and the remaining parameters are omitted, the default 1180 configuration activity is performed. 1181 1182 If the 'site_config' setting is defined, the specific site configuration 1183 will be changed. 1184 """ 1185 1186 wikiconfig_py = self.get_site_config() 1187 1188 status("Editing configuration from %s..." % wikiconfig_py) 1189 1190 wikiconfig = Configuration(wikiconfig_py) 1191 1192 try: 1193 # Perform default configuration. 1194 1195 if name is None and value is None: 1196 self._configure_moin(wikiconfig) 1197 else: 1198 wikiconfig.set(name, value, raw=raw) 1199 1200 finally: 1201 wikiconfig.close() 1202 1203 def set_auth_method(self, method_name): 1204 1205 """ 1206 Edit the installed Wiki configuration file, configuring the 1207 authentication method having the given 'method_name'. 1208 1209 Currently recognised authentication methods are: 1210 1211 * openid (uses OpenIDAuth to access OpenID providers) 1212 * moin, default (use MoinAuth to provide a login form) 1213 * given, remote-user (use HTTPAuth to obtain Web server credentials) 1214 1215 If the 'farm_config' setting is defined, the Wiki farm configuration 1216 will be changed. 1217 """ 1218 1219 wikiconfig_py = self.get_global_config() 1220 1221 status("Editing configuration from %s..." % wikiconfig_py) 1222 1223 wikiconfig = Configuration(wikiconfig_py) 1224 1225 try: 1226 # OpenID authentication. 1227 1228 if method_name.lower() == "openid": 1229 wikiconfig.set_import("MoinMoin.auth.openidrp", ["OpenIDAuth"]) 1230 1231 if self.moin_version.startswith("1.9"): 1232 if wikiconfig.get("cookie_lifetime"): 1233 wikiconfig.replace("cookie_lifetime", "(12, 12)", raw=1) 1234 else: 1235 wikiconfig.set("cookie_lifetime", "(12, 12)", raw=1) 1236 else: 1237 if wikiconfig.get("anonymous_session_lifetime"): 1238 wikiconfig.replace("anonymous_session_lifetime", "1000", raw=1) 1239 else: 1240 wikiconfig.set("anonymous_session_lifetime", "1000", raw=1) 1241 1242 auth_object = "OpenIDAuth()" 1243 1244 # Default Moin authentication. 1245 1246 elif method_name.lower() in ("moin", "default"): 1247 wikiconfig.set_import("MoinMoin.auth", ["MoinAuth"]) 1248 auth_object = "MoinAuth()" 1249 1250 # REMOTE_USER authentication. 1251 1252 elif method_name.lower() in ("given", "remote-user"): 1253 wikiconfig.set_import("MoinMoin.auth.http", ["HTTPAuth"]) 1254 auth_object = "HTTPAuth(autocreate=True)" 1255 1256 # Other methods are not currently supported. 1257 1258 else: 1259 return 1260 1261 # Edit the authentication setting. 1262 1263 auth = wikiconfig.get("auth") 1264 if auth: 1265 wikiconfig.replace("auth", "%s + [%s]" % (auth, auth_object), raw=1) 1266 else: 1267 wikiconfig.set("auth", "[%s]" % auth_object, raw=1) 1268 1269 finally: 1270 wikiconfig.close() 1271 1272 def migrate_instance(self, test=0, change_site=0): 1273 1274 """ 1275 Migrate the Wiki to the currently supported layout. If 'test' is 1276 specified and set to a non-empty or true value, only print whether the 1277 migration can be performed. 1278 1279 If 'change_site' is specified and set to a non-empty or true value, the 1280 site definitions will be updated; this will overwrite any changes made 1281 to the site definitions after they were last produced by moinsetup, and 1282 care must be taken to ensure that things like access controls are 1283 re-added to the definitions after this action is performed. 1284 """ 1285 1286 conf_dir = join(self.common_dir, "conf") 1287 if exists(conf_dir): 1288 for filename in listdir(conf_dir): 1289 pathname = join(conf_dir, filename) 1290 target = join(self.common_dir, filename) 1291 if not exists(target): 1292 print "Move", filename, "from conf directory." 1293 if not test: 1294 rename(pathname, target) 1295 else: 1296 print "No conf directory." 1297 1298 wikidata = join(self.common_dir, "wikidata") 1299 if exists(wikidata): 1300 htdocs = join(wikidata, "share", "moin", "htdocs") 1301 if exists(htdocs): 1302 target = join(self.common_dir, "htdocs") 1303 if not exists(target): 1304 print "Move htdocs from wikidata directory." 1305 if not test: 1306 rename(htdocs, target) 1307 else: 1308 print "No wikidata directory." 1309 1310 # Remove links and directories. 1311 1312 for name in ("conf", "wikidata"): 1313 d = join(self.common_dir, name) 1314 if islink(d): 1315 print "Remove %s symbolic link." % name 1316 if not test: 1317 remove(d) 1318 1319 if isdir(conf_dir): 1320 print "Remove conf directory." 1321 if not test: 1322 rmdir(conf_dir) 1323 1324 # Add any missing htdocs directory. 1325 1326 if not exists(self.htdocs_dir): 1327 print "Copy htdocs into the instance." 1328 if not test: 1329 self.install_static_data() 1330 1331 # Now attempt to reconfigure the Wiki. 1332 1333 print "Reconfigure the Wiki, the Web script%s." % (change_site and " and the site files" or "") 1334 if not test: 1335 self.configure_moin() 1336 self.edit_moin_web_script() 1337 if change_site: 1338 self.make_site_files() 1339 1340 def install_theme(self, theme_dir, theme_name=None): 1341 1342 """ 1343 Install Wiki theme provided in the given 'theme_dir' having the given 1344 optional 'theme_name' (if different from the 'theme_dir' name). 1345 """ 1346 1347 theme_dir = normpath(theme_dir) 1348 theme_name = theme_name or split(theme_dir)[-1] 1349 theme_module = join(theme_dir, theme_name + extsep + "py") 1350 1351 plugin_theme_dir = self.get_plugin_directory("theme") 1352 1353 # Copy the theme module. 1354 1355 status("Copying theme module to %s..." % plugin_theme_dir) 1356 1357 shutil.copy(theme_module, plugin_theme_dir) 1358 1359 # Copy the resources. 1360 1361 resources_dir = join(self.htdocs_dir, theme_name) 1362 1363 if not exists(resources_dir): 1364 mkdir(resources_dir) 1365 1366 status("Copying theme resources to %s..." % resources_dir) 1367 1368 for d in ("css", "img"): 1369 target_dir = join(resources_dir, d) 1370 if exists(target_dir): 1371 status("Replacing %s..." % target_dir) 1372 shutil.rmtree(target_dir) 1373 shutil.copytree(join(theme_dir, d), target_dir) 1374 1375 # Copy additional resources from other themes. 1376 1377 resources_source_dir = join(self.htdocs_dir, self.theme_master) 1378 target_dir = join(resources_dir, "css") 1379 1380 status("Copying resources from %s..." % resources_source_dir) 1381 1382 for css_file in self.extra_theme_css_files: 1383 css_file_path = join(resources_source_dir, "css", css_file) 1384 if exists(css_file_path): 1385 shutil.copy(css_file_path, target_dir) 1386 1387 note("Don't forget to add theme resources for extensions for this theme.\n" 1388 "Don't forget to edit this theme's stylesheets for extensions.") 1389 1390 def install_extension_package(self, extension_dir): 1391 1392 "Install any libraries from 'extension_dir' using a setup script." 1393 1394 this_dir = os.getcwd() 1395 chdir(extension_dir) 1396 1397 try: 1398 options = "install --install-lib=%s" % self.prefix_site_packages 1399 os.system("%s setup.py %s" % (sys.executable, options)) 1400 finally: 1401 chdir(this_dir) 1402 1403 def install_plugins(self, plugins_dir, plugin_type): 1404 1405 """ 1406 Install Wiki actions provided in the given 'plugins_dir' of the 1407 specified 'plugin_type'. 1408 """ 1409 1410 plugin_target_dir = self.get_plugin_directory(plugin_type) 1411 1412 # Copy the modules. 1413 1414 status("Copying %s modules to %s..." % (plugin_type, plugin_target_dir)) 1415 1416 # If a setup file is detected, abandon the installation. 1417 1418 if exists(join(plugins_dir, "setup%spy" % extsep)): 1419 print "Plugins not installed: setup%spy was detected." % extsep 1420 return 1421 1422 for module in glob(join(plugins_dir, "*%spy" % extsep)): 1423 shutil.copy(module, plugin_target_dir) 1424 1425 # Compile the plugin. 1426 1427 plugin = join(plugins_dir, split(module)[-1]) 1428 py_compile.compile(plugin) 1429 1430 def install_actions(self, actions_dir): 1431 1432 "Install Wiki actions provided in the given 'actions_dir'." 1433 1434 self.install_plugins(actions_dir, "action") 1435 1436 def install_macros(self, macros_dir): 1437 1438 "Install Wiki macros provided in the given 'macros_dir'." 1439 1440 self.install_plugins(macros_dir, "macro") 1441 1442 def install_parsers(self, parsers_dir): 1443 1444 "Install Wiki parsers provided in the given 'parsers_dir'." 1445 1446 self.install_plugins(parsers_dir, "parser") 1447 1448 def install_event_handlers(self, events_dir): 1449 1450 "Install Wiki event handlers provided in the given 'events_dir'." 1451 1452 self.install_plugins(events_dir, "events") 1453 1454 def install_theme_resources(self, theme_resources_dir, theme_name=None): 1455 1456 """ 1457 Install theme resources provided in the given 'theme_resources_dir'. If 1458 a specific 'theme_name' is given, only that theme will be given the 1459 specified resources. 1460 """ 1461 1462 for theme_name, theme_dir in self.get_theme_directories(theme_name): 1463 1464 # Copy the resources. 1465 1466 copied = 0 1467 1468 for d in ("css", "img"): 1469 source_dir = join(theme_resources_dir, d) 1470 target_dir = join(theme_dir, d) 1471 1472 if not exists(target_dir): 1473 continue 1474 1475 for resource in glob(join(source_dir, "*%s*" % extsep)): 1476 shutil.copy(resource, target_dir) 1477 copied = 1 1478 1479 if copied: 1480 status("Copied theme resources into %s..." % theme_dir) 1481 1482 note("Don't forget to edit theme stylesheets for any extensions.") 1483 1484 def edit_theme_stylesheet(self, theme_stylesheet, imported_stylesheet, action="ensure", theme_name=None): 1485 1486 """ 1487 Edit the given 'theme_stylesheet', ensuring (or removing) a reference to 1488 the 'imported_stylesheet' according to the given 'action' (optional, 1489 defaulting to "ensure"). If a specific 'theme_name' is given, only that 1490 theme will be affected. 1491 """ 1492 1493 if action == "ensure": 1494 ensure = 1 1495 elif action == "remove": 1496 ensure = 0 1497 else: 1498 error("Action %s not valid: it must be given as either 'ensure' or 'remove'." % action) 1499 return 1500 1501 for theme_name, theme_dir in self.get_theme_directories(theme_name): 1502 1503 # Locate the resources. 1504 1505 css_dir = join(theme_dir, "css") 1506 1507 if not exists(css_dir): 1508 continue 1509 1510 theme_stylesheet_filename = join(css_dir, theme_stylesheet) 1511 imported_stylesheet_filename = join(css_dir, imported_stylesheet) 1512 1513 if not exists(theme_stylesheet_filename): 1514 error("Stylesheet %s not defined in theme %s." % (theme_stylesheet, theme_name)) 1515 continue 1516 1517 if not exists(imported_stylesheet_filename): 1518 error("Stylesheet %s not defined in theme %s." % (imported_stylesheet, theme_name)) 1519 continue 1520 1521 # Edit the resources. 1522 1523 s = readfile(theme_stylesheet_filename) 1524 after_point = 0 1525 1526 for stylesheet_import in css_import_stylesheet.finditer(s): 1527 before, filename, after = stylesheet_import.groups() 1528 before_point, after_point = stylesheet_import.span() 1529 1530 # Test the import for a reference to the requested imported 1531 # stylesheet. 1532 1533 if filename == imported_stylesheet: 1534 if ensure: 1535 break 1536 else: 1537 if s[after_point:after_point+1] == "\n": 1538 after_point += 1 1539 s = "%s%s" % (s[:before_point], s[after_point:]) 1540 1541 status("Removing %s from %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 1542 writefile(theme_stylesheet_filename, s) 1543 break 1544 1545 # Where no import references the imported stylesheet, insert a 1546 # reference into the theme stylesheet. 1547 1548 else: 1549 if ensure: 1550 1551 # Assume that the stylesheet can follow other imports. 1552 1553 if s[after_point:after_point+1] == "\n": 1554 after_point += 1 1555 s = "%s%s\n%s" % (s[:after_point], '@import "%s";' % imported_stylesheet, s[after_point:]) 1556 1557 status("Adding %s to %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 1558 writefile(theme_stylesheet_filename, s) 1559 1560 def make_page_package(self, page_directory, package_filename): 1561 1562 """ 1563 Make a package containing the pages in 'page_directory', using the 1564 filenames as the page names, and writing the package to a file with the 1565 given 'package_filename'. 1566 """ 1567 1568 package = ZipFile(package_filename, "w") 1569 1570 try: 1571 script = ["MoinMoinPackage|1"] 1572 self._add_page_package_files(page_directory, "", package, script) 1573 package.writestr("MOIN_PACKAGE", "\n".join(script) + "\n") 1574 1575 finally: 1576 package.close() 1577 1578 def _add_page_package_files(self, page_directory, prefix, package, script): 1579 1580 """ 1581 Add files in the given 'page_directory' as pages with the given 'prefix' 1582 to the given 'package', recording them in the given 'script'. 1583 """ 1584 1585 filenames = listdir(page_directory) 1586 filenames.sort() 1587 1588 for filename in filenames: 1589 pathname = join(page_directory, filename) 1590 1591 # Add files as pages having the filename as page name. 1592 1593 if os.path.isfile(pathname): 1594 pagename = prefix + filename 1595 zipname = pagename.replace("/", "__") 1596 package.write(pathname, zipname) 1597 script.append("AddRevision|%s|%s" % (zipname, pagename)) 1598 1599 elif os.path.isdir(pathname): 1600 1601 # Add directories ending with "-attachments" as collections of 1602 # attachments for a particular page. 1603 1604 if filename.endswith("-attachments"): 1605 pagename = prefix + filename[:-len("-attachments")] 1606 zipname = pagename.replace("/", "__") 1607 1608 # Add each file as an attachment. 1609 1610 for attachment in listdir(pathname): 1611 azipname = "%s_%s" % (zipname, attachment) 1612 package.write(join(pathname, attachment), azipname) 1613 script.append("AddAttachment|%s|%s|%s||" % ( 1614 azipname, attachment, pagename)) 1615 1616 # Descend into other directories. 1617 1618 else: 1619 pagename = prefix + filename 1620 self._add_page_package_files(pathname, "%s/" % pagename, package, script) 1621 1622 def install_page_package(self, package_filename): 1623 1624 """ 1625 Install a package from the file with the given 'package_filename'. 1626 """ 1627 1628 path = self._set_pythonpath() 1629 installer = join(self.prefix_site_packages, "MoinMoin", "packages.py") 1630 cmd = "%s %s i %s" % (sys.executable, installer, package_filename) 1631 os.system(cmd) 1632 self._reset_pythonpath(path) 1633 1634 def show_methods(): 1635 print "Methods:" 1636 print 1637 for method_name in Installation.method_names: 1638 doc = getattr(Installation, method_name).__doc__.strip() 1639 print "%-30s%-s" % (method_name, format(doc, 30)) 1640 print 1641 1642 # Command line option syntax. 1643 1644 syntax_description = "[ -f <config-filename> ] ( -m <method> | --method=METHOD ) [ <method-argument> ... ]" 1645 1646 # Main program. 1647 1648 if __name__ == "__main__": 1649 from ConfigParser import ConfigParser 1650 import sys, cmdsyntax 1651 1652 # Check the command syntax. 1653 1654 syntax = cmdsyntax.Syntax(syntax_description) 1655 try: 1656 matches = syntax.get_args(sys.argv[1:]) 1657 args = matches[0] 1658 except IndexError: 1659 print "Syntax:" 1660 print sys.argv[0], syntax_description 1661 print 1662 show_methods() 1663 sys.exit(1) 1664 1665 # Obtain configuration details. 1666 1667 try: 1668 config_filename = args.get("config-filename", "moinsetup.cfg") 1669 1670 if not exists(config_filename): 1671 print "Configuration", config_filename, "not found." 1672 sys.exit(1) 1673 1674 config = ConfigParser() 1675 config.read(config_filename) 1676 1677 # Obtain as many arguments as needed from the configuration. 1678 1679 config_arguments = dict(config.items("installation") + config.items("site")) 1680 method_arguments = args.get("method-argument", []) 1681 1682 # Attempt to initialise the configuration. 1683 1684 installation = Installation(**config_arguments) 1685 1686 except TypeError, exc: 1687 print "Error:" 1688 print 1689 print exc.args[0] 1690 print 1691 print "Configuration settings:" 1692 print 1693 print Installation.__init__.__doc__ 1694 print 1695 sys.exit(1) 1696 1697 # Obtain the method. 1698 1699 try: 1700 method = getattr(installation, args["method"]) 1701 except AttributeError: 1702 show_methods() 1703 sys.exit(1) 1704 1705 try: 1706 method(*method_arguments) 1707 except TypeError, exc: 1708 print "Error:" 1709 print 1710 print exc.args[0] 1711 print 1712 print "Method documentation:" 1713 print 1714 print method.__doc__ 1715 print 1716 sys.exit(1) 1717 1718 # vim: tabstop=4 expandtab shiftwidth=4