Back to index

moin  1.9.0~rc2
packages.py
Go to the documentation of this file.
00001 # -*- coding: iso-8859-1 -*-
00002 """
00003     MoinMoin - Package Installer
00004 
00005     @copyright: 2005 MoinMoin:AlexanderSchremmer,
00006                 2007-2009 MoinMoin:ReimarBauer
00007     @license: GNU GPL, see COPYING for details.
00008 """
00009 
00010 import os, re, sys
00011 import zipfile
00012 
00013 from MoinMoin import config, wikiutil, caching, user
00014 from MoinMoin.Page import Page
00015 from MoinMoin.PageEditor import PageEditor
00016 from MoinMoin.logfile import editlog, eventlog
00017 
00018 MOIN_PACKAGE_FILE = 'MOIN_PACKAGE'
00019 MAX_VERSION = 1
00020 
00021 
00022 # Exceptions
00023 class PackageException(Exception):
00024     """ Raised if the package is broken. """
00025     pass
00026 
00027 class ScriptException(Exception):
00028     """ Raised when there is a problem in the script. """
00029 
00030     def __unicode__(self):
00031         """ Return unicode error message """
00032         if isinstance(self.args[0], str):
00033             return unicode(self.args[0], config.charset)
00034         else:
00035             return unicode(self.args[0])
00036 
00037 class RuntimeScriptException(ScriptException):
00038     """ Raised when the script problem occurs at runtime. """
00039 
00040 class ScriptExit(Exception):
00041     """ Raised by the script commands when the script should quit. """
00042 
00043 def event_logfile(self, pagename, pagefile):
00044     # add event log entry
00045     eventtype = 'SAVENEW'
00046     mtime_usecs = wikiutil.timestamp2version(os.path.getmtime(pagefile))
00047     elog = eventlog.EventLog(self.request)
00048     elog.add(self.request, eventtype, {'pagename': pagename}, 1, mtime_usecs)
00049 
00050 def edit_logfile_append(self, pagename, pagefile, rev, action, logname='edit-log', comment=u'', author=u"Scripting Subsystem"):
00051     glog = editlog.EditLog(self.request, uid_override=author)
00052     pagelog = Page(self.request, pagename).getPagePath(logname, use_underlay=0, isfile=1)
00053     llog = editlog.EditLog(self.request, filename=pagelog,
00054                                uid_override=author)
00055     mtime_usecs = wikiutil.timestamp2version(os.path.getmtime(pagefile))
00056     host = '::1'
00057     extra = u''
00058     glog.add(self.request, mtime_usecs, rev, action, pagename, host, comment)
00059     llog.add(self.request, mtime_usecs, rev, action, pagename, host, extra, comment)
00060     event_logfile(self, pagename, pagefile)
00061 
00062 # Parsing and (un)quoting for script files
00063 def packLine(items, separator="|"):
00064     """ Packs a list of items into a string that is separated by `separator`. """
00065     return '|'.join([item.replace('\\', '\\\\').replace(separator, '\\' + separator) for item in items])
00066 
00067 def unpackLine(string, separator="|"):
00068     """ Unpacks a string that was packed by packLine. """
00069     result = []
00070     token = None
00071     escaped = False
00072     for char in string:
00073         if token is None:
00074             token = ""
00075         if escaped and char in ('\\', separator):
00076             token += char
00077             escaped = False
00078             continue
00079         escaped = (char == '\\')
00080         if escaped:
00081             continue
00082         if char == separator:
00083             result.append(token)
00084             token = ""
00085         else:
00086             token += char
00087     if token is not None:
00088         result.append(token)
00089     return result
00090 
00091 def str2boolean(string):
00092     """
00093     Converts the parameter to a boolean value by recognising different
00094     truth literals.
00095     """
00096     return (string.lower() in ('yes', 'true', '1'))
00097 
00098 class ScriptEngine:
00099     """
00100     The script engine supplies the needed commands to execute the installation
00101     script.
00102     """
00103 
00104     def _extractToFile(self, source, target):
00105         """ Extracts source and writes the contents into target. """
00106         # TODO, add file dates
00107         target_file = open(target, "wb")
00108         target_file.write(self.extract_file(source))
00109         target_file.close()
00110 
00111     def __init__(self):
00112         self.themename = None
00113         self.ignoreExceptions = False
00114         self.goto = 0
00115 
00116         #Satisfy pylint
00117         self.msg = getattr(self, "msg", "")
00118         self.request = getattr(self, "request", None)
00119 
00120     def do_addattachment(self, zipname, filename, pagename, author=u"Scripting Subsystem", comment=u""):
00121         """
00122         Installs an attachment
00123 
00124         @param pagename: Page where the file is attached. Or in 2.0, the file itself.
00125         @param zipname: Filename of the attachment from the zip file
00126         @param filename: Filename of the attachment (just applicable for MoinMoin < 2.0)
00127         """
00128         if self.request.user.may.write(pagename):
00129             _ = self.request.getText
00130 
00131             attachments = Page(self.request, pagename).getPagePath("attachments", check_create=1)
00132             filename = wikiutil.taintfilename(filename)
00133             zipname = wikiutil.taintfilename(zipname)
00134             target = os.path.join(attachments, filename)
00135             page = PageEditor(self.request, pagename, do_editor_backup=0, uid_override=author)
00136             rev = page.current_rev()
00137             path = page.getPagePath(check_create=0)
00138             if not os.path.exists(target):
00139                 self._extractToFile(zipname, target)
00140                 if os.path.exists(target):
00141                     os.chmod(target, config.umask )
00142                     action = 'ATTNEW'
00143                     edit_logfile_append(self, pagename, path, rev, action, logname='edit-log',
00144                                        comment=u'%(filename)s' % {"filename": filename}, author=author)
00145                 self.msg += u"%(filename)s attached \n" % {"filename": filename}
00146             else:
00147                 self.msg += u"%(filename)s not attached \n" % {"filename": filename}
00148         else:
00149             self.msg += u"action add attachment: not enough rights - nothing done \n"
00150 
00151     def do_delattachment(self, filename, pagename, author=u"Scripting Subsystem", comment=u""):
00152         """
00153         Removes an attachment
00154 
00155         @param pagename: Page where the file is attached. Or in 2.0, the file itself.
00156         @param filename: Filename of the attachment (just applicable for MoinMoin < 2.0)
00157         """
00158         if self.request.user.may.write(pagename):
00159             _ = self.request.getText
00160 
00161             attachments = Page(self.request, pagename).getPagePath("attachments", check_create=1)
00162             filename = wikiutil.taintfilename(filename)
00163             target = os.path.join(attachments, filename)
00164             page = PageEditor(self.request, pagename, do_editor_backup=0, uid_override=author)
00165             rev = page.current_rev()
00166             path = page.getPagePath(check_create=0)
00167             if os.path.exists(target):
00168                 os.remove(target)
00169                 action = 'ATTDEL'
00170                 edit_logfile_append(self, pagename, path, rev, action, logname='edit-log',
00171                                     comment=u'%(filename)s' % {"filename": filename}, author=author)
00172                 self.msg += u"%(filename)s removed \n" % {"filename": filename}
00173             else:
00174                 self.msg += u"%(filename)s does not exist \n" % {"filename": filename}
00175         else:
00176             self.msg += u"action delete attachment: not enough rights - nothing done \n"
00177 
00178     def do_print(self, *param):
00179         """ Prints the parameters into output of the script. """
00180         self.msg += '; '.join(param) + "\n"
00181 
00182     def do_exit(self):
00183         """ Exits the script. """
00184         raise ScriptExit
00185 
00186     def do_ignoreexceptions(self, boolean):
00187         """ Sets the ignore exceptions setting. If exceptions are ignored, the
00188         script does not stop if one is encountered. """
00189         self.ignoreExceptions = str2boolean(boolean)
00190 
00191     def do_ensureversion(self, version, lines=0):
00192         """
00193         Ensures that the version of MoinMoin is greater or equal than
00194         version. If lines is unspecified, the script aborts. Otherwise,
00195         the next lines (amount specified by lines) are not executed.
00196 
00197         @param version: required version of MoinMoin (e.g. "1.3.4")
00198         @param lines: lines to ignore
00199         """
00200         _ = self.request.getText
00201 
00202         from MoinMoin.version import release
00203         version_int = [int(x) for x in version.split(".")]
00204         # use a regex here to get only the numbers of the release string (e.g. ignore betaX)
00205         release = re.compile('\d+').findall(release)[0:3]
00206         release = [int(x) for x in release]
00207         if version_int > release:
00208             if lines > 0:
00209                 self.goto = lines
00210             else:
00211                 raise RuntimeScriptException(_("The package needs a newer version"
00212                                                " of MoinMoin (at least %s).") %
00213                                              version)
00214 
00215     def do_setthemename(self, themename):
00216         """ Sets the name of the theme which will be altered next. """
00217         self.themename = wikiutil.taintfilename(str(themename))
00218 
00219     def do_copythemefile(self, filename, ftype, target):
00220         """ Copies a theme-related file (CSS, PNG, etc.) into a directory of the
00221         current theme.
00222 
00223         @param filename: name of the file in this package
00224         @param ftype:   the subdirectory of the theme directory, e.g. "css"
00225         @param target: filename, e.g. "screen.css"
00226         """
00227         _ = self.request.getText
00228         if self.themename is None:
00229             raise RuntimeScriptException(_("The theme name is not set."))
00230 
00231         from MoinMoin.web.static import STATIC_FILES_PATH as htdocs_dir
00232         if not os.access(htdocs_dir, os.W_OK):
00233             raise RuntimeScriptException(_("Theme files not installed! Write rights missing for %s.") % htdocs_dir)
00234 
00235         theme_file = os.path.join(htdocs_dir, self.themename,
00236                                   wikiutil.taintfilename(ftype),
00237                                   wikiutil.taintfilename(target))
00238         theme_dir = os.path.dirname(theme_file)
00239         if not os.path.exists(theme_dir):
00240             os.makedirs(theme_dir)
00241         self._extractToFile(filename, theme_file)
00242 
00243     def do_installplugin(self, filename, visibility, ptype, target):
00244         """
00245         Installs a python code file into the appropriate directory.
00246 
00247         @param filename: name of the file in this package
00248         @param visibility: 'local' will copy it into the plugin folder of the
00249             current wiki. 'global' will use the folder of the MoinMoin python
00250             package.
00251         @param ptype: the type of the plugin, e.g. "parser"
00252         @param target: the filename of the plugin, e.g. wiki.py
00253         """
00254         visibility = visibility.lower()
00255         ptype = wikiutil.taintfilename(ptype.lower())
00256 
00257         if visibility == 'global':
00258             basedir = os.path.dirname(__import__("MoinMoin").__file__)
00259         elif visibility == 'local':
00260             basedir = self.request.cfg.plugin_dir
00261 
00262         target = os.path.join(basedir, ptype, wikiutil.taintfilename(target))
00263 
00264         self._extractToFile(filename, target)
00265         wikiutil._wiki_plugins = {}
00266 
00267     def do_installpackage(self, pagename, filename):
00268         """
00269         Installs a package.
00270 
00271         @param pagename: Page where the file is attached. Or in 2.0, the file itself.
00272         @param filename: Filename of the attachment (just applicable for MoinMoin < 2.0)
00273         """
00274         _ = self.request.getText
00275 
00276         attachments = Page(self.request, pagename).getPagePath("attachments", check_create=0)
00277         package = ZipPackage(self.request, os.path.join(attachments, wikiutil.taintfilename(filename)))
00278 
00279         if package.isPackage():
00280             if not package.installPackage():
00281                 raise RuntimeScriptException(_("Installation of '%(filename)s' failed.") % {
00282                     'filename': filename} + "\n" + package.msg)
00283         else:
00284             raise RuntimeScriptException(_('The file %s is not a MoinMoin package file.') % filename)
00285 
00286         self.msg += package.msg
00287 
00288     def do_addrevision(self, filename, pagename, author=u"Scripting Subsystem", comment=u"", trivial=u"No"):
00289         """ Adds a revision to a page.
00290 
00291         @param filename: name of the file in this package
00292         @param pagename: name of the target page
00293         @param author:   user name of the editor (optional)
00294         @param comment:  comment related to this revision (optional)
00295         @param trivial:  boolean, if it is a trivial edit
00296         """
00297         _ = self.request.getText
00298         trivial = str2boolean(trivial)
00299         if self.request.user.may.write(pagename):
00300             page = PageEditor(self.request, pagename, do_editor_backup=0)
00301             try:
00302                 page.saveText(self.extract_file(filename).decode("utf-8"), 0, trivial=trivial, comment=comment)
00303             except PageEditor.Unchanged:
00304                 pass
00305             else:
00306                 self.msg += u"%(pagename)s added \n" % {"pagename": pagename}
00307         else:
00308             self.msg += u"action add revision: not enough rights - nothing done \n"
00309 
00310     def do_renamepage(self, pagename, newpagename, author=u"Scripting Subsystem", comment=u"Renamed by the scripting subsystem."):
00311         """ Renames a page.
00312 
00313         @param pagename: name of the target page
00314         @param newpagename: name of the new page
00315         @param author:   user name of the editor (optional)
00316         @param comment:  comment related to this revision (optional)
00317         """
00318         if self.request.user.may.write(pagename):
00319             _ = self.request.getText
00320             page = PageEditor(self.request, pagename, do_editor_backup=0, uid_override=author)
00321             if not page.exists():
00322                 raise RuntimeScriptException(_("The page %s does not exist.") % pagename)
00323             newpage = PageEditor(self.request, newpagename)
00324             page.renamePage(newpage.page_name, comment=u"Renamed from '%s'" % (pagename))
00325             self.msg += u'%(pagename)s renamed to %(newpagename)s\n' % {
00326                             "pagename": pagename,
00327                             "newpagename": newpagename}
00328         else:
00329             self.msg += u"action rename page: not enough rights - nothing done \n"
00330 
00331     def do_deletepage(self, pagename, comment="Deleted by the scripting subsystem."):
00332         """ Marks a page as deleted (like the DeletePage action).
00333 
00334         @param pagename: page to delete
00335         @param comment:  the related comment (optional)
00336         """
00337         if self.request.user.may.write(pagename):
00338             _ = self.request.getText
00339             page = PageEditor(self.request, pagename, do_editor_backup=0)
00340             if not page.exists():
00341                 raise RuntimeScriptException(_("The page %s does not exist.") % pagename)
00342             page.deletePage(comment)
00343         else:
00344             self.msg += u"action delete page: not enough rights - nothing done \n"
00345 
00346     def do_replaceunderlayattachment(self, zipname, filename, pagename, author=u"Scripting Subsystem", comment=u""):
00347         """
00348         overwrite underlay attachments
00349 
00350         @param pagename: Page where the file is attached. Or in 2.0, the file itself.
00351         @param zipname: Filename of the attachment from the zip file
00352         @param filename: Filename of the attachment (just applicable for MoinMoin < 2.0)
00353         """
00354         if self.request.user.may.write(pagename):
00355             _ = self.request.getText
00356             filename = wikiutil.taintfilename(filename)
00357             zipname = wikiutil.taintfilename(zipname)
00358             page = PageEditor(self.request, pagename, do_editor_backup=0, uid_override=author)
00359             pagedir = page.getPagePath(use_underlay=1, check_create=1)
00360             attachments = os.path.join(pagedir, 'attachments')
00361             if not os.path.exists(attachments):
00362                 os.mkdir(attachments)
00363             target = os.path.join(attachments, filename)
00364             self._extractToFile(zipname, target)
00365             if os.path.exists(target):
00366                 os.chmod(target, config.umask )
00367         else:
00368             self.msg += u"action replace underlay attachment: not enough rights - nothing done \n"
00369 
00370     def do_replaceunderlay(self, filename, pagename):
00371         """
00372         Overwrites underlay pages. Implementational detail: This needs to be
00373         kept in sync with the page class.
00374 
00375         @param filename: name of the file in the package
00376         @param pagename: page to be overwritten
00377         """
00378         page = Page(self.request, pagename)
00379 
00380         pagedir = page.getPagePath(use_underlay=1, check_create=1)
00381 
00382         revdir = os.path.join(pagedir, 'revisions')
00383         cfn = os.path.join(pagedir, 'current')
00384 
00385         revstr = '%08d' % 1
00386         if not os.path.exists(revdir):
00387             os.mkdir(revdir)
00388 
00389         currentf = open(cfn, 'w')
00390         currentf.write(revstr + "\n")
00391         currentf.close()
00392 
00393         pagefile = os.path.join(revdir, revstr)
00394         self._extractToFile(filename, pagefile)
00395         # Clear caches
00396         # TODO Code from MoinMoin/script/maint/cleancache.py may be used
00397 
00398     def runScript(self, commands):
00399         """ Runs the commands.
00400 
00401         @param commands: list of strings which contain a command each
00402         @return True on success
00403         """
00404         _ = self.request.getText
00405 
00406         headerline = unpackLine(commands[0])
00407 
00408         if headerline[0].lower() != "MoinMoinPackage".lower():
00409             raise PackageException(_("Invalid package file header."))
00410 
00411         self.revision = int(headerline[1])
00412         if self.revision > MAX_VERSION:
00413             raise PackageException(_("Package file format unsupported."))
00414 
00415         lineno = 1
00416         success = True
00417 
00418         for line in commands[1:]:
00419             lineno += 1
00420             if self.goto > 0:
00421                 self.goto -= 1
00422                 continue
00423 
00424             if line.startswith("#") or len(line) == 0:
00425                 continue
00426             elements = unpackLine(line)
00427             fnname = elements[0].strip().lower()
00428             if fnname == '':
00429                 continue
00430             try:
00431                 fn = getattr(self, "do_" + fnname)
00432             except AttributeError:
00433                 self.msg += u"Exception RuntimeScriptException: %s\n" % (
00434                         _("Unknown function %(func)s in line %(lineno)i.") %
00435                         {'func': elements[0], 'lineno': lineno}, )
00436                 success = False
00437                 break
00438 
00439             try:
00440                 fn(*elements[1:])
00441             except ScriptExit:
00442                 break
00443             except TypeError, e:
00444                 self.msg += u"Exception %s (line %i): %s\n" % (e.__class__.__name__, lineno, unicode(e))
00445                 success = False
00446                 break
00447             except RuntimeScriptException, e:
00448                 if not self.ignoreExceptions:
00449                     self.msg += u"Exception %s (line %i): %s\n" % (e.__class__.__name__, lineno, unicode(e))
00450                     success = False
00451                     break
00452 
00453         return success
00454 
00455 class Package:
00456     """ A package consists of a bunch of files which can be installed. """
00457     def __init__(self, request):
00458         self.request = request
00459         self.msg = ""
00460 
00461     def installPackage(self):
00462         """ Opens the package and executes the script. """
00463 
00464         _ = self.request.getText
00465 
00466         if not self.isPackage():
00467             raise PackageException(_("The file %s was not found in the package.") % MOIN_PACKAGE_FILE)
00468 
00469         commands = self.getScript().splitlines()
00470 
00471         return self.runScript(commands)
00472 
00473     def getScript(self):
00474         """ Returns the script. """
00475         return self.extract_file(MOIN_PACKAGE_FILE).decode("utf-8").replace(u"\ufeff", "")
00476 
00477     def extract_file(self, filename):
00478         """ Returns the contents of a file in the package. """
00479         raise NotImplementedError
00480 
00481     def filelist(self):
00482         """ Returns a list of all files. """
00483         raise NotImplementedError
00484 
00485     def isPackage(self):
00486         """ Returns true if this package is recognised. """
00487         raise NotImplementedError
00488 
00489 class ZipPackage(Package, ScriptEngine):
00490     """ A package that reads its files from a .zip file. """
00491     def __init__(self, request, filename):
00492         """ Initialise the package.
00493 
00494         @param request: RequestBase instance
00495         @param filename: filename of the .zip file
00496         """
00497 
00498         Package.__init__(self, request)
00499         ScriptEngine.__init__(self)
00500         self.filename = filename
00501 
00502         self._isZipfile = zipfile.is_zipfile(filename)
00503         if self._isZipfile:
00504             self.zipfile = zipfile.ZipFile(filename)
00505         # self.zipfile.getinfo(name)
00506 
00507     def extract_file(self, filename):
00508         """ Returns the contents of a file in the package. """
00509         _ = self.request.getText
00510         try:
00511             return self.zipfile.read(filename.encode("cp437"))
00512         except KeyError:
00513             raise RuntimeScriptException(_(
00514                 "The file %s was not found in the package.") % filename)
00515 
00516     def filelist(self):
00517         """ Returns a list of all files. """
00518         return self.zipfile.namelist()
00519 
00520     def isPackage(self):
00521         """ Returns true if this package is recognised. """
00522         return self._isZipfile and MOIN_PACKAGE_FILE in self.zipfile.namelist()
00523 
00524 def main():
00525     args = sys.argv
00526     if len(args)-1 not in (2, 3) or args[1] not in ('l', 'i'):
00527         print >> sys.stderr, """MoinMoin Package Installer v%(version)i
00528 
00529 %(myname)s action packagefile [request URL]
00530 
00531 action      - Either "l" for listing the script or "i" for installing.
00532 packagefile - The path to the file containing the MoinMoin installer package
00533 request URL - Just needed if you are running a wiki farm, used to differentiate
00534               the correct wiki.
00535 
00536 Example:
00537 
00538 %(myname)s i ../package.zip
00539 
00540 """ % {"version": MAX_VERSION, "myname": os.path.basename(args[0])}
00541         raise SystemExit
00542 
00543     packagefile = args[2]
00544     if len(args) > 3:
00545         request_url = args[3]
00546     else:
00547         request_url = None
00548 
00549     # Setup MoinMoin environment
00550     from MoinMoin.web.contexts import ScriptContext
00551     request = ScriptContext(url=request_url)
00552 
00553     package = ZipPackage(request, packagefile)
00554     if not package.isPackage():
00555         print "The specified file %s is not a package." % packagefile
00556         raise SystemExit
00557 
00558     if args[1] == 'l':
00559         print package.getScript()
00560     elif args[1] == 'i':
00561         if package.installPackage():
00562             print "Installation was successful!"
00563         else:
00564             print "Installation failed."
00565         if package.msg:
00566             print package.msg
00567 
00568 if __name__ == '__main__':
00569     main()