1 #!/usr/bin/env python 2 3 """ 4 A setup and configuration script for MoinMoin. 5 6 Copyright (C) 2010, 2011, 2012, 2013, 2014, 2016 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_complete", "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_complete=None, 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_complete - optional: the complete URL at which the Wiki 474 will be made available by a wikifarm (such as 475 https://moinmo.in/ or http://my.wiki/Mine) 476 * url_path - the URL path at which the Wiki will be made 477 available (such as / or /mywiki) 478 * static_url_path - optional: the URL path at which static resources 479 will be made available (such as / or /mywiki) 480 * superuser - the name of the site's superuser (such as 481 "AdminUser", can be omitted) 482 * site_name - the name of the site (such as "My Wiki") 483 * site_identifier - optional: an identifier used to refer to the 484 site, typically derived from 'site_name' 485 * front_page_name - the front page name for the site (such as 486 "FrontPage" or a specific name for the site) 487 * theme_default - optional: the default theme (such as modern) 488 """ 489 490 self.moin_distribution = moin_distribution 491 self.superuser = superuser 492 self.site_name = site_name 493 self.site_identifier = site_identifier or site_name.replace(" ", "").lower() 494 self.front_page_name = front_page_name 495 self.farm_config = farm_config 496 self.site_config = site_config 497 self.theme_default = theme_default 498 499 self.web_user = web_user or self.web_user_default 500 self.web_group = web_group or self.web_group_default 501 502 # NOTE: Support the detection of the Apache sites directory. 503 504 self.prefix, self.site_packages, self.web_app_dir, self.web_site_dir, self.web_static_dir, self.common_dir = \ 505 map(self._get_abspath, (prefix, site_packages, web_app_dir, web_site_dir, web_static_dir, common_dir)) 506 507 if not self.web_app_dir: 508 raise TypeError, "The 'web_app_dir' setting must be specified." 509 510 self.url_complete = url_complete 511 512 # Strip any trailing "/" from the URL path. 513 514 if not url_path: 515 raise TypeError, "The 'url_path' setting must be specified." 516 517 self.url_path = self._tidy_url_path(url_path) 518 self.static_url_path = static_url_path and self._tidy_url_path(static_url_path) 519 520 self.url_path_tr = self._truncate_url_path(url_path) 521 522 # Define and create specific directories. 523 # Here are the configuration and actual Wiki data directories. 524 525 if not self.common_dir: 526 raise TypeError, "The 'common_dir' setting must be specified." 527 528 # Define the place where the MoinMoin package will actually reside. 529 530 if not self.prefix: 531 raise TypeError, "The 'prefix' setting must be specified." 532 533 self.prefix_site_packages = self.site_packages or \ 534 join(self.prefix, "lib", "python%s.%s" % sys.version_info[:2], "site-packages") 535 536 # Find the version. 537 538 self.moin_version = self.get_moin_version() 539 540 # The static resources reside in different locations depending on the 541 # version of MoinMoin, but the Web server is used to serve static 542 # resources in both cases, even though MoinMoin 1.9 can serve static 543 # files itself. 544 545 # A shared data directory may be in use. 546 547 self.htdocs_dir_source = join(self.get_moin_data(), "htdocs") 548 549 if self.htdocs_dir_source is None or not exists(self.htdocs_dir_source): 550 551 # 1.9: moin/lib/python2.x/site-packages/MoinMoin/web/static/htdocs 552 553 if self.moin_version.startswith("1.9"): 554 self.htdocs_dir_source = join(self.prefix_site_packages, "MoinMoin", "web", "static", "htdocs") 555 else: 556 raise SetupException, "The static resources could not be found." 557 558 # Add the static identifier to the URL path. For example: 559 # 560 # / -> /moin_static187 561 # /hgwiki -> /hgwiki-moin_static187 562 # 563 # This allows multiple Wiki instances to have their own static resources 564 # in the same hosting area. 565 # 566 # Where a separate static URL path has been given, the resources are 567 # located under that path: 568 # 569 # / -> /moin_static187 570 # /hgwiki -> /hgwiki/moin_static187 571 572 # The final URL path is the principal location of the Wiki. 573 574 self.final_url_path = self.static_url_path or self.url_path 575 576 # The static URL resources path is the specific location of static 577 # resources. 578 579 self.static_url_resources_path = \ 580 self.final_url_path + \ 581 (self.static_url_path 582 and (self.static_url_path != "/" 583 and "/" 584 or "") 585 or (self.final_url_path != "/" 586 and "-" 587 or "")) + \ 588 self.get_static_identifier() 589 590 self.static_dir_name = self.static_url_resources_path.split("/")[-1] 591 592 # In limited hosting, the static resources directory is related to 593 # the URL path, either a specified static URL path or the general path. 594 595 if self.limited_hosting(): 596 self.htdocs_dir = join(self.web_static_dir or self.web_app_dir, self.static_dir_name) 597 598 # Otherwise, a mapping is made to the directory. 599 # This may be placed in a special static directory if desired. 600 601 else: 602 self.htdocs_dir = join(self.web_static_dir or self.common_dir, "htdocs") 603 604 def show_config(self): 605 606 "Show the configuration." 607 608 print 609 if self.limited_hosting(): 610 print "Limited hosting configuration detected." 611 print "Published resources will be configured using .htaccess." 612 else: 613 print "Privileged hosting configuration detected..." 614 print "Published resources will be configured using site definition files." 615 print 616 for section in ("source", "instance", "site"): 617 print section.title() 618 print "-" * len(section) 619 print 620 for setting in getattr(self, "%s_config_names" % section): 621 print "%-24s%s" % (setting, getattr(self, setting)) 622 print 623 624 print "Configuration locations" 625 print "-----------------------" 626 print 627 print "%-24s%s" % ("site-level", self.get_site_config()) 628 print "%-24s%s" % ("global", self.get_global_config()) 629 print 630 print "Theme directories" 631 print "-----------------" 632 print 633 634 try: 635 for theme in self.get_theme_directories(): 636 print "%-24s%s" % theme 637 except OSError, exc: 638 print "Not shown:", str(exc) 639 640 def _get_abspath(self, d): 641 return d and abspath(d) or None 642 643 def _tidy_url_path(self, url_path): 644 if url_path != "/" and url_path.endswith("/"): 645 return url_path[:-1] 646 else: 647 return url_path 648 649 def _truncate_url_path(self, url_path): 650 if url_path.endswith("/"): 651 return url_path[:-1] 652 else: 653 return url_path 654 655 def _set_pythonpath(self): 656 path = os.environ.get("PYTHONPATH", "") 657 658 conf_dir = self.get_wikiconfig_directory() 659 660 if path: 661 os.environ["PYTHONPATH"] = path + ":" + self.prefix_site_packages + ":" + conf_dir 662 else: 663 os.environ["PYTHONPATH"] = self.prefix_site_packages + ":" + conf_dir 664 665 return path 666 667 def _reset_pythonpath(self, path): 668 if path: 669 os.environ["PYTHONPATH"] = path 670 else: 671 del os.environ["PYTHONPATH"] 672 673 def get_moin_version(self): 674 675 "Return the MoinMoin version." 676 677 this_dir = os.getcwd() 678 679 for dir in [self.moin_distribution, self.prefix_site_packages]: 680 if dir: 681 try: 682 chdir(dir) 683 version = self.get_moin_version_from_package_info() or \ 684 self.get_moin_version_from_import() 685 if version: 686 return version 687 688 finally: 689 chdir(this_dir) 690 else: 691 return self.get_moin_version_from_import() 692 693 def get_moin_version_from_package_info(self): 694 695 "Inspect the MoinMoin package information, returning the version." 696 697 try: 698 f = open("PKG-INFO") 699 try: 700 for line in f.xreadlines(): 701 columns = line.split() 702 if columns[0] == "Version:": 703 return columns[1] 704 finally: 705 f.close() 706 707 except IOError: 708 pass 709 710 return None 711 712 def get_moin_version_from_import(self): 713 714 "Return the MoinMoin version from an import of the package itself." 715 716 # Where no distribution information can be read, try and import an 717 # installed version module. 718 719 f = os.popen("%s -c 'from MoinMoin.version import release; print release'" % sys.executable) 720 try: 721 return f.read().strip() 722 finally: 723 f.close() 724 725 def get_moin_data(self): 726 727 "Return the exact location of MoinMoin data." 728 729 return self.moin_distribution and join(self.moin_distribution, "wiki") or \ 730 self.prefix and join(self.prefix, "share", "moin") or None 731 732 def get_moin_script(self): 733 734 "Return the location of the general-purpose moin script." 735 736 return join(self.prefix, "bin", "moin") 737 738 def get_wikiconfig_directory(self): 739 740 "Return the location of the Wiki configuration." 741 742 if self.site_config: 743 return split(self.site_config)[0] 744 else: 745 return self.common_dir 746 747 def get_site_config(self): 748 749 "Return the file providing the site-level configuration." 750 751 if self.site_config: 752 return self.site_config 753 else: 754 return join(self.common_dir, "wikiconfig.py") 755 756 def get_global_config(self): 757 758 "Return the file providing the global MoinMoin configuration." 759 760 if self.farm_config: 761 return self.farm_config 762 else: 763 return join(self.common_dir, "wikiconfig.py") 764 765 def get_static_identifier(self): 766 767 "Return the static URL/directory identifier for the Wiki." 768 769 return "moin_static%s" % self.moin_version.replace(".", "") 770 771 def get_plugin_directory(self, plugin_type): 772 773 "Return the directory for plugins of the given 'plugin_type'." 774 775 data_dir = join(self.common_dir, "data") 776 return join(data_dir, "plugin", plugin_type) 777 778 def limited_hosting(self): 779 780 "Return whether limited Web hosting is being used." 781 782 return not self.web_site_dir 783 784 def ensure_directories(self): 785 786 "Make sure that all the directories are available." 787 788 for d in (self.common_dir, self.web_app_dir, self.web_static_dir, self.web_site_dir): 789 if d is not None and not exists(d): 790 makedirs(d) 791 792 def get_theme_directories(self, theme_name=None): 793 794 """ 795 Return tuples of the form (theme name, theme directory) for all themes, 796 or for a single theme if the optional 'theme_name' is specified. 797 """ 798 799 filenames = theme_name and [theme_name] or listdir(self.htdocs_dir) 800 directories = [] 801 802 for filename in filenames: 803 theme_dir = join(self.htdocs_dir, filename) 804 805 if not exists(theme_dir) or not isdir(theme_dir): 806 continue 807 808 directories.append((filename, theme_dir)) 809 810 return directories 811 812 def _get_temp_filename(self): 813 fd, temp_filename = tempfile.mkstemp(dir=self.common_dir) 814 os.close(fd) 815 return temp_filename 816 817 def have_setfacl(self): 818 819 "Work out whether setfacl works." 820 821 temp_filename = self._get_temp_filename() 822 823 try: 824 return os.system("setfacl -m user:%(web_user)s:r %(file)s > /dev/null 2>&1" % { 825 "web_user" : self.web_user, "file" : temp_filename}) == 0 826 finally: 827 remove(temp_filename) 828 829 # Main methods. 830 831 def setup(self): 832 833 "Set up the installation." 834 835 self.ensure_directories() 836 self.install_moin() 837 self.edit_moin_script() 838 self._setup_wiki() 839 840 def setup_wiki(self): 841 842 "Set up a Wiki without installing MoinMoin." 843 844 self.ensure_directories() 845 self._setup_wiki() 846 847 def _setup_wiki(self): 848 849 "Set up a Wiki without installing MoinMoin." 850 851 self.install_data() 852 self.install_static_data() 853 self.configure_moin() 854 self.add_superuser() 855 self.edit_moin_web_script(self.make_site_files()) 856 self.make_post_install_script() 857 858 if self.moin_version.startswith("1.9"): 859 note("You may need to visit the LanguageSetup page in the Wiki to create the standard set of pages.") 860 861 def install_moin(self): 862 863 "Enter the distribution directory and run the setup script." 864 865 # NOTE: Possibly check for an existing installation and skip repeated 866 # NOTE: installation attempts. 867 868 if not self.moin_distribution: 869 raise SetupException, "Cannot install MoinMoin without a 'moin_distribution' setting being defined." 870 871 this_dir = os.getcwd() 872 chdir(self.moin_distribution) 873 874 log_filename = "install-%s.log" % split(self.common_dir)[-1] 875 876 status("Installing MoinMoin in %s..." % self.prefix) 877 878 install_cmd = "install" 879 options = "--install-lib='%s' --install-data='%s' --install-scripts='%s' --record='%s'" % ( 880 self.prefix_site_packages, 881 self.prefix, 882 join(self.prefix, "bin"), 883 log_filename) 884 885 os.system("%s setup.py --quiet %s %s --force" % (sys.executable, install_cmd, options)) 886 887 chdir(this_dir) 888 889 def install_data(self): 890 891 "Install Wiki data into an instance." 892 893 moin_data = self.get_moin_data() 894 895 if not moin_data: 896 raise SetupException, \ 897 "Cannot install MoinMoin data without either a 'moin_distribution' or a 'prefix' setting being defined." 898 899 # The default wikiconfig assumes data and underlay in the same directory. 900 901 status("Installing data and underlay in %s..." % self.common_dir) 902 903 for d in ("data", "underlay"): 904 source = join(moin_data, d) 905 source_tar = source + extsep + "tar" 906 907 if exists(source): 908 shutil.copytree(source, join(self.common_dir, d)) 909 elif exists(source_tar): 910 911 note("Copying archive %s instead of directory %s. Running...\n" 912 "make pagepacks\n" 913 "in the distribution directory should rectify this." % (source_tar, source)) 914 915 shutil.copy(source_tar, self.common_dir) 916 os.system("tar xf %s -C %s" % (source_tar, self.common_dir)) 917 else: 918 status("Could not copy %s into installed Wiki." % d) 919 920 def install_static_data(self): 921 922 "Install static Web data if appropriate." 923 924 if not exists(self.htdocs_dir): 925 mkdir(self.htdocs_dir) 926 927 for item in listdir(self.htdocs_dir_source): 928 path = join(self.htdocs_dir_source, item) 929 if isdir(path): 930 shutil.copytree(path, join(self.htdocs_dir, item)) 931 else: 932 shutil.copy(path, join(self.htdocs_dir, item)) 933 934 def configure_moin(self, reset=0): 935 936 """ 937 Edit the Wiki configuration file. If the optional 'reset' parameter is 938 specified as a true value, a default configuration will be copied from 939 the distribution if appropriate. 940 """ 941 942 moin_data = self.get_moin_data() 943 944 if not moin_data: 945 raise SetupException, \ 946 "Cannot configure MoinMoin without either a 'moin_distribution' or a 'prefix' setting being defined." 947 948 # NOTE: MoinMoin usually uses an apparently common URL space associated 949 # NOTE: with the version, but more specific locations are probably 950 # NOTE: acceptable if less efficient. 951 952 url_prefix_static = "%r" % self.static_url_resources_path 953 954 # Use a farm configuration file. 955 956 if self.farm_config: 957 wikiconfig_py = self.farm_config 958 959 # Or copy the Wiki configuration file from the distribution. 960 961 else: 962 wikiconfig_py = join(self.common_dir, "wikiconfig.py") 963 964 if not exists(wikiconfig_py) or reset: 965 shutil.copyfile(join(moin_data, "config", "wikiconfig.py"), wikiconfig_py) 966 967 status("Editing configuration from %s..." % wikiconfig_py) 968 969 # Edit the Wiki configuration file. 970 971 wikiconfig = Configuration(wikiconfig_py) 972 973 try: 974 wikiconfig.set("url_prefix_static", url_prefix_static, raw=1) 975 if self.superuser: 976 wikiconfig.set("superuser", [self.superuser]) 977 wikiconfig.set("acl_rights_before", u"%s:read,write,delete,revert,admin" % self.superuser) 978 else: 979 note("Superuser not defined. The ACL rules should be fixed in the configuration.") 980 981 # Edit any created Wiki configuration. 982 983 if not self.site_config: 984 self._configure_moin(wikiconfig) 985 986 finally: 987 wikiconfig.close() 988 989 # Edit any separate site configuration file. 990 991 if self.site_config: 992 status("Editing configuration from %s..." % self.site_config) 993 994 wikiconfig = Configuration(self.site_config) 995 996 try: 997 self._configure_moin(wikiconfig) 998 finally: 999 wikiconfig.close() 1000 1001 def _configure_moin(self, wikiconfig): 1002 1003 """ 1004 Configure Moin, accessing the configuration file using 'wikiconfig'. 1005 """ 1006 1007 # Specific site configurations also appear to need 'data_dir', even in 1008 # 1.9. 1009 1010 if not self.moin_version.startswith("1.9") or self.site_config: 1011 data_dir = join(self.common_dir, "data") 1012 data_underlay_dir = join(self.common_dir, "underlay") 1013 1014 wikiconfig.set("data_dir", data_dir) 1015 wikiconfig.set("data_underlay_dir", data_underlay_dir) 1016 1017 wikiconfig.set("site_name", self.site_name) 1018 wikiconfig.set("page_front_page", self.front_page_name, count=1) 1019 1020 if self.theme_default is not None: 1021 wikiconfig.set("theme_default", self.theme_default) 1022 1023 def edit_moin_script(self): 1024 1025 "Edit the moin script." 1026 1027 moin_script = self.get_moin_script() 1028 status("Editing moin script at %s..." % moin_script) 1029 1030 s = readfile(moin_script) 1031 s = s.replace("#!/usr/bin/env python", "#!%s" % sys.executable) 1032 s = s.replace("#import sys", "import sys\nsys.path.insert(0, %r)" % self.prefix_site_packages) 1033 1034 writefile(moin_script, s) 1035 1036 def edit_moin_web_script(self, site_file_configured=1): 1037 1038 "Edit and install CGI script." 1039 1040 # NOTE: CGI only so far. 1041 # NOTE: Permissions should be checked. 1042 1043 moin_data = self.get_moin_data() 1044 1045 if self.moin_version.startswith("1.9"): 1046 moin_cgi_script = "moin.fcgi" 1047 else: 1048 moin_cgi_script = "moin.cgi" 1049 1050 moin_cgi = join(moin_data, "server", moin_cgi_script) 1051 moin_cgi_installed = join(self.web_app_dir, "moin.cgi") 1052 1053 status("Editing moin.cgi script from %s, writing to %s..." % (moin_cgi, moin_cgi_installed)) 1054 1055 s = readfile(moin_cgi) 1056 s = s.replace("#!/usr/bin/env python", "#!%s" % sys.executable) 1057 s = moin_cgi_prefix.sub("sys.path.insert(0, %r)" % self.prefix_site_packages, s) 1058 s = moin_cgi_wikiconfig.sub("sys.path.insert(0, %r)" % self.common_dir, s) 1059 1060 # Handle differences in script names when using limited hosting with 1061 # URL rewriting. 1062 1063 if self.limited_hosting(): 1064 if not site_file_configured: 1065 note("Site file not configured: script name not changed.") 1066 else: 1067 url_path = self.final_url_path 1068 1069 if self.moin_version.startswith("1.9"): 1070 s = moin_cgi_fix_script_name.sub(r"\1\2 %r" % url_path, s) 1071 else: 1072 s = moin_cgi_properties.sub(r"\1\2 %r" % {"script_name" : url_path}, s) 1073 1074 # NOTE: Use CGI for now. 1075 1076 if self.moin_version.startswith("1.9"): 1077 s = moin_cgi_force_cgi.sub(r"\1", s) 1078 1079 writefile(moin_cgi_installed, s) 1080 os.system("chmod a+rx '%s'" % moin_cgi_installed) 1081 1082 # Fix the cause of opaque errors in some Apache environments. 1083 1084 os.system("chmod go-w '%s'" % moin_cgi_installed) 1085 1086 def add_superuser(self): 1087 1088 "Add the superuser account." 1089 1090 if not self.superuser: 1091 return 1092 1093 print "Creating superuser", self.superuser, "using..." 1094 email = raw_input("E-mail address: ") 1095 password = getpass("Password: ") 1096 1097 path = self._set_pythonpath() 1098 1099 cmd = "%s --config-dir='%s' account create --name='%s' --email='%s' --password='%s'" % ( 1100 self.get_moin_script(), self.common_dir, self.superuser, email, password) 1101 os.system(cmd) 1102 1103 self._reset_pythonpath(path) 1104 1105 def make_site_files(self): 1106 1107 "Make the Apache site files." 1108 1109 # NOTE: Using local namespace for substitution. 1110 1111 # Where the site definitions and applications directories are different, 1112 # use a normal site definition. 1113 1114 if not self.limited_hosting(): 1115 1116 # NOTE: Add ".conf" for awkward Apache management tools. 1117 1118 site_def = join(self.web_site_dir, self.site_identifier) 1119 1120 # NOTE: Put the more specific definition first. 1121 1122 s = apache_site % self.__dict__ 1123 s += apache_site_extra % self.__dict__ 1124 1125 status("Writing Apache site definitions to %s..." % site_def) 1126 writefile(site_def, s) 1127 1128 note("Copy the site definitions to the appropriate sites directory if appropriate.\n" 1129 "Then, make sure that the site is enabled by running the appropriate tools (such as a2ensite).") 1130 1131 return 1 1132 1133 # Otherwise, use an .htaccess file. 1134 1135 else: 1136 site_def = join(self.web_static_dir or self.web_app_dir, ".htaccess") 1137 1138 s = apache_htaccess_combined_mod_rewrite % self.__dict__ 1139 1140 status("Writing .htaccess file to %s..." % site_def) 1141 try: 1142 writefile(site_def, s) 1143 except IOError: 1144 note("The .htaccess file could not be written. This will also affect the script name setting.") 1145 return 0 1146 else: 1147 return 1 1148 1149 def make_post_install_script(self): 1150 1151 "Write a post-install script with additional actions." 1152 1153 # Create the scripts. 1154 1155 this_user = os.environ["USER"] 1156 1157 vars = {} 1158 vars.update(Installation.__dict__) 1159 vars.update(self.__dict__) 1160 vars.update(locals()) 1161 1162 for postinst_script, start, extra, logs in postinst_scripts.values(): 1163 1164 s = start % vars 1165 s += extra % vars 1166 s += logs % vars 1167 1168 writefile(postinst_script, s) 1169 chmod(postinst_script, 0755) 1170 1171 if self.have_setfacl(): 1172 note("Run %s to set file access permissions.\n" 1173 "If this somehow fails..." % postinst_scripts["setfacl"][0]) 1174 1175 note("Run %s as root to set file ownership and permissions." % postinst_scripts["chown"][0]) 1176 1177 note("Run %s as root to set SELinux permissions, if applicable." % postinst_scripts["semanage"][0]) 1178 1179 # Accessory methods. 1180 1181 def reconfigure_moin(self, name=None, value=None, raw=0): 1182 1183 """ 1184 Edit the installed Wiki configuration file, setting a parameter with any 1185 given 'name' to the given 'value', treating the value as a raw 1186 expression (not a string) if 'raw' is set to a true value. 1187 1188 If 'name' and the remaining parameters are omitted, the default 1189 configuration activity is performed. 1190 1191 If the 'site_config' setting is defined, the specific site configuration 1192 will be changed. 1193 """ 1194 1195 wikiconfig_py = self.get_site_config() 1196 1197 status("Editing configuration from %s..." % wikiconfig_py) 1198 1199 wikiconfig = Configuration(wikiconfig_py) 1200 1201 try: 1202 # Perform default configuration. 1203 1204 if name is None and value is None: 1205 self._configure_moin(wikiconfig) 1206 else: 1207 wikiconfig.set(name, value, raw=raw) 1208 1209 finally: 1210 wikiconfig.close() 1211 1212 def set_auth_method(self, method_name): 1213 1214 """ 1215 Edit the installed Wiki configuration file, configuring the 1216 authentication method having the given 'method_name'. 1217 1218 Currently recognised authentication methods are: 1219 1220 * openid (uses OpenIDAuth to access OpenID providers) 1221 * moin, default (use MoinAuth to provide a login form) 1222 * given, remote-user (use HTTPAuth to obtain Web server credentials) 1223 1224 If the 'farm_config' setting is defined, the Wiki farm configuration 1225 will be changed. 1226 """ 1227 1228 wikiconfig_py = self.get_global_config() 1229 1230 status("Editing configuration from %s..." % wikiconfig_py) 1231 1232 wikiconfig = Configuration(wikiconfig_py) 1233 1234 try: 1235 # OpenID authentication. 1236 1237 if method_name.lower() == "openid": 1238 wikiconfig.set_import("MoinMoin.auth.openidrp", ["OpenIDAuth"]) 1239 1240 if self.moin_version.startswith("1.9"): 1241 if wikiconfig.get("cookie_lifetime"): 1242 wikiconfig.replace("cookie_lifetime", "(12, 12)", raw=1) 1243 else: 1244 wikiconfig.set("cookie_lifetime", "(12, 12)", raw=1) 1245 else: 1246 if wikiconfig.get("anonymous_session_lifetime"): 1247 wikiconfig.replace("anonymous_session_lifetime", "1000", raw=1) 1248 else: 1249 wikiconfig.set("anonymous_session_lifetime", "1000", raw=1) 1250 1251 auth_object = "OpenIDAuth()" 1252 1253 # Default Moin authentication. 1254 1255 elif method_name.lower() in ("moin", "default"): 1256 wikiconfig.set_import("MoinMoin.auth", ["MoinAuth"]) 1257 auth_object = "MoinAuth()" 1258 1259 # REMOTE_USER authentication. 1260 1261 elif method_name.lower() in ("given", "remote-user"): 1262 wikiconfig.set_import("MoinMoin.auth.http", ["HTTPAuth"]) 1263 auth_object = "HTTPAuth(autocreate=True)" 1264 1265 # Other methods are not currently supported. 1266 1267 else: 1268 return 1269 1270 # Edit the authentication setting. 1271 1272 auth = wikiconfig.get("auth") 1273 if auth: 1274 wikiconfig.replace("auth", "%s + [%s]" % (auth, auth_object), raw=1) 1275 else: 1276 wikiconfig.set("auth", "[%s]" % auth_object, raw=1) 1277 1278 finally: 1279 wikiconfig.close() 1280 1281 def migrate_instance(self, test=0, change_site=0): 1282 1283 """ 1284 Migrate the Wiki to the currently supported layout. If 'test' is 1285 specified and set to a non-empty or true value, only print whether the 1286 migration can be performed. 1287 1288 If 'change_site' is specified and set to a non-empty or true value, the 1289 site definitions will be updated; this will overwrite any changes made 1290 to the site definitions after they were last produced by moinsetup, and 1291 care must be taken to ensure that things like access controls are 1292 re-added to the definitions after this action is performed. 1293 """ 1294 1295 conf_dir = join(self.common_dir, "conf") 1296 if exists(conf_dir): 1297 for filename in listdir(conf_dir): 1298 pathname = join(conf_dir, filename) 1299 target = join(self.common_dir, filename) 1300 if not exists(target): 1301 print "Move", filename, "from conf directory." 1302 if not test: 1303 rename(pathname, target) 1304 else: 1305 print "No conf directory." 1306 1307 wikidata = join(self.common_dir, "wikidata") 1308 if exists(wikidata): 1309 htdocs = join(wikidata, "share", "moin", "htdocs") 1310 if exists(htdocs): 1311 target = join(self.common_dir, "htdocs") 1312 if not exists(target): 1313 print "Move htdocs from wikidata directory." 1314 if not test: 1315 rename(htdocs, target) 1316 else: 1317 print "No wikidata directory." 1318 1319 # Remove links and directories. 1320 1321 for name in ("conf", "wikidata"): 1322 d = join(self.common_dir, name) 1323 if islink(d): 1324 print "Remove %s symbolic link." % name 1325 if not test: 1326 remove(d) 1327 1328 if isdir(conf_dir): 1329 print "Remove conf directory." 1330 if not test: 1331 rmdir(conf_dir) 1332 1333 # Add any missing htdocs directory. 1334 1335 if not exists(self.htdocs_dir): 1336 print "Copy htdocs into the instance." 1337 if not test: 1338 self.install_static_data() 1339 1340 # Now attempt to reconfigure the Wiki. 1341 1342 print "Reconfigure the Wiki, the Web script%s." % (change_site and " and the site files" or "") 1343 if not test: 1344 self.configure_moin() 1345 self.edit_moin_web_script() 1346 if change_site: 1347 self.make_site_files() 1348 1349 def install_theme(self, theme_dir, theme_name=None): 1350 1351 """ 1352 Install Wiki theme provided in the given 'theme_dir' having the given 1353 optional 'theme_name' (if different from the 'theme_dir' name). 1354 """ 1355 1356 theme_dir = normpath(theme_dir) 1357 theme_name = theme_name or split(theme_dir)[-1] 1358 theme_module = join(theme_dir, theme_name + extsep + "py") 1359 1360 plugin_theme_dir = self.get_plugin_directory("theme") 1361 1362 # Copy the theme module. 1363 1364 status("Copying theme module to %s..." % plugin_theme_dir) 1365 1366 shutil.copy(theme_module, plugin_theme_dir) 1367 1368 # Copy the resources. 1369 1370 resources_dir = join(self.htdocs_dir, theme_name) 1371 1372 if not exists(resources_dir): 1373 mkdir(resources_dir) 1374 1375 status("Copying theme resources to %s..." % resources_dir) 1376 1377 for d in ("css", "img"): 1378 target_dir = join(resources_dir, d) 1379 if exists(target_dir): 1380 status("Replacing %s..." % target_dir) 1381 shutil.rmtree(target_dir) 1382 shutil.copytree(join(theme_dir, d), target_dir) 1383 1384 # Copy additional resources from other themes. 1385 1386 resources_source_dir = join(self.htdocs_dir, self.theme_master) 1387 target_dir = join(resources_dir, "css") 1388 1389 status("Copying resources from %s..." % resources_source_dir) 1390 1391 for css_file in self.extra_theme_css_files: 1392 css_file_path = join(resources_source_dir, "css", css_file) 1393 if exists(css_file_path): 1394 shutil.copy(css_file_path, target_dir) 1395 1396 note("Don't forget to add theme resources for extensions for this theme.\n" 1397 "Don't forget to edit this theme's stylesheets for extensions.") 1398 1399 def install_extension_package(self, extension_dir): 1400 1401 "Install any libraries from 'extension_dir' using a setup script." 1402 1403 this_dir = os.getcwd() 1404 chdir(extension_dir) 1405 1406 try: 1407 options = "install --install-lib=%s" % self.prefix_site_packages 1408 os.system("%s setup.py %s" % (sys.executable, options)) 1409 finally: 1410 chdir(this_dir) 1411 1412 def install_plugins(self, plugins_dir, plugin_type): 1413 1414 """ 1415 Install Wiki actions provided in the given 'plugins_dir' of the 1416 specified 'plugin_type'. 1417 """ 1418 1419 plugin_target_dir = self.get_plugin_directory(plugin_type) 1420 1421 # Copy the modules. 1422 1423 status("Copying %s modules to %s..." % (plugin_type, plugin_target_dir)) 1424 1425 # If a setup file is detected, abandon the installation. 1426 1427 if exists(join(plugins_dir, "setup%spy" % extsep)): 1428 print "Plugins not installed: setup%spy was detected." % extsep 1429 return 1430 1431 for module in glob(join(plugins_dir, "*%spy" % extsep)): 1432 shutil.copy(module, plugin_target_dir) 1433 1434 # Compile the plugin. 1435 1436 plugin = join(plugins_dir, split(module)[-1]) 1437 py_compile.compile(plugin) 1438 1439 def install_actions(self, actions_dir): 1440 1441 "Install Wiki actions provided in the given 'actions_dir'." 1442 1443 self.install_plugins(actions_dir, "action") 1444 1445 def install_macros(self, macros_dir): 1446 1447 "Install Wiki macros provided in the given 'macros_dir'." 1448 1449 self.install_plugins(macros_dir, "macro") 1450 1451 def install_parsers(self, parsers_dir): 1452 1453 "Install Wiki parsers provided in the given 'parsers_dir'." 1454 1455 self.install_plugins(parsers_dir, "parser") 1456 1457 def install_event_handlers(self, events_dir): 1458 1459 "Install Wiki event handlers provided in the given 'events_dir'." 1460 1461 self.install_plugins(events_dir, "events") 1462 1463 def install_theme_resources(self, theme_resources_dir, theme_name=None): 1464 1465 """ 1466 Install theme resources provided in the given 'theme_resources_dir'. If 1467 a specific 'theme_name' is given, only that theme will be given the 1468 specified resources. 1469 """ 1470 1471 for theme_name, theme_dir in self.get_theme_directories(theme_name): 1472 1473 # Copy the resources. 1474 1475 copied = 0 1476 1477 for d in ("css", "img"): 1478 source_dir = join(theme_resources_dir, d) 1479 target_dir = join(theme_dir, d) 1480 1481 if not exists(target_dir): 1482 continue 1483 1484 for resource in glob(join(source_dir, "*%s*" % extsep)): 1485 shutil.copy(resource, target_dir) 1486 copied = 1 1487 1488 if copied: 1489 status("Copied theme resources into %s..." % theme_dir) 1490 1491 note("Don't forget to edit theme stylesheets for any extensions.") 1492 1493 def edit_theme_stylesheet(self, theme_stylesheet, imported_stylesheet, action="ensure", theme_name=None): 1494 1495 """ 1496 Edit the given 'theme_stylesheet', ensuring (or removing) a reference to 1497 the 'imported_stylesheet' according to the given 'action' (optional, 1498 defaulting to "ensure"). If a specific 'theme_name' is given, only that 1499 theme will be affected. 1500 """ 1501 1502 if action == "ensure": 1503 ensure = 1 1504 elif action == "remove": 1505 ensure = 0 1506 else: 1507 error("Action %s not valid: it must be given as either 'ensure' or 'remove'." % action) 1508 return 1509 1510 for theme_name, theme_dir in self.get_theme_directories(theme_name): 1511 1512 # Locate the resources. 1513 1514 css_dir = join(theme_dir, "css") 1515 1516 if not exists(css_dir): 1517 continue 1518 1519 theme_stylesheet_filename = join(css_dir, theme_stylesheet) 1520 imported_stylesheet_filename = join(css_dir, imported_stylesheet) 1521 1522 if not exists(theme_stylesheet_filename): 1523 error("Stylesheet %s not defined in theme %s." % (theme_stylesheet, theme_name)) 1524 continue 1525 1526 if not exists(imported_stylesheet_filename): 1527 error("Stylesheet %s not defined in theme %s." % (imported_stylesheet, theme_name)) 1528 continue 1529 1530 # Edit the resources. 1531 1532 s = readfile(theme_stylesheet_filename) 1533 after_point = 0 1534 1535 for stylesheet_import in css_import_stylesheet.finditer(s): 1536 before, filename, after = stylesheet_import.groups() 1537 before_point, after_point = stylesheet_import.span() 1538 1539 # Test the import for a reference to the requested imported 1540 # stylesheet. 1541 1542 if filename == imported_stylesheet: 1543 if ensure: 1544 break 1545 else: 1546 if s[after_point:after_point+1] == "\n": 1547 after_point += 1 1548 s = "%s%s" % (s[:before_point], s[after_point:]) 1549 1550 status("Removing %s from %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 1551 writefile(theme_stylesheet_filename, s) 1552 break 1553 1554 # Where no import references the imported stylesheet, insert a 1555 # reference into the theme stylesheet. 1556 1557 else: 1558 if ensure: 1559 1560 # Assume that the stylesheet can follow other imports. 1561 1562 if s[after_point:after_point+1] == "\n": 1563 after_point += 1 1564 s = "%s%s\n%s" % (s[:after_point], '@import "%s";' % imported_stylesheet, s[after_point:]) 1565 1566 status("Adding %s to %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 1567 writefile(theme_stylesheet_filename, s) 1568 1569 def make_page_package(self, page_directory, package_filename): 1570 1571 """ 1572 Make a package containing the pages in 'page_directory', using the 1573 filenames as the page names, and writing the package to a file with the 1574 given 'package_filename'. 1575 """ 1576 1577 package = ZipFile(package_filename, "w") 1578 1579 try: 1580 script = ["MoinMoinPackage|1"] 1581 self._add_page_package_files(page_directory, "", package, script) 1582 package.writestr("MOIN_PACKAGE", "\n".join(script) + "\n") 1583 1584 finally: 1585 package.close() 1586 1587 def _add_page_package_files(self, page_directory, prefix, package, script): 1588 1589 """ 1590 Add files in the given 'page_directory' as pages with the given 'prefix' 1591 to the given 'package', recording them in the given 'script'. 1592 """ 1593 1594 filenames = listdir(page_directory) 1595 filenames.sort() 1596 1597 for filename in filenames: 1598 pathname = join(page_directory, filename) 1599 1600 # Add files as pages having the filename as page name. 1601 1602 if os.path.isfile(pathname): 1603 pagename = prefix + filename 1604 zipname = pagename.replace("/", "__") 1605 package.write(pathname, zipname) 1606 script.append("AddRevision|%s|%s" % (zipname, pagename)) 1607 1608 elif os.path.isdir(pathname): 1609 1610 # Add directories ending with "-attachments" as collections of 1611 # attachments for a particular page. 1612 1613 if filename.endswith("-attachments"): 1614 pagename = prefix + filename[:-len("-attachments")] 1615 zipname = pagename.replace("/", "__") 1616 1617 # Add each file as an attachment. 1618 1619 for attachment in listdir(pathname): 1620 azipname = "%s_%s" % (zipname, attachment) 1621 package.write(join(pathname, attachment), azipname) 1622 script.append("AddAttachment|%s|%s|%s||" % ( 1623 azipname, attachment, pagename)) 1624 1625 # Descend into other directories. 1626 1627 else: 1628 pagename = prefix + filename 1629 self._add_page_package_files(pathname, "%s/" % pagename, package, script) 1630 1631 def install_page_package(self, package_filename): 1632 1633 """ 1634 Install a package from the file with the given 'package_filename'. 1635 """ 1636 1637 path = self._set_pythonpath() 1638 installer = join(self.prefix_site_packages, "MoinMoin", "packages.py") 1639 cmd = "%s %s i %s %s" % (sys.executable, installer, package_filename, self.url_complete or self.url_path) 1640 os.system(cmd) 1641 self._reset_pythonpath(path) 1642 1643 def show_methods(): 1644 print "Methods:" 1645 print 1646 for method_name in Installation.method_names: 1647 doc = getattr(Installation, method_name).__doc__.strip() 1648 print "%-30s%-s" % (method_name, format(doc, 30)) 1649 print 1650 1651 # Command line option syntax. 1652 1653 syntax_description = "[ -f <config-filename> ] ( -m <method> | --method=METHOD ) [ <method-argument> ... ]" 1654 1655 # Main program. 1656 1657 if __name__ == "__main__": 1658 from ConfigParser import ConfigParser 1659 import sys, cmdsyntax 1660 1661 # Check the command syntax. 1662 1663 syntax = cmdsyntax.Syntax(syntax_description) 1664 try: 1665 matches = syntax.get_args(sys.argv[1:]) 1666 args = matches[0] 1667 except IndexError: 1668 print "Syntax:" 1669 print sys.argv[0], syntax_description 1670 print 1671 show_methods() 1672 sys.exit(1) 1673 1674 # Obtain configuration details. 1675 1676 try: 1677 config_filename = args.get("config-filename", "moinsetup.cfg") 1678 1679 if not exists(config_filename): 1680 print "Configuration", config_filename, "not found." 1681 sys.exit(1) 1682 1683 config = ConfigParser() 1684 config.read(config_filename) 1685 1686 # Obtain as many arguments as needed from the configuration. 1687 1688 config_arguments = dict(config.items("installation") + config.items("site")) 1689 method_arguments = args.get("method-argument", []) 1690 1691 # Attempt to initialise the configuration. 1692 1693 installation = Installation(**config_arguments) 1694 1695 except TypeError, exc: 1696 print "Error:" 1697 print 1698 print exc.args[0] 1699 print 1700 print "Configuration settings:" 1701 print 1702 print Installation.__init__.__doc__ 1703 print 1704 sys.exit(1) 1705 1706 # Obtain the method. 1707 1708 try: 1709 method = getattr(installation, args["method"]) 1710 except AttributeError: 1711 show_methods() 1712 sys.exit(1) 1713 1714 try: 1715 method(*method_arguments) 1716 except TypeError, exc: 1717 print "Error:" 1718 print 1719 print exc.args[0] 1720 print 1721 print "Method documentation:" 1722 print 1723 print method.__doc__ 1724 print 1725 sys.exit(1) 1726 1727 # vim: tabstop=4 expandtab shiftwidth=4