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