Back to index

moin  1.9.0~rc2
__init__.py
Go to the documentation of this file.
00001 # -*- coding: iso-8859-1 -*-
00002 """
00003     MoinMoin - Action Implementation
00004 
00005     Actions are triggered by the user clicking on special links on the page
00006     (e.g. the "edit" link). The name of the action is passed in the "action"
00007     CGI parameter.
00008 
00009     The sub-package "MoinMoin.action" contains external actions, you can
00010     place your own extensions there (similar to extension macros). User
00011     actions that start with a capital letter will be displayed in a list
00012     at the bottom of each page.
00013 
00014     User actions starting with a lowercase letter can be used to work
00015     together with a user macro; those actions a likely to work only if
00016     invoked BY that macro, and are thus hidden from the user interface.
00017 
00018     Additionally to the usual stuff, we provide an ActionBase class here with
00019     some of the usual base functionality for an action, like checking
00020     actions_excluded, making and checking tickets, rendering some form,
00021     displaying errors and doing stuff after an action. Also utility functions
00022     regarding actions are located here.
00023 
00024     @copyright: 2000-2004 Juergen Hermann <jh@web.de>,
00025                 2006 MoinMoin:ThomasWaldmann
00026                 2008 MoinMoin:FlorianKrupicka
00027     @license: GNU GPL, see COPYING for details.
00028 """
00029 
00030 import re
00031 
00032 from MoinMoin.util import pysupport
00033 from MoinMoin import config, wikiutil
00034 from MoinMoin.Page import Page
00035 from MoinMoin.support.python_compatibility import set
00036 
00037 # create a list of extension actions from the package directory
00038 modules = pysupport.getPackageModules(__file__)
00039 
00040 # builtin-stuff (see do_<name> below):
00041 names = ['show', 'recall', 'raw', 'format', 'content', 'print', 'refresh', 'goto', ]
00042 
00043 class ActionBase:
00044     """ action base class with some generic stuff to inherit
00045 
00046     Note: the action name is the class name of the derived class
00047     """
00048     def __init__(self, pagename, request):
00049         self.request = request
00050         self.form = request.form
00051         self.cfg = request.cfg
00052         self._ = _ = request.getText
00053         self.pagename = pagename
00054         self.actionname = self.__class__.__name__
00055         self.use_ticket = False # set this to True if you want to use a ticket
00056         self.user_html = '''Just checking.''' # html fragment for make_form
00057         self.form_cancel = "cancel" # form key for cancelling action
00058         self.form_cancel_label = _("Cancel") # label for the cancel button
00059         self.form_trigger = "doit" # form key for triggering action (override with e.g. 'rename')
00060         self.form_trigger_label = _("Do it.") # label for the trigger button
00061         self.page = Page(request, pagename)
00062         self.error = ''
00063         self.method = 'POST'
00064         self.enctype = 'multipart/form-data'
00065 
00066     # CHECKS -----------------------------------------------------------------
00067     def is_excluded(self):
00068         """ Return True if action is excluded """
00069         return self.actionname in self.cfg.actions_excluded
00070 
00071     def is_allowed(self):
00072         """
00073         Return True if action is allowed (by ACL), or
00074         return a tuple (allowed, message) to show a
00075         message other than the default.
00076         """
00077         return True
00078 
00079     def check_condition(self):
00080         """ Check if some other condition is not allowing us to do that action,
00081             return error msg or None if there is no problem.
00082 
00083             You can use this to e.g. check if a page exists.
00084         """
00085         return None
00086 
00087     def ticket_ok(self):
00088         """ Return True if we check for tickets and there is some valid ticket
00089             in the form data or if we don't check for tickets at all.
00090             Use this to make sure someone really used the web interface.
00091         """
00092         if not self.use_ticket:
00093             return True
00094         # Require a valid ticket. Make outside attacks harder by
00095         # requiring two full HTTP transactions
00096         ticket = self.form.get('ticket', '')
00097         return wikiutil.checkTicket(self.request, ticket)
00098 
00099     # UI ---------------------------------------------------------------------
00100     def get_form_html(self, buttons_html):
00101         """ Override this to assemble the inner part of the form,
00102             for convenience we give him some pre-assembled html for the buttons.
00103         """
00104         _ = self._
00105         f = self.request.formatter
00106         prompt = _("Execute action %(actionname)s?") % {'actionname': self.actionname}
00107         return f.paragraph(1) + f.text(prompt) + f.paragraph(0) + f.rawHTML(buttons_html)
00108 
00109     def make_buttons(self):
00110         """ return a list of form buttons for the action form """
00111         return [
00112             (self.form_trigger, self.form_trigger_label),
00113             (self.form_cancel, self.form_cancel_label),
00114         ]
00115 
00116     def make_form(self):
00117         """ Make some form html for later display.
00118 
00119         The form might contain an error that happened when trying to do the action.
00120         """
00121         from MoinMoin.widget.dialog import Dialog
00122         _ = self._
00123 
00124         if self.error:
00125             error_html = u'<p class="error">%s</p>\n' % self.error
00126         else:
00127             error_html = ''
00128 
00129         buttons = self.make_buttons()
00130         buttons_html = []
00131         for button in buttons:
00132             buttons_html.append('<input type="submit" name="%s" value="%s">' % button)
00133         buttons_html = "".join(buttons_html)
00134 
00135         if self.use_ticket:
00136             ticket_html = '<input type="hidden" name="ticket" value="%s">' % wikiutil.createTicket(self.request)
00137         else:
00138             ticket_html = ''
00139 
00140         d = {
00141             'method': self.method,
00142             'url': self.request.href(self.pagename),
00143             'enctype': self.enctype,
00144             'error_html': error_html,
00145             'actionname': self.actionname,
00146             'ticket_html': ticket_html,
00147             'user_html': self.get_form_html(buttons_html),
00148         }
00149 
00150         form_html = '''
00151 %(error_html)s
00152 <form action="%(url)s" method="%(method)s" enctype="%(enctype)s">
00153 <div>
00154 <input type="hidden" name="action" value="%(actionname)s">
00155 %(ticket_html)s
00156 %(user_html)s
00157 </div>
00158 </form>''' % d
00159 
00160         return Dialog(self.request, content=form_html)
00161 
00162     def render_msg(self, msg, msgtype):
00163         """ Called to display some message (can also be the action form) """
00164         self.request.theme.add_msg(msg, msgtype)
00165         do_show(self.pagename, self.request)
00166 
00167     def render_success(self, msg, msgtype):
00168         """ Called to display some message when the action succeeded """
00169         self.request.theme.add_msg(msg, msgtype)
00170         do_show(self.pagename, self.request)
00171 
00172     def render_cancel(self):
00173         """ Called when user has hit the cancel button """
00174         do_show(self.pagename, self.request)
00175 
00176     def render(self):
00177         """ Render action - this is the main function called by action's
00178             execute() function.
00179 
00180             We usually render a form here, check for posted forms, etc.
00181         """
00182         _ = self._
00183         form = self.form
00184 
00185         if self.form_cancel in form:
00186             self.render_cancel()
00187             return
00188 
00189         # Validate allowance, user rights and other conditions.
00190         error = None
00191         if self.is_excluded():
00192             error = _('Action %(actionname)s is excluded in this wiki!') % {'actionname': self.actionname }
00193         else:
00194             allowed = self.is_allowed()
00195             if isinstance(allowed, tuple):
00196                 allowed, msg = allowed
00197             else:
00198                 msg = _('You are not allowed to use action %(actionname)s on this page!') % {'actionname': self.actionname }
00199             if not allowed:
00200                 error = msg
00201         if error is None:
00202             error = self.check_condition()
00203         if error:
00204             self.render_msg(error, "error")
00205         elif self.form_trigger in form: # user hit the trigger button
00206             if self.ticket_ok():
00207                 success, self.error = self.do_action()
00208             else:
00209                 success = False
00210                 self.error = _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': self.actionname }
00211             self.do_action_finish(success)
00212         else:
00213             # Return a new form
00214             self.render_msg(self.make_form(), "dialog")
00215 
00216     # Executing the action ---------------------------------------------------
00217     def do_action(self):
00218         """ Do the action and either return error msg or None, if there was no error. """
00219         return None
00220 
00221     # AFTER the action -------------------------------------------------------
00222     def do_action_finish(self, success):
00223         """ Override this to handle success or failure (with error in self.error) of your action.
00224         """
00225         if success:
00226             self.render_success(self.error, "info")
00227         else:
00228             self.render_msg(self.make_form(), "dialog") # display the form again
00229 
00230 
00231 # Builtin Actions ------------------------------------------------------------
00232 
00233 MIMETYPE_CRE = re.compile('[a-zA-Z0-9.+\-]{1,100}/[a-zA-Z0-9.+\-]{1,100}')
00234 
00235 def do_raw(pagename, request):
00236     """ send raw content of a page (e.g. wiki markup) """
00237     if not request.user.may.read(pagename):
00238         Page(request, pagename).send_page()
00239     else:
00240         rev = request.rev or 0
00241         mimetype = request.values.get('mimetype', None)
00242         if mimetype and not MIMETYPE_CRE.match(mimetype):
00243             mimetype = None
00244         Page(request, pagename, rev=rev).send_raw(mimetype=mimetype)
00245 
00246 def do_show(pagename, request, content_only=0, count_hit=1, cacheable=1, print_mode=0, mimetype=u'text/html'):
00247     """ show a page, either current revision or the revision given by "rev=" value.
00248         if count_hit is non-zero, we count the request for statistics.
00249     """
00250     # We must check if the current page has different ACLs.
00251     if not request.user.may.read(pagename):
00252         Page(request, pagename).send_page()
00253     else:
00254         mimetype = request.values.get('mimetype', mimetype)
00255         rev = request.rev or 0
00256         if rev == 0:
00257             request.cacheable = cacheable
00258         Page(request, pagename, rev=rev, formatter=mimetype).send_page(
00259             count_hit=count_hit,
00260             print_mode=print_mode,
00261             content_only=content_only,
00262         )
00263 
00264 def do_format(pagename, request):
00265     """ send a page using a specific formatter given by "mimetype=" value.
00266         Since 5.5.2006 this functionality is also done by do_show, but do_format
00267         has a default of text/plain when no format is given.
00268         It also does not count in statistics and also does not set the cacheable flag.
00269         DEPRECATED: remove this action when we don't need it any more for compatibility.
00270     """
00271     do_show(pagename, request, count_hit=0, cacheable=0, mimetype=u'text/plain')
00272 
00273 def do_content(pagename, request):
00274     """ same as do_show, but we only show the content """
00275     # XXX temporary fix to make it work until Page.send_page gets refactored
00276     request.mimetype = 'text/html'
00277     request.status_code = 200
00278     do_show(pagename, request, count_hit=0, content_only=1)
00279 
00280 def do_print(pagename, request):
00281     """ same as do_show, but with print_mode set """
00282     do_show(pagename, request, print_mode=1)
00283 
00284 def do_recall(pagename, request):
00285     """ same as do_show, but never caches and never counts hits """
00286     do_show(pagename, request, count_hit=0, cacheable=0)
00287 
00288 def do_refresh(pagename, request):
00289     """ Handle refresh action """
00290     # Without arguments, refresh action will refresh the page text_html cache.
00291     arena = request.values.get('arena', 'Page.py')
00292     if arena == 'Page.py':
00293         arena = Page(request, pagename)
00294     key = request.values.get('key', 'text_html')
00295 
00296     # Remove cache entry (if exists), and send the page
00297     from MoinMoin import caching
00298     caching.CacheEntry(request, arena, key, scope='item').remove()
00299     caching.CacheEntry(request, arena, "pagelinks", scope='item').remove()
00300     do_show(pagename, request)
00301 
00302 def do_goto(pagename, request):
00303     """ redirect to another page """
00304     target = request.values.get('target', '')
00305     request.http_redirect(Page(request, target).url(request))
00306 
00307 # Dispatching ----------------------------------------------------------------
00308 def get_names(config):
00309     """ Get a list of known actions.
00310 
00311     @param config: a config object
00312     @rtype: set
00313     @return: set of known actions
00314     """
00315     if not hasattr(config.cache, 'action_names'):
00316         actions = names[:]
00317         actions.extend(wikiutil.getPlugins('action', config))
00318         actions = set([action for action in actions
00319                       if not action in config.actions_excluded])
00320         config.cache.action_names = actions # remember it
00321     return config.cache.action_names
00322 
00323 def getHandler(request, action, identifier="execute"):
00324     """ return a handler function for a given action or None.
00325 
00326     TODO: remove request dependency
00327     """
00328     cfg = request.cfg
00329     # check for excluded actions
00330     if action in cfg.actions_excluded:
00331         return None
00332 
00333     try:
00334         handler = wikiutil.importPlugin(cfg, "action", action, identifier)
00335     except wikiutil.PluginMissingError:
00336         handler = globals().get('do_' + action)
00337 
00338     return handler
00339 
00340 def get_available_actions(config, page, user):
00341         """ Get a list of actions available on a particular page
00342         for a particular user.
00343 
00344         The set does not contain actions that starts with lower case.
00345         Themes use this set to display the actions to the user.
00346 
00347         @param config: a config object (for the per-wiki actions)
00348         @param page: the page to which the actions should apply
00349         @param user: the user which wants to apply an action
00350         @rtype: set
00351         @return: set of avaiable actions
00352         """
00353         if not user.may.read(page.page_name):
00354             return []
00355 
00356 
00357         actions = get_names(config)
00358 
00359         # Filter non ui actions (starts with lower case letter)
00360         actions = [action for action in actions if not action[0].islower()]
00361 
00362         # Filter actions by page type, acl and user state
00363         excluded = []
00364         if (page.isUnderlayPage() and not page.isStandardPage()) or \
00365                 not user.may.write(page.page_name) or \
00366                 not user.may.delete(page.page_name):
00367                 # Prevent modification of underlay only pages, or pages
00368                 # the user can't write and can't delete
00369                 excluded = [u'RenamePage', u'DeletePage', ] # AttachFile must NOT be here!
00370         return set([action for action in actions if not action in excluded])
00371 
00372