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