1 #!/usr/bin/env python 2 3 from os.path import abspath, exists, extsep, isdir, join, normpath, split 4 from getpass import getpass 5 from glob import glob 6 import os 7 import sys 8 import shutil 9 import re 10 11 # Regular expressions for editing MoinMoin scripts and configuration files. 12 13 def compile_definition(name): 14 return re.compile(r"^(\s*)#*\s*(%s =).*$" % name, re.MULTILINE) 15 16 moin_cgi_prefix = re.compile("^#sys\.path\.insert\(0, 'PREFIX.*$", re.MULTILINE) 17 moin_cgi_wikiconfig = re.compile("^#sys\.path\.insert\(0, '/path/to/wikiconfigdir.*$", re.MULTILINE) 18 moin_cgi_properties = compile_definition("properties") 19 moin_cgi_fix_script_name = compile_definition("fix_script_name") 20 moin_cgi_force_cgi = re.compile("^#(os.environ\['FCGI_FORCE_CGI'\].*)$", re.MULTILINE) 21 22 css_import_stylesheet = re.compile("^(\s*@import\s+[\"'])(.*?)([\"'].*)$", re.MULTILINE) 23 24 # Templates for Apache site definitions. 25 26 apache_site = """ 27 ScriptAlias %(url_path)s "%(web_app_dir)s/moin.cgi" 28 """ 29 30 apache_site_extra_moin18 = """ 31 Alias %(static_url_path)s "%(htdocs_dir)s/" 32 """ 33 34 # Limited hosting .htaccess definitions require the following settings to be 35 # configured in the main Apache configuration files: 36 # 37 # Options ExecCGI FollowSymLinks Indexes SymLinksIfOwnerMatch 38 # AllowOverride FileInfo Limit 39 # AddHandler cgi-script .cgi 40 41 apache_htaccess_combined_mod_rewrite = """ 42 DirectoryIndex moin.cgi 43 RewriteEngine On 44 RewriteBase %(url_path)s 45 RewriteCond %%{REQUEST_FILENAME} !-f 46 RewriteCond %%{REQUEST_FILENAME} !-d 47 RewriteRule ^(.*) moin.cgi/$1 [PT,L,QSA] 48 """ 49 50 # Utility functions. 51 52 def readfile(filename): 53 f = open(filename) 54 try: 55 return f.read() 56 finally: 57 f.close() 58 59 def writefile(filename, s): 60 f = open(filename, "w") 61 try: 62 f.write(s) 63 finally: 64 f.close() 65 66 def status(message): 67 print message 68 69 def note(message): 70 print message 71 72 class Configuration: 73 74 "A class representing the configuration." 75 76 special_names = ["site_name"] 77 78 def __init__(self, filename): 79 self.content = readfile(filename) 80 self.filename = filename 81 82 def get_pattern(self, name): 83 84 # Make underscores optional for certain names. 85 86 if name in self.special_names: 87 name = name.replace("_", "_?") 88 89 return compile_definition(name) 90 91 def set(self, name, value, count=None, raw=0): 92 93 """ 94 Set the configuration parameter having the given 'name' with the given 95 'value', limiting the number of appropriately named parameters changed 96 to 'count', if specified. 97 98 If the configuration parameter of the given 'name' does not exist, 99 insert such a parameter at the end of the file. 100 101 If the optional 'raw' parameter is specified and set to a true value, 102 the provided 'value' is inserted directly into the configuration file. 103 """ 104 105 if not self.replace(name, value, count, raw): 106 self.insert(name, value, raw) 107 108 def replace(self, name, value, count=None, raw=0): 109 110 """ 111 Replace configuration parameters having the given 'name' with the given 112 'value', limiting the number of appropriately named parameters changed 113 to 'count', if specified. 114 115 If the optional 'raw' parameter is specified and set to a true value, 116 the provided 'value' is inserted directly into the configuration file. 117 118 Return the number of substitutions made. 119 """ 120 121 if raw: 122 substitution = r"\1\2 %s" % value 123 else: 124 substitution = r"\1\2 %r" % value 125 126 pattern = self.get_pattern(name) 127 128 if count is None: 129 self.content, n = pattern.subn(substitution, self.content) 130 else: 131 self.content, n = pattern.subn(substitution, self.content, count=count) 132 133 return n 134 135 def insert(self, name, value, raw=0): 136 137 """ 138 Insert the configuration parameter having the given 'name' and 'value'. 139 140 If the optional 'raw' parameter is specified and set to a true value, 141 the provided 'value' is inserted directly into the configuration file. 142 """ 143 144 if raw: 145 insertion = "\n %s = %s\n" 146 else: 147 insertion = "\n %s = %r\n" 148 149 self.content += insertion % (name, value) 150 151 def close(self): 152 153 "Close the file, writing the content." 154 155 writefile(self.filename, self.content) 156 157 class Installation: 158 159 "A class for installing and initialising MoinMoin." 160 161 # NOTE: Need to detect Web server user. 162 163 web_user = "www-data" 164 web_group = "www-data" 165 166 # MoinMoin resources. 167 168 theme_master = "modernized" 169 extra_theme_css_files = ["SlideShow.css"] 170 171 def __init__(self, moin_distribution, prefix, web_app_dir, web_site_dir, 172 common_dir, url_path, superuser, site_name, front_page_name, 173 theme_default=None): 174 175 """ 176 Initialise a Wiki installation using the following: 177 178 * moin_distribution - the directory containing a MoinMoin source 179 distribution 180 * prefix - the installation prefix (equivalent to /usr) 181 * web_app_dir - the directory where Web applications and scripts 182 reside (such as /home/www-user/cgi-bin) 183 * web_site_dir - the directory where Web site definitions reside 184 (such as /etc/apache2/sites-available) 185 * common_dir - the directory where the Wiki configuration, 186 resources and instance will reside (such as 187 /home/www-user/mywiki) 188 * url_path - the URL path at which the Wiki will be made 189 available (such as / or /mywiki) 190 * superuser - the name of the site's superuser (such as 191 "AdminUser") 192 * site_name - the name of the site (such as "My Wiki") 193 * front_page_name - the front page name for the site (such as 194 "FrontPage" or a specific name for the site) 195 * theme_default - optional: the default theme (such as modern) 196 """ 197 198 self.moin_distribution = moin_distribution 199 self.superuser = superuser 200 self.site_name = site_name 201 self.front_page_name = front_page_name 202 self.theme_default = theme_default 203 204 # NOTE: Support the detection of the Apache sites directory. 205 206 self.prefix, self.web_app_dir, self.web_site_dir, self.common_dir = \ 207 map(abspath, (prefix, web_app_dir, web_site_dir, common_dir)) 208 209 # Strip any trailing "/" from the URL path. 210 211 if url_path != "/" and url_path.endswith("/"): 212 self.url_path = url_path[:-1] 213 else: 214 self.url_path = url_path 215 216 # Define and create specific directories. 217 218 self.conf_dir = join(self.common_dir, "conf") 219 self.instance_dir = join(self.common_dir, "wikidata") 220 221 # Define useful directories. 222 223 self.prefix_site_packages = join(self.prefix, "lib", "python%s.%s" % sys.version_info[:2], "site-packages") 224 225 # Find the version. 226 227 self.moin_version = self.get_moin_version() 228 229 # The static resources reside in different locations depending on the 230 # version of MoinMoin. Moreover, these resources may end up in a 231 # published directory for 1.8 installations where the Web server cannot 232 # be instructed to fetch the content from outside certain designated 233 # locations. 234 235 # 1.9: moin/lib/python2.x/site-packages/MoinMoin/web/static/htdocs 236 237 if self.moin_version.startswith("1.9"): 238 self.htdocs_dir = self.htdocs_dir_source = join(self.prefix_site_packages, "MoinMoin", "web", "static", "htdocs") 239 240 # 1.8: moin/share/moin/htdocs (optionally copied to a Web directory) 241 242 else: 243 self.htdocs_dir_source = join(self.instance_dir, "share", "moin", "htdocs") 244 245 if self.limited_hosting(): 246 self.htdocs_dir = join(self.web_app_dir, self.get_static_identifier()) 247 else: 248 self.htdocs_dir = self.htdocs_dir_source 249 250 def get_moin_version(self): 251 252 "Inspect the MoinMoin package information, returning the version." 253 254 this_dir = os.getcwd() 255 os.chdir(self.moin_distribution) 256 257 try: 258 try: 259 f = open("PKG-INFO") 260 try: 261 for line in f.xreadlines(): 262 columns = line.split() 263 if columns[0] == "Version:": 264 return columns[1] 265 266 return None 267 268 finally: 269 f.close() 270 271 except IOError: 272 f = os.popen("%s -c 'from MoinMoin.version import release; print release'" % sys.executable) 273 try: 274 return f.read() 275 finally: 276 f.close() 277 finally: 278 os.chdir(this_dir) 279 280 def get_static_identifier(self): 281 282 "Return the static URL/directory identifier for the Wiki." 283 284 return "moin_static%s" % self.moin_version.replace(".", "") 285 286 def get_plugin_directory(self, plugin_type): 287 288 "Return the directory for plugins of the given 'plugin_type'." 289 290 data_dir = join(self.conf_dir, "data") 291 return join(data_dir, "plugin", plugin_type) 292 293 def limited_hosting(self): 294 295 "Return whether limited Web hosting is being used." 296 297 return self.web_site_dir == self.web_app_dir 298 299 def ensure_directories(self): 300 301 "Make sure that all the directories are available." 302 303 for d in (self.conf_dir, self.instance_dir, self.web_app_dir, self.web_site_dir): 304 if not exists(d): 305 os.makedirs(d) 306 307 # Main methods. 308 309 def setup(self): 310 311 "Set up the installation." 312 313 self.ensure_directories() 314 self.install_moin() 315 self._setup_wiki() 316 317 def setup_wiki(self): 318 319 "Set up a Wiki without installing MoinMoin." 320 321 self.ensure_directories() 322 self.install_moin(data_only=1) 323 self._setup_wiki() 324 325 def _setup_wiki(self): 326 327 "Set up a Wiki without installing MoinMoin." 328 329 self.install_data() 330 self.configure_moin() 331 self.edit_moin_script() 332 self.edit_moin_web_script() 333 self.add_superuser() 334 self.make_site_files() 335 self.make_post_install_script() 336 337 def install_moin(self, data_only=0): 338 339 "Enter the distribution directory and run the setup script." 340 341 # NOTE: Possibly check for an existing installation and skip repeated 342 # NOTE: installation attempts. 343 344 this_dir = os.getcwd() 345 os.chdir(self.moin_distribution) 346 347 log_filename = "install-%s.log" % split(self.common_dir)[-1] 348 349 status("Installing MoinMoin in %s..." % self.prefix) 350 351 if data_only: 352 install_cmd = "install_data" 353 options = "--install-dir='%s'" % self.instance_dir 354 else: 355 install_cmd = "install" 356 options = "--prefix='%s' --install-data='%s' --record='%s'" % (self.prefix, self.instance_dir, log_filename) 357 358 os.system("python setup.py --quiet %s %s --force" % (install_cmd, options)) 359 360 os.chdir(this_dir) 361 362 def install_data(self): 363 364 "Install Wiki data." 365 366 # The default wikiconfig assumes data and underlay in the same directory. 367 368 status("Installing data and underlay in %s..." % self.conf_dir) 369 370 for d in ("data", "underlay"): 371 source = join(self.moin_distribution, "wiki", d) 372 source_tar = source + os.path.extsep + "tar" 373 d_tar = source + os.path.extsep + "tar" 374 375 if os.path.exists(source): 376 shutil.copytree(source, join(self.conf_dir, d)) 377 elif os.path.exists(source_tar): 378 shutil.copy(source_tar, self.conf_dir) 379 os.system("tar xf %s -C %s" % (d_tar, self.conf_dir)) 380 else: 381 status("Could not copy %s into installed Wiki." % d) 382 383 # Copy static Web data if appropriate. 384 385 if not self.moin_version.startswith("1.9") and self.limited_hosting(): 386 387 if not exists(self.htdocs_dir): 388 os.mkdir(self.htdocs_dir) 389 390 for item in os.listdir(self.htdocs_dir_source): 391 path = join(self.htdocs_dir_source, item) 392 if isdir(path): 393 shutil.copytree(path, join(self.htdocs_dir, item)) 394 else: 395 shutil.copy(path, join(self.htdocs_dir, item)) 396 397 def configure_moin(self): 398 399 "Edit the Wiki configuration file." 400 401 # NOTE: Single Wiki only so far. 402 403 # Static URLs seem to be different in MoinMoin 1.9.x. 404 # For earlier versions, reserve URL space alongside the Wiki. 405 # NOTE: MoinMoin usually uses an apparently common URL space associated 406 # NOTE: with the version, but more specific locations are probably 407 # NOTE: acceptable if less efficient. 408 409 if self.moin_version.startswith("1.9"): 410 self.static_url_path = self.url_path 411 url_prefix_static = "%r + url_prefix_static" % self.static_url_path 412 else: 413 # Add the static identifier to the URL path. For example: 414 # / -> /moin_static187 415 # /hgwiki -> /hgwiki/moin_static187 416 417 self.static_url_path = self.url_path + (self.url_path != "/" and "/" or "") + self.get_static_identifier() 418 url_prefix_static = "%r" % self.static_url_path 419 420 # Copy the Wiki configuration file from the distribution. 421 422 wikiconfig_py = join(self.conf_dir, "wikiconfig.py") 423 shutil.copyfile(join(self.moin_distribution, "wiki", "config", "wikiconfig.py"), wikiconfig_py) 424 425 status("Editing configuration from %s..." % wikiconfig_py) 426 427 # Edit the Wiki configuration file. 428 429 wikiconfig = Configuration(wikiconfig_py) 430 431 try: 432 wikiconfig.set("url_prefix_static", url_prefix_static, raw=1) 433 wikiconfig.set("superuser", [self.superuser]) 434 wikiconfig.set("acl_rights_before", u"%s:read,write,delete,revert,admin" % self.superuser) 435 436 if not self.moin_version.startswith("1.9"): 437 data_dir = join(self.conf_dir, "data") 438 data_underlay_dir = join(self.conf_dir, "underlay") 439 440 wikiconfig.set("data_dir", data_dir) 441 wikiconfig.set("data_underlay_dir", data_underlay_dir) 442 443 self._configure_moin(wikiconfig) 444 445 finally: 446 wikiconfig.close() 447 448 def _configure_moin(self, wikiconfig): 449 450 """ 451 Configure Moin, accessing the configuration file using 'wikiconfig'. 452 """ 453 454 wikiconfig.set("site_name", self.site_name) 455 wikiconfig.set("page_front_page", self.front_page_name, count=1) 456 457 if self.theme_default is not None: 458 wikiconfig.set("theme_default", self.theme_default) 459 460 def edit_moin_script(self): 461 462 "Edit the moin script." 463 464 moin_script = join(self.prefix, "bin", "moin") 465 466 status("Editing moin script at %s..." % moin_script) 467 468 s = readfile(moin_script) 469 s = s.replace("#import sys", "import sys\nsys.path.insert(0, %r)" % self.prefix_site_packages) 470 471 writefile(moin_script, s) 472 473 def edit_moin_web_script(self): 474 475 "Edit and install CGI script." 476 477 # NOTE: CGI only so far. 478 # NOTE: Permissions should be checked. 479 480 if self.moin_version.startswith("1.9"): 481 moin_cgi = join(self.instance_dir, "share", "moin", "server", "moin.fcgi") 482 else: 483 moin_cgi = join(self.instance_dir, "share", "moin", "server", "moin.cgi") 484 485 moin_cgi_installed = join(self.web_app_dir, "moin.cgi") 486 487 status("Editing moin.cgi script from %s..." % moin_cgi) 488 489 s = readfile(moin_cgi) 490 s = moin_cgi_prefix.sub("sys.path.insert(0, %r)" % self.prefix_site_packages, s) 491 s = moin_cgi_wikiconfig.sub("sys.path.insert(0, %r)" % self.conf_dir, s) 492 493 # Handle differences in script names when using limited hosting with 494 # URL rewriting. 495 496 if self.limited_hosting(): 497 if self.moin_version.startswith("1.9"): 498 s = moin_cgi_fix_script_name.sub(r"\1\2 %r" % self.url_path, s) 499 s = moin_cgi_force_cgi.sub(r"\1", s) 500 else: 501 s = moin_cgi_properties.sub(r"\1\2 %r" % {"script_name" : self.url_path}, s) 502 503 writefile(moin_cgi_installed, s) 504 os.system("chmod a+rx '%s'" % moin_cgi_installed) 505 506 def add_superuser(self): 507 508 "Add the superuser account." 509 510 moin_script = join(self.prefix, "bin", "moin") 511 512 print "Creating superuser", self.superuser, "using..." 513 email = raw_input("E-mail address: ") 514 password = getpass("Password: ") 515 516 path = os.environ.get("PYTHONPATH", "") 517 518 if path: 519 os.environ["PYTHONPATH"] = path + ":" + self.conf_dir 520 else: 521 os.environ["PYTHONPATH"] = self.conf_dir 522 523 os.system(moin_script + " account create --name='%s' --email='%s' --password='%s'" % (self.superuser, email, password)) 524 525 if path: 526 os.environ["PYTHONPATH"] = path 527 else: 528 del os.environ["PYTHONPATH"] 529 530 def make_site_files(self): 531 532 "Make the Apache site files." 533 534 # NOTE: Using local namespace for substitution. 535 536 # Where the site definitions and applications directories are different, 537 # use a normal site definition. 538 539 if not self.limited_hosting(): 540 541 site_def = join(self.web_site_dir, self.site_name) 542 543 s = apache_site % self.__dict__ 544 545 if not self.moin_version.startswith("1.9"): 546 s += apache_site_extra_moin18 % self.__dict__ 547 548 # Otherwise, use an .htaccess file. 549 550 else: 551 site_def = join(self.web_site_dir, ".htaccess") 552 553 s = apache_htaccess_combined_mod_rewrite % self.__dict__ 554 555 status("Writing Apache site definitions to %s..." % site_def) 556 557 writefile(site_def, s) 558 559 def make_post_install_script(self): 560 561 "Write a post-install script with additional actions." 562 563 this_user = os.environ["USER"] 564 postinst_script = "moinsetup-post.sh" 565 566 s = "#!/bin/sh\n" 567 568 for d in ("data", "underlay"): 569 s += "chown -R %s.%s '%s'\n" % (this_user, self.web_group, join(self.conf_dir, d)) 570 s += "chmod -R g+w '%s'\n" % join(self.conf_dir, d) 571 572 if not self.moin_version.startswith("1.9"): 573 s += "chown -R %s.%s '%s'\n" % (this_user, self.web_group, self.htdocs_dir) 574 575 writefile(postinst_script, s) 576 os.chmod(postinst_script, 0755) 577 note("Run %s as root to set file ownership and permissions." % postinst_script) 578 579 # Accessory methods. 580 581 def reconfigure_moin(self, name=None, value=None, raw=0): 582 583 "Edit the installed Wiki configuration file." 584 585 wikiconfig_py = join(self.conf_dir, "wikiconfig.py") 586 587 status("Editing configuration from %s..." % wikiconfig_py) 588 589 wikiconfig = Configuration(wikiconfig_py) 590 591 try: 592 # Perform default configuration. 593 594 if name is None and value is None: 595 self._configure_moin(wikiconfig) 596 else: 597 wikiconfig.set(name, value, raw=raw) 598 599 finally: 600 wikiconfig.close() 601 602 def install_theme(self, theme_dir): 603 604 "Install Wiki theme provided in the given 'theme_dir'." 605 606 theme_dir = normpath(theme_dir) 607 theme_name = split(theme_dir)[-1] 608 theme_module = join(theme_dir, theme_name + extsep + "py") 609 610 plugin_theme_dir = self.get_plugin_directory("theme") 611 612 # Copy the theme module. 613 614 status("Copying theme module to %s..." % plugin_theme_dir) 615 616 shutil.copy(theme_module, plugin_theme_dir) 617 618 # Copy the resources. 619 620 resources_dir = join(self.htdocs_dir, theme_name) 621 622 if not exists(resources_dir): 623 os.mkdir(resources_dir) 624 625 status("Copying theme resources to %s..." % resources_dir) 626 627 for d in ("css", "img"): 628 target_dir = join(resources_dir, d) 629 if exists(target_dir): 630 status("Replacing %s..." % target_dir) 631 shutil.rmtree(target_dir) 632 shutil.copytree(join(theme_dir, d), target_dir) 633 634 # Copy additional resources from other themes. 635 636 resources_source_dir = join(self.htdocs_dir, self.theme_master) 637 target_dir = join(resources_dir, "css") 638 639 status("Copying resources from %s..." % resources_source_dir) 640 641 for css_file in self.extra_theme_css_files: 642 css_file_path = join(resources_source_dir, "css", css_file) 643 if exists(css_file_path): 644 shutil.copy(css_file_path, target_dir) 645 646 def install_plugins(self, plugins_dir, plugin_type): 647 648 """ 649 Install Wiki actions provided in the given 'plugins_dir' of the 650 specified 'plugin_type'. 651 """ 652 653 plugin_target_dir = self.get_plugin_directory(plugin_type) 654 655 # Copy the modules. 656 657 status("Copying %s modules to %s..." % (plugin_type, plugin_target_dir)) 658 659 for module in glob(join(plugins_dir, "*%spy" % extsep)): 660 shutil.copy(module, plugin_target_dir) 661 662 def install_actions(self, actions_dir): 663 664 "Install Wiki actions provided in the given 'actions_dir'." 665 666 self.install_plugins(actions_dir, "action") 667 668 def install_macros(self, macros_dir): 669 670 "Install Wiki macros provided in the given 'macros_dir'." 671 672 self.install_plugins(macros_dir, "macro") 673 674 def install_theme_resources(self, theme_resources_dir, theme_name=None): 675 676 """ 677 Install theme resources provided in the given 'theme_resources_dir'. If 678 a specific 'theme_name' is given, only that theme will be given the 679 specified resources. 680 """ 681 682 # Copy the resources. 683 684 filenames = theme_name and [theme_name] or os.listdir(self.htdocs_dir) 685 686 for filename in filenames: 687 theme_dir = join(self.htdocs_dir, filename) 688 689 if not exists(theme_dir) or not isdir(theme_dir): 690 continue 691 692 copied = 0 693 694 for d in ("css", "img"): 695 source_dir = join(theme_resources_dir, d) 696 target_dir = join(theme_dir, d) 697 698 if not exists(target_dir): 699 continue 700 701 for resource in glob(join(source_dir, "*%s*" % extsep)): 702 shutil.copy(resource, target_dir) 703 copied = 1 704 705 if copied: 706 status("Copied theme resources into %s..." % theme_dir) 707 708 # Command line option syntax. 709 710 syntax_description = "<argument> ... --method=METHOD [ <method-argument> ... ]" 711 712 # Main program. 713 714 if __name__ == "__main__": 715 import sys, cmdsyntax 716 717 # Check the command syntax. 718 719 syntax = cmdsyntax.Syntax(syntax_description) 720 try: 721 matches = syntax.get_args(sys.argv[1:]) 722 args = matches[0] 723 724 # Obtain as many arguments as needed for the configuration. 725 726 arguments = args["argument"] 727 method_arguments = args.get("method-argument", []) 728 729 # Attempt to initialise the configuration. 730 731 installation = Installation(*arguments) 732 733 except (IndexError, TypeError): 734 print "Syntax:" 735 print sys.argv[0], syntax_description 736 print 737 print "Arguments:" 738 print Installation.__init__.__doc__ 739 sys.exit(1) 740 741 # Obtain and perform the method. 742 743 if args.has_key("method"): 744 method = getattr(installation, args["method"]) 745 else: 746 method = installation.setup 747 748 method(*method_arguments) 749 750 # vim: tabstop=4 expandtab shiftwidth=4