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