1 #!/usr/bin/env python 2 3 """ 4 A setup and configuration script for MoinMoin. 5 6 Copyright (C) 2010, 2011 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from os.path import abspath, exists, extsep, isdir, join, normpath, split 23 from getpass import getpass 24 from glob import glob 25 from zipfile import ZipFile 26 import os 27 import sys 28 import shutil 29 import re 30 import tempfile 31 32 __version__ = "0.2" 33 34 # Regular expressions for editing MoinMoin scripts and configuration files. 35 36 def compile_definition(name): 37 return re.compile(r"^(\s*)#*\s*(%s =)\s*(.*)$" % name, re.MULTILINE) 38 39 moin_cgi_prefix = re.compile("^#sys\.path\.insert\(0, 'PREFIX.*$", re.MULTILINE) 40 moin_cgi_wikiconfig = re.compile("^#sys\.path\.insert\(0, '/path/to/wikiconfigdir.*$", re.MULTILINE) 41 moin_cgi_properties = compile_definition("properties") 42 moin_cgi_fix_script_name = compile_definition("fix_script_name") 43 moin_cgi_force_cgi = re.compile("^#(os.environ\['FCGI_FORCE_CGI'\].*)$", re.MULTILINE) 44 45 css_import_stylesheet = re.compile("(\s*@import\s+[\"'])(.*?)([\"']\s*;)") 46 47 # Templates for Apache site definitions. 48 49 apache_site = """ 50 ScriptAlias %(url_path)s "%(web_app_dir)s/moin.cgi" 51 """ 52 53 apache_site_extra_moin18 = """ 54 Alias %(static_url_path)s "%(htdocs_dir)s/" 55 """ 56 57 # Limited hosting .htaccess definitions require the following settings to be 58 # configured in the main Apache configuration files: 59 # 60 # Options ExecCGI FollowSymLinks Indexes SymLinksIfOwnerMatch 61 # AllowOverride FileInfo Limit 62 # AddHandler cgi-script .cgi 63 64 apache_htaccess_combined_mod_rewrite = """ 65 DirectoryIndex moin.cgi 66 RewriteEngine On 67 RewriteBase %(url_path)s 68 RewriteCond %%{REQUEST_FILENAME} !-f 69 RewriteCond %%{REQUEST_FILENAME} !-d 70 RewriteRule ^(.*) moin.cgi/$1 [PT,L,QSA] 71 """ 72 73 # Post-setup templates. 74 75 postsetup_setfacl = """#!/bin/sh 76 77 find '%(conf_dir)s/data' -type f | xargs setfacl -m u:%(web_user)s:rw 78 find '%(conf_dir)s/data' -type d | xargs setfacl -m u:%(web_user)s:rwx 79 find '%(conf_dir)s/underlay' -type f | xargs setfacl -m u:%(web_user)s:rw 80 find '%(conf_dir)s/underlay' -type d | xargs setfacl -m u:%(web_user)s:rwx 81 """ 82 83 postsetup_setfacl_moin18_extra = """ 84 find '%(htdocs_dir)s' -type f | xargs setfacl -m u:%(web_user)s:r 85 find '%(htdocs_dir)s' -type d | xargs setfacl -m u:%(web_user)s:rx 86 """ 87 88 postsetup_chown_chmod = """#!/bin/sh 89 90 chown -R %(this_user)s.%(web_group)s '%(conf_dir)s/data' 91 chown -R %(this_user)s.%(web_group)s '%(conf_dir)s/underlay' 92 chmod -R g+w '%(conf_dir)s/data' 93 chmod -R g+w '%(conf_dir)s/underlay' 94 """ 95 96 postsetup_chown_moin18_extra = """ 97 chown -R %(this_user)s.%(web_group)s '%(htdocs_dir)s' 98 """ 99 100 # Utility functions. 101 102 def readfile(filename): 103 f = open(filename) 104 try: 105 return f.read() 106 finally: 107 f.close() 108 109 def writefile(filename, s): 110 f = open(filename, "w") 111 try: 112 f.write(s) 113 finally: 114 f.close() 115 116 def status(message): 117 print message 118 119 note = status 120 error = status 121 122 def format(s, indent): 123 return re.sub("\n\s+", "\n" + " " * indent, s) 124 125 # Classes. 126 127 class Configuration: 128 129 "A class representing the configuration." 130 131 special_names = ["site_name"] 132 133 def __init__(self, filename): 134 self.content = readfile(filename) 135 self.filename = filename 136 137 def get_pattern(self, name): 138 139 # Make underscores optional for certain names. 140 141 if name in self.special_names: 142 name = name.replace("_", "_?") 143 144 return compile_definition(name) 145 146 def get(self, name): 147 148 """ 149 Return the raw value of the last definition having the given 'name'. 150 """ 151 152 pattern = self.get_pattern(name) 153 results = [match.group(3) for match in pattern.finditer(self.content)] 154 if results: 155 return results[-1] 156 else: 157 return None 158 159 def set(self, name, value, count=None, raw=0): 160 161 """ 162 Set the configuration parameter having the given 'name' with the given 163 'value', limiting the number of appropriately named parameters changed 164 to 'count', if specified. 165 166 If the configuration parameter of the given 'name' does not exist, 167 insert such a parameter at the end of the file. 168 169 If the optional 'raw' parameter is specified and set to a true value, 170 the provided 'value' is inserted directly into the configuration file. 171 """ 172 173 if not self.replace(name, value, count, raw): 174 self.insert(name, value, raw) 175 176 def replace(self, name, value, count=None, raw=0): 177 178 """ 179 Replace configuration parameters having the given 'name' with the given 180 'value', limiting the number of appropriately named parameters changed 181 to 'count', if specified. 182 183 If the optional 'raw' parameter is specified and set to a true value, 184 the provided 'value' is inserted directly into the configuration file. 185 186 Return the number of substitutions made. 187 """ 188 189 if raw: 190 substitution = r"\1\2 %s" % value 191 else: 192 substitution = r"\1\2 %r" % value 193 194 pattern = self.get_pattern(name) 195 196 if count is None: 197 self.content, n = pattern.subn(substitution, self.content) 198 else: 199 self.content, n = pattern.subn(substitution, self.content, count=count) 200 201 return n 202 203 def insert(self, name, value, raw=0): 204 205 """ 206 Insert the configuration parameter having the given 'name' and 'value'. 207 208 If the optional 'raw' parameter is specified and set to a true value, 209 the provided 'value' is inserted directly into the configuration file. 210 """ 211 212 if raw: 213 insertion = "%s = %s" 214 else: 215 insertion = "%s = %r" 216 217 self.insert_text(insertion % (name, value)) 218 219 def insert_text(self, text): 220 221 "Insert the given 'text' at the end of the configuration." 222 223 if not self.content.endswith("\n"): 224 self.content += "\n" 225 self.content += " %s\n" % text 226 227 def close(self): 228 229 "Close the file, writing the content." 230 231 writefile(self.filename, self.content) 232 233 class Installation: 234 235 "A class for installing and initialising MoinMoin." 236 237 method_names = ( 238 "setup", 239 "setup_wiki", 240 "install_moin", 241 "install_data", 242 "configure_moin", 243 "edit_moin_script", 244 "edit_moin_web_script", 245 "add_superuser", 246 "make_site_files", 247 "make_post_install_script", 248 "reconfigure_moin", 249 "set_auth_method", 250 251 # Post-installation activities. 252 253 "install_theme", 254 "install_extension_package", 255 "install_plugins", 256 "install_actions", 257 "install_macros", 258 "install_theme_resources", 259 "edit_theme_stylesheet", 260 261 # Other activities. 262 263 "make_page_package", 264 "install_page_package", 265 ) 266 267 # NOTE: Need to detect Web server user. 268 269 web_user = "www-data" 270 web_group = "www-data" 271 272 # MoinMoin resources. 273 274 theme_master = "modernized" 275 extra_theme_css_files = ["SlideShow.css"] 276 277 def __init__(self, moin_distribution, prefix, web_app_dir, 278 common_dir, url_path, superuser, site_name, front_page_name, 279 web_site_dir=None, web_static_dir=None, theme_default=None): 280 281 """ 282 Initialise a Wiki installation using the following: 283 284 * moin_distribution - the directory containing a MoinMoin source 285 distribution 286 * prefix - the installation prefix (equivalent to /usr) 287 * web_app_dir - the directory where Web applications and scripts 288 reside (such as /home/www-user/cgi-bin) 289 * common_dir - the directory where the Wiki configuration, 290 resources and instance will reside (such as 291 /home/www-user/mywiki) 292 * url_path - the URL path at which the Wiki will be made 293 available (such as / or /mywiki) 294 * superuser - the name of the site's superuser (such as 295 "AdminUser") 296 * site_name - the name of the site (such as "My Wiki") 297 * front_page_name - the front page name for the site (such as 298 "FrontPage" or a specific name for the site) 299 * web_site_dir - optional: the directory where Web site 300 definitions reside (such as 301 /etc/apache2/sites-available) 302 * web_static_dir - optional: the directory where static Web 303 resources reside (such as /home/www-user/htdocs) 304 * theme_default - optional: the default theme (such as modern) 305 """ 306 307 self.moin_distribution = moin_distribution 308 self.superuser = superuser 309 self.site_name = site_name 310 self.front_page_name = front_page_name 311 self.theme_default = theme_default 312 313 # NOTE: Support the detection of the Apache sites directory. 314 315 self.prefix, self.web_app_dir, self.web_site_dir, self.web_static_dir, self.common_dir = \ 316 map(self._get_abspath, (prefix, web_app_dir, web_site_dir, web_static_dir, common_dir)) 317 318 # Strip any trailing "/" from the URL path. 319 320 if url_path != "/" and url_path.endswith("/"): 321 self.url_path = url_path[:-1] 322 else: 323 self.url_path = url_path 324 325 # Define and create specific directories. 326 # Here are the configuration and actual Wiki data directories. 327 328 self.conf_dir = join(self.common_dir, "conf") 329 self.instance_dir = join(self.common_dir, "wikidata") 330 331 # Define the place where the MoinMoin package will actually reside. 332 333 self.prefix_site_packages = join(self.prefix, "lib", "python%s.%s" % sys.version_info[:2], "site-packages") 334 335 # Find the version. 336 337 self.moin_version = self.get_moin_version() 338 339 # The static resources reside in different locations depending on the 340 # version of MoinMoin. Moreover, these resources may end up in a 341 # published directory for 1.8 installations where the Web server cannot 342 # be instructed to fetch the content from outside certain designated 343 # locations. 344 345 # 1.9: moin/lib/python2.x/site-packages/MoinMoin/web/static/htdocs 346 347 if self.moin_version.startswith("1.9"): 348 self.htdocs_dir = self.htdocs_dir_source = join(self.prefix_site_packages, "MoinMoin", "web", "static", "htdocs") 349 self.static_url_path = self.url_path 350 351 # 1.8: moin/share/moin/htdocs (optionally copied to a Web directory) 352 353 else: 354 self.htdocs_dir_source = join(self.instance_dir, "share", "moin", "htdocs") 355 356 # Add the static identifier to the URL path. For example: 357 # / -> /moin_static187 358 # /hgwiki -> /hgwiki-moin_static187 359 360 self.static_url_path = self.url_path + (self.url_path != "/" and "-" or "") + self.get_static_identifier() 361 362 # In limited hosting, the static resources directory is related to 363 # the URL path. 364 365 if self.limited_hosting(): 366 self.htdocs_dir = join(self.web_static_dir or self.web_app_dir, self.static_url_path.lstrip("/")) 367 368 # Otherwise, a mapping is made to the directory. 369 370 else: 371 self.htdocs_dir = self.htdocs_dir_source 372 373 def _get_abspath(self, d): 374 return d and abspath(d) or None 375 376 def get_moin_version(self): 377 378 "Inspect the MoinMoin package information, returning the version." 379 380 this_dir = os.getcwd() 381 os.chdir(self.moin_distribution) 382 383 try: 384 try: 385 f = open("PKG-INFO") 386 try: 387 for line in f.xreadlines(): 388 columns = line.split() 389 if columns[0] == "Version:": 390 return columns[1] 391 392 return None 393 394 finally: 395 f.close() 396 397 except IOError: 398 f = os.popen("%s -c 'from MoinMoin.version import release; print release'" % sys.executable) 399 try: 400 return f.read().strip() 401 finally: 402 f.close() 403 finally: 404 os.chdir(this_dir) 405 406 def get_static_identifier(self): 407 408 "Return the static URL/directory identifier for the Wiki." 409 410 return "moin_static%s" % self.moin_version.replace(".", "") 411 412 def get_plugin_directory(self, plugin_type): 413 414 "Return the directory for plugins of the given 'plugin_type'." 415 416 data_dir = join(self.conf_dir, "data") 417 return join(data_dir, "plugin", plugin_type) 418 419 def limited_hosting(self): 420 421 "Return whether limited Web hosting is being used." 422 423 return not self.web_site_dir 424 425 def ensure_directories(self): 426 427 "Make sure that all the directories are available." 428 429 for d in (self.conf_dir, self.instance_dir, self.web_app_dir, self.web_static_dir, self.web_site_dir): 430 if d is not None and not exists(d): 431 os.makedirs(d) 432 433 def get_theme_directories(self, theme_name=None): 434 435 """ 436 Return tuples of the form (theme name, theme directory) for all themes, 437 or for a single theme if the optional 'theme_name' is specified. 438 """ 439 440 filenames = theme_name and [theme_name] or os.listdir(self.htdocs_dir) 441 directories = [] 442 443 for filename in filenames: 444 theme_dir = join(self.htdocs_dir, filename) 445 446 if not exists(theme_dir) or not isdir(theme_dir): 447 continue 448 449 directories.append((filename, theme_dir)) 450 451 return directories 452 453 # Main methods. 454 455 def setup(self): 456 457 "Set up the installation." 458 459 self.ensure_directories() 460 self.install_moin() 461 self._setup_wiki() 462 463 def setup_wiki(self): 464 465 "Set up a Wiki without installing MoinMoin." 466 467 self.ensure_directories() 468 self.install_moin(data_only=1) 469 self._setup_wiki() 470 471 def _setup_wiki(self): 472 473 "Set up a Wiki without installing MoinMoin." 474 475 self.install_data() 476 self.configure_moin() 477 self.edit_moin_script() 478 self.add_superuser() 479 self.edit_moin_web_script(self.make_site_files()) 480 self.make_post_install_script() 481 482 def install_moin(self, data_only=0): 483 484 "Enter the distribution directory and run the setup script." 485 486 # NOTE: Possibly check for an existing installation and skip repeated 487 # NOTE: installation attempts. 488 489 this_dir = os.getcwd() 490 os.chdir(self.moin_distribution) 491 492 log_filename = "install-%s.log" % split(self.common_dir)[-1] 493 494 status("Installing MoinMoin%s in %s..." % (data_only and " (data only)" or "", self.prefix)) 495 496 if data_only: 497 install_cmd = "install_data" 498 options = "--install-dir='%s'" % self.instance_dir 499 else: 500 install_cmd = "install" 501 options = "--prefix='%s' --install-data='%s' --record='%s'" % (self.prefix, self.instance_dir, log_filename) 502 503 os.system("python setup.py --quiet %s %s --force" % (install_cmd, options)) 504 505 os.chdir(this_dir) 506 507 def install_data(self): 508 509 "Install Wiki data." 510 511 # The default wikiconfig assumes data and underlay in the same directory. 512 513 status("Installing data and underlay in %s..." % self.conf_dir) 514 515 for d in ("data", "underlay"): 516 source = join(self.moin_distribution, "wiki", d) 517 source_tar = source + os.path.extsep + "tar" 518 d_tar = source + os.path.extsep + "tar" 519 520 if os.path.exists(source): 521 shutil.copytree(source, join(self.conf_dir, d)) 522 elif os.path.exists(source_tar): 523 shutil.copy(source_tar, self.conf_dir) 524 os.system("tar xf %s -C %s" % (d_tar, self.conf_dir)) 525 else: 526 status("Could not copy %s into installed Wiki." % d) 527 528 # Copy static Web data if appropriate. 529 530 if not self.moin_version.startswith("1.9") and self.limited_hosting(): 531 532 if not exists(self.htdocs_dir): 533 os.mkdir(self.htdocs_dir) 534 535 for item in os.listdir(self.htdocs_dir_source): 536 path = join(self.htdocs_dir_source, item) 537 if isdir(path): 538 shutil.copytree(path, join(self.htdocs_dir, item)) 539 else: 540 shutil.copy(path, join(self.htdocs_dir, item)) 541 542 def configure_moin(self): 543 544 "Edit the Wiki configuration file." 545 546 # NOTE: Single Wiki only so far. 547 548 # Static URLs seem to be different in MoinMoin 1.9.x. 549 # For earlier versions, reserve URL space alongside the Wiki. 550 # NOTE: MoinMoin usually uses an apparently common URL space associated 551 # NOTE: with the version, but more specific locations are probably 552 # NOTE: acceptable if less efficient. 553 554 if self.moin_version.startswith("1.9"): 555 url_prefix_static = "%r + url_prefix_static" % self.static_url_path 556 else: 557 url_prefix_static = "%r" % self.static_url_path 558 559 # Copy the Wiki configuration file from the distribution. 560 561 wikiconfig_py = join(self.conf_dir, "wikiconfig.py") 562 shutil.copyfile(join(self.moin_distribution, "wiki", "config", "wikiconfig.py"), wikiconfig_py) 563 564 status("Editing configuration from %s..." % wikiconfig_py) 565 566 # Edit the Wiki configuration file. 567 568 wikiconfig = Configuration(wikiconfig_py) 569 570 try: 571 wikiconfig.set("url_prefix_static", url_prefix_static, raw=1) 572 wikiconfig.set("superuser", [self.superuser]) 573 wikiconfig.set("acl_rights_before", u"%s:read,write,delete,revert,admin" % self.superuser) 574 575 if not self.moin_version.startswith("1.9"): 576 data_dir = join(self.conf_dir, "data") 577 data_underlay_dir = join(self.conf_dir, "underlay") 578 579 wikiconfig.set("data_dir", data_dir) 580 wikiconfig.set("data_underlay_dir", data_underlay_dir) 581 582 self._configure_moin(wikiconfig) 583 584 finally: 585 wikiconfig.close() 586 587 def _configure_moin(self, wikiconfig): 588 589 """ 590 Configure Moin, accessing the configuration file using 'wikiconfig'. 591 """ 592 593 wikiconfig.set("site_name", self.site_name) 594 wikiconfig.set("page_front_page", self.front_page_name, count=1) 595 596 if self.theme_default is not None: 597 wikiconfig.set("theme_default", self.theme_default) 598 599 def edit_moin_script(self): 600 601 "Edit the moin script." 602 603 moin_script = join(self.prefix, "bin", "moin") 604 605 status("Editing moin script at %s..." % moin_script) 606 607 s = readfile(moin_script) 608 s = s.replace("#import sys", "import sys\nsys.path.insert(0, %r)" % self.prefix_site_packages) 609 610 writefile(moin_script, s) 611 612 def edit_moin_web_script(self, site_file_configured=1): 613 614 "Edit and install CGI script." 615 616 # NOTE: CGI only so far. 617 # NOTE: Permissions should be checked. 618 619 if self.moin_version.startswith("1.9"): 620 moin_cgi = join(self.instance_dir, "share", "moin", "server", "moin.fcgi") 621 else: 622 moin_cgi = join(self.instance_dir, "share", "moin", "server", "moin.cgi") 623 624 moin_cgi_installed = join(self.web_app_dir, "moin.cgi") 625 626 status("Editing moin.cgi script from %s..." % moin_cgi) 627 628 s = readfile(moin_cgi) 629 s = moin_cgi_prefix.sub("sys.path.insert(0, %r)" % self.prefix_site_packages, s) 630 s = moin_cgi_wikiconfig.sub("sys.path.insert(0, %r)" % self.conf_dir, s) 631 632 # Handle differences in script names when using limited hosting with 633 # URL rewriting. 634 635 if self.limited_hosting(): 636 if not site_file_configured: 637 note("Site file not configured: script name not changed.") 638 else: 639 if self.moin_version.startswith("1.9"): 640 s = moin_cgi_fix_script_name.sub(r"\1\2 %r" % self.url_path, s) 641 else: 642 s = moin_cgi_properties.sub(r"\1\2 %r" % {"script_name" : self.url_path}, s) 643 644 # NOTE: Use CGI for now. 645 646 if self.moin_version.startswith("1.9"): 647 s = moin_cgi_force_cgi.sub(r"\1", s) 648 649 writefile(moin_cgi_installed, s) 650 os.system("chmod a+rx '%s'" % moin_cgi_installed) 651 652 def add_superuser(self): 653 654 "Add the superuser account." 655 656 moin_script = join(self.prefix, "bin", "moin") 657 658 print "Creating superuser", self.superuser, "using..." 659 email = raw_input("E-mail address: ") 660 password = getpass("Password: ") 661 662 path = os.environ.get("PYTHONPATH", "") 663 664 if path: 665 os.environ["PYTHONPATH"] = path + ":" + self.conf_dir 666 else: 667 os.environ["PYTHONPATH"] = self.conf_dir 668 669 os.system(moin_script + " account create --name='%s' --email='%s' --password='%s'" % (self.superuser, email, password)) 670 671 if path: 672 os.environ["PYTHONPATH"] = path 673 else: 674 del os.environ["PYTHONPATH"] 675 676 def make_site_files(self): 677 678 "Make the Apache site files." 679 680 # NOTE: Using local namespace for substitution. 681 682 # Where the site definitions and applications directories are different, 683 # use a normal site definition. 684 685 if not self.limited_hosting(): 686 687 site_def = join(self.web_site_dir, self.site_name) 688 689 s = apache_site % self.__dict__ 690 691 if not self.moin_version.startswith("1.9"): 692 s += apache_site_extra_moin18 % self.__dict__ 693 694 status("Writing Apache site definitions to %s..." % site_def) 695 writefile(site_def, s) 696 697 note("Copy the site definitions to the appropriate sites directory if appropriate.") 698 note("Then, make sure that the site is enabled by running the appropriate tools (such as a2ensite).") 699 700 return 1 701 702 # Otherwise, use an .htaccess file. 703 704 else: 705 site_def = join(self.web_app_dir, ".htaccess") 706 707 s = apache_htaccess_combined_mod_rewrite % self.__dict__ 708 709 status("Writing .htaccess file to %s..." % site_def) 710 try: 711 writefile(site_def, s) 712 except IOError: 713 note("The .htaccess file could not be written. This will also affect the script name setting.") 714 return 0 715 else: 716 return 1 717 718 def make_post_install_script(self): 719 720 "Write a post-install script with additional actions." 721 722 # Work out whether setfacl works. 723 724 fd, temp_filename = tempfile.mkstemp(dir=self.conf_dir) 725 os.close(fd) 726 727 have_setfacl = os.system("setfacl -m user:%(web_user)s:r %(file)s > /dev/null 2>&1" % { 728 "web_user" : self.web_user, "file" : temp_filename}) == 0 729 730 os.remove(temp_filename) 731 732 # Create the scripts. 733 734 this_user = os.environ["USER"] 735 postinst_scripts = "moinsetup-post-chown.sh", "moinsetup-post-setfacl.sh" 736 737 vars = {} 738 vars.update(Installation.__dict__) 739 vars.update(self.__dict__) 740 vars.update(locals()) 741 742 for postinst_script, start, extra in [ 743 (postinst_scripts[0], postsetup_chown_chmod, postsetup_chown_moin18_extra), 744 (postinst_scripts[1], postsetup_setfacl, postsetup_setfacl_moin18_extra) 745 ]: 746 747 s = start % vars 748 749 if not self.moin_version.startswith("1.9"): 750 s += extra % vars 751 752 writefile(postinst_script, s) 753 os.chmod(postinst_script, 0755) 754 755 if have_setfacl: 756 note("Run %s to set file ownership and permissions." % postinst_scripts[1]) 757 note("If this somehow fails...") 758 759 note("Run %s as root to set file ownership and permissions." % postinst_scripts[0]) 760 761 # Accessory methods. 762 763 def reconfigure_moin(self, name=None, value=None, raw=0): 764 765 """ 766 Edit the installed Wiki configuration file, setting a parameter with any 767 given 'name' to the given 'value', treating the value as a raw 768 expression (not a string) if 'raw' is set to a true value. 769 770 If 'name' and the remaining parameters are omitted, the default 771 configuration activity is performed. 772 """ 773 774 wikiconfig_py = join(self.conf_dir, "wikiconfig.py") 775 776 status("Editing configuration from %s..." % wikiconfig_py) 777 778 wikiconfig = Configuration(wikiconfig_py) 779 780 try: 781 # Perform default configuration. 782 783 if name is None and value is None: 784 self._configure_moin(wikiconfig) 785 else: 786 wikiconfig.set(name, value, raw=raw) 787 788 finally: 789 wikiconfig.close() 790 791 def set_auth_method(self, method_name): 792 793 """ 794 Edit the installed Wiki configuration file, configuring the 795 authentication method having the given 'method_name'. 796 """ 797 798 wikiconfig_py = join(self.conf_dir, "wikiconfig.py") 799 800 status("Editing configuration from %s..." % wikiconfig_py) 801 802 wikiconfig = Configuration(wikiconfig_py) 803 804 try: 805 if method_name.lower() == "openid": 806 wikiconfig.insert_text("from MoinMoin.auth.openidrp import OpenIDAuth") 807 808 if wikiconfig.get("anonymous_session_lifetime"): 809 wikiconfig.replace("anonymous_session_lifetime", "1000", raw=1) 810 else: 811 wikiconfig.set("anonymous_session_lifetime", "1000", raw=1) 812 813 auth = wikiconfig.get("auth") 814 if auth: 815 wikiconfig.replace("auth", "%s + [OpenIDAuth()]" % auth, raw=1) 816 else: 817 wikiconfig.set("auth", "[OpenIDAuth()]", raw=1) 818 819 finally: 820 wikiconfig.close() 821 822 def install_theme(self, theme_dir, theme_name=None): 823 824 """ 825 Install Wiki theme provided in the given 'theme_dir' having the given 826 optional 'theme_name' (if different from the 'theme_dir' name). 827 """ 828 829 theme_dir = normpath(theme_dir) 830 theme_name = theme_name or split(theme_dir)[-1] 831 theme_module = join(theme_dir, theme_name + extsep + "py") 832 833 plugin_theme_dir = self.get_plugin_directory("theme") 834 835 # Copy the theme module. 836 837 status("Copying theme module to %s..." % plugin_theme_dir) 838 839 shutil.copy(theme_module, plugin_theme_dir) 840 841 # Copy the resources. 842 843 resources_dir = join(self.htdocs_dir, theme_name) 844 845 if not exists(resources_dir): 846 os.mkdir(resources_dir) 847 848 status("Copying theme resources to %s..." % resources_dir) 849 850 for d in ("css", "img"): 851 target_dir = join(resources_dir, d) 852 if exists(target_dir): 853 status("Replacing %s..." % target_dir) 854 shutil.rmtree(target_dir) 855 shutil.copytree(join(theme_dir, d), target_dir) 856 857 # Copy additional resources from other themes. 858 859 resources_source_dir = join(self.htdocs_dir, self.theme_master) 860 target_dir = join(resources_dir, "css") 861 862 status("Copying resources from %s..." % resources_source_dir) 863 864 for css_file in self.extra_theme_css_files: 865 css_file_path = join(resources_source_dir, "css", css_file) 866 if exists(css_file_path): 867 shutil.copy(css_file_path, target_dir) 868 869 note("Don't forget to add theme resources for extensions for this theme.") 870 note("Don't forget to edit this theme's stylesheets for extensions.") 871 872 def install_extension_package(self, extension_dir): 873 874 "Install any libraries from 'extension_dir' using a setup script." 875 876 this_dir = os.getcwd() 877 os.chdir(extension_dir) 878 os.system("python setup.py install --prefix=%s" % self.prefix) 879 os.chdir(this_dir) 880 881 def install_plugins(self, plugins_dir, plugin_type): 882 883 """ 884 Install Wiki actions provided in the given 'plugins_dir' of the 885 specified 'plugin_type'. 886 """ 887 888 plugin_target_dir = self.get_plugin_directory(plugin_type) 889 890 # Copy the modules. 891 892 status("Copying %s modules to %s..." % (plugin_type, plugin_target_dir)) 893 894 for module in glob(join(plugins_dir, "*%spy" % extsep)): 895 shutil.copy(module, plugin_target_dir) 896 897 def install_actions(self, actions_dir): 898 899 "Install Wiki actions provided in the given 'actions_dir'." 900 901 self.install_plugins(actions_dir, "action") 902 903 def install_macros(self, macros_dir): 904 905 "Install Wiki macros provided in the given 'macros_dir'." 906 907 self.install_plugins(macros_dir, "macro") 908 909 def install_theme_resources(self, theme_resources_dir, theme_name=None): 910 911 """ 912 Install theme resources provided in the given 'theme_resources_dir'. If 913 a specific 'theme_name' is given, only that theme will be given the 914 specified resources. 915 """ 916 917 for theme_name, theme_dir in self.get_theme_directories(theme_name): 918 919 # Copy the resources. 920 921 copied = 0 922 923 for d in ("css", "img"): 924 source_dir = join(theme_resources_dir, d) 925 target_dir = join(theme_dir, d) 926 927 if not exists(target_dir): 928 continue 929 930 for resource in glob(join(source_dir, "*%s*" % extsep)): 931 shutil.copy(resource, target_dir) 932 copied = 1 933 934 if copied: 935 status("Copied theme resources into %s..." % theme_dir) 936 937 note("Don't forget to edit theme stylesheets for any extensions.") 938 939 def edit_theme_stylesheet(self, theme_stylesheet, imported_stylesheet, action="ensure", theme_name=None): 940 941 """ 942 Edit the given 'theme_stylesheet', ensuring (or removing) a reference to 943 the 'imported_stylesheet' according to the given 'action' (optional, 944 defaulting to "ensure"). If a specific 'theme_name' is given, only that 945 theme will be affected. 946 """ 947 948 if action == "ensure": 949 ensure = 1 950 elif action == "remove": 951 ensure = 0 952 else: 953 error("Action %s not valid: it must be given as either 'ensure' or 'remove'." % action) 954 return 955 956 for theme_name, theme_dir in self.get_theme_directories(theme_name): 957 958 # Locate the resources. 959 960 css_dir = join(theme_dir, "css") 961 962 if not exists(css_dir): 963 continue 964 965 theme_stylesheet_filename = join(css_dir, theme_stylesheet) 966 imported_stylesheet_filename = join(css_dir, imported_stylesheet) 967 968 if not exists(theme_stylesheet_filename): 969 error("Stylesheet %s not defined in theme %s." % (theme_stylesheet, theme_name)) 970 continue 971 972 if not exists(imported_stylesheet_filename): 973 error("Stylesheet %s not defined in theme %s." % (imported_stylesheet, theme_name)) 974 continue 975 976 # Edit the resources. 977 978 s = readfile(theme_stylesheet_filename) 979 after_point = 0 980 981 for stylesheet_import in css_import_stylesheet.finditer(s): 982 before, filename, after = stylesheet_import.groups() 983 before_point, after_point = stylesheet_import.span() 984 985 # Test the import for a reference to the requested imported 986 # stylesheet. 987 988 if filename == imported_stylesheet: 989 if ensure: 990 break 991 else: 992 if s[after_point:after_point+1] == "\n": 993 after_point += 1 994 s = "%s%s" % (s[:before_point], s[after_point:]) 995 996 status("Removing %s from %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 997 writefile(theme_stylesheet_filename, s) 998 break 999 1000 # Where no import references the imported stylesheet, insert a 1001 # reference into the theme stylesheet. 1002 1003 else: 1004 if ensure: 1005 1006 # Assume that the stylesheet can follow other imports. 1007 1008 if s[after_point:after_point+1] == "\n": 1009 after_point += 1 1010 s = "%s%s\n%s" % (s[:after_point], '@import "%s";' % imported_stylesheet, s[after_point:]) 1011 1012 status("Adding %s to %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name)) 1013 writefile(theme_stylesheet_filename, s) 1014 1015 def make_page_package(self, page_directory, package_filename): 1016 1017 """ 1018 Make a package containing the pages in 'page_directory', using the 1019 filenames as the page names, and writing the package to a file with the 1020 given 'package_filename'. 1021 """ 1022 1023 package = ZipFile(package_filename, "w") 1024 1025 try: 1026 script = ["MoinMoinPackage|1"] 1027 1028 for filename in os.listdir(page_directory): 1029 package.write(join(page_directory, filename), filename) 1030 script.append("AddRevision|%s|%s" % (filename, filename)) 1031 1032 package.writestr("MOIN_PACKAGE", "\n".join(script)) 1033 1034 finally: 1035 package.close() 1036 1037 def install_page_package(self, package_filename): 1038 1039 """ 1040 Install a package from the file with the given 'package_filename'. 1041 """ 1042 1043 path = os.environ.get("PYTHONPATH", "") 1044 1045 if path: 1046 os.environ["PYTHONPATH"] = path + ":" + self.prefix_site_packages + ":" + self.conf_dir 1047 else: 1048 os.environ["PYTHONPATH"] = self.prefix_site_packages + ":" + self.conf_dir 1049 1050 installer = join(self.prefix_site_packages, "MoinMoin", "packages.py") 1051 os.system("python %s i %s" % (installer, package_filename)) 1052 1053 if path: 1054 os.environ["PYTHONPATH"] = path 1055 else: 1056 del os.environ["PYTHONPATH"] 1057 1058 def show_methods(): 1059 print "Methods:" 1060 print 1061 for method_name in Installation.method_names: 1062 doc = getattr(Installation, method_name).__doc__.strip() 1063 print "%-30s%-s" % (method_name, format(doc, 30)) 1064 print 1065 1066 # Command line option syntax. 1067 1068 syntax_description = "[ -f <config-filename> ] ( <method> | --method=METHOD ) [ <method-argument> ... ]" 1069 1070 # Main program. 1071 1072 if __name__ == "__main__": 1073 from ConfigParser import ConfigParser 1074 import sys, cmdsyntax 1075 1076 # Check the command syntax. 1077 1078 syntax = cmdsyntax.Syntax(syntax_description) 1079 try: 1080 matches = syntax.get_args(sys.argv[1:]) 1081 args = matches[0] 1082 except IndexError: 1083 print "Syntax:" 1084 print sys.argv[0], syntax_description 1085 print 1086 show_methods() 1087 sys.exit(1) 1088 1089 # Obtain configuration details. 1090 1091 try: 1092 config_filename = args.get("config-filename", "moinsetup.cfg") 1093 config = ConfigParser() 1094 config.read(config_filename) 1095 1096 # Obtain as many arguments as needed from the configuration. 1097 1098 config_arguments = dict(config.items("installation") + config.items("site")) 1099 method_arguments = args.get("method-argument", []) 1100 1101 # Attempt to initialise the configuration. 1102 1103 installation = Installation(**config_arguments) 1104 1105 except TypeError: 1106 print "Configuration settings:" 1107 print 1108 print Installation.__init__.__doc__ 1109 print 1110 sys.exit(1) 1111 1112 # Obtain the method. 1113 1114 try: 1115 method = getattr(installation, args["method"]) 1116 except AttributeError: 1117 show_methods() 1118 sys.exit(1) 1119 1120 try: 1121 method(*method_arguments) 1122 except TypeError: 1123 print "Method documentation:" 1124 print 1125 print method.__doc__ 1126 print 1127 raise 1128 1129 # vim: tabstop=4 expandtab shiftwidth=4