moinsetup

Annotated moinsetup.py

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