1# -*- coding: iso-8859-1 -*-
2"""
3    MoinMoin - Action Implementation
4
5    Actions are triggered by the user clicking on special links on the page
6    (e.g. the "edit" link). The name of the action is passed in the "action"
7    CGI parameter.
8
9    The sub-package "MoinMoin.action" contains external actions, you can
10    place your own extensions there (similar to extension macros). User
11    actions that start with a capital letter will be displayed in a list
12    at the bottom of each page.
13
14    User actions starting with a lowercase letter can be used to work
15    together with a user macro; those actions a likely to work only if
16    invoked BY that macro, and are thus hidden from the user interface.
17
18    Additionally to the usual stuff, we provide an ActionBase class here with
19    some of the usual base functionality for an action, like checking
20    actions_excluded, making and checking tickets, rendering some form,
21    displaying errors and doing stuff after an action. Also utility functions
22    regarding actions are located here.
23
24    @copyright: 2000-2004 Juergen Hermann <jh@web.de>,
25                2006 MoinMoin:ThomasWaldmann
26                2008 MoinMoin:FlorianKrupicka
27    @license: GNU GPL, see COPYING for details.
28"""
29
30import re
31
32from MoinMoin.util import pysupport
33from MoinMoin import config, wikiutil
34from MoinMoin.Page import Page
35
36# create a list of extension actions from the package directory
37modules = pysupport.getPackageModules(__file__)
38
39# builtin-stuff (see do_<name> below):
40names = ['show', 'recall', 'raw', 'format', 'content', 'print', 'refresh', 'goto', ]
41
42class ActionBase:
43    """ action base class with some generic stuff to inherit
44
45    Note: the action name is the class name of the derived class
46    """
47    def __init__(self, pagename, request, only_form=False):
48        self.request = request
49        if only_form:
50            # use only form (POST) data, this was 1.9.0 .. 1.9.2 default,
51            # but different from 1.8 behaviour:
52            self.form = request.form
53        else:
54            # use query string values mixed with post form data - this gives
55            # better compatibility to moin 1.8 behaviour
56            self.form = request.values
57        self.cfg = request.cfg
58        self._ = _ = request.getText
59        self.pagename = pagename
60        self.actionname = self.__class__.__name__
61        self.use_ticket = False # set this to True if you want to use a ticket
62        self.user_html = '''Just checking.''' # html fragment for make_form
63        self.form_cancel = "cancel" # form key for cancelling action
64        self.form_cancel_label = _("Cancel") # label for the cancel button
65        self.form_trigger = "doit" # form key for triggering action (override with e.g. 'rename')
66        self.form_trigger_label = _("Do it.") # label for the trigger button
67        self.page = Page(request, pagename)
68        self.error = ''
69        self.method = 'POST'
70        self.enctype = 'multipart/form-data'
71
72    # CHECKS -----------------------------------------------------------------
73    def is_excluded(self):
74        """ Return True if action is excluded """
75        return self.actionname in self.cfg.actions_excluded
76
77    def is_allowed(self):
78        """
79        Return True if action is allowed (by ACL), or
80        return a tuple (allowed, message) to show a
81        message other than the default.
82        """
83        return True
84
85    def check_condition(self):
86        """ Check if some other condition is not allowing us to do that action,
87            return error msg or None if there is no problem.
88
89            You can use this to e.g. check if a page exists.
90        """
91        return None
92
93    def ticket_ok(self):
94        """ Return True if we check for tickets and there is some valid ticket
95            in the form data or if we don't check for tickets at all.
96            Use this to make sure someone really used the web interface.
97        """
98        if not self.use_ticket:
99            return True
100        # Require a valid ticket. Make outside attacks harder by
101        # requiring two full HTTP transactions
102        ticket = self.form.get('ticket', '')
103        return wikiutil.checkTicket(self.request, ticket)
104
105    # UI ---------------------------------------------------------------------
106    def get_form_html(self, buttons_html):
107        """ Override this to assemble the inner part of the form,
108            for convenience we give him some pre-assembled html for the buttons.
109        """
110        _ = self._
111        f = self.request.formatter
112        prompt = _("Execute action %(actionname)s?") % {'actionname': self.actionname}
113        return f.paragraph(1) + f.text(prompt) + f.paragraph(0) + f.rawHTML(buttons_html)
114
115    def make_buttons(self):
116        """ return a list of form buttons for the action form """
117        return [
118            (self.form_trigger, self.form_trigger_label),
119            (self.form_cancel, self.form_cancel_label),
120        ]
121
122    def make_form(self):
123        """ Make some form html for later display.
124
125        The form might contain an error that happened when trying to do the action.
126        """
127        from MoinMoin.widget.dialog import Dialog
128        _ = self._
129
130        if self.error:
131            error_html = u'<p class="error">%s</p>\n' % self.error
132        else:
133            error_html = ''
134
135        buttons = self.make_buttons()
136        buttons_html = []
137        for button in buttons:
138            buttons_html.append('<input type="submit" name="%s" value="%s">' % button)
139        buttons_html = "".join(buttons_html)
140
141        if self.use_ticket:
142            ticket_html = '<input type="hidden" name="ticket" value="%s">' % wikiutil.createTicket(self.request)
143        else:
144            ticket_html = ''
145
146        d = {
147            'method': self.method,
148            'url': self.request.href(self.pagename),
149            'enctype': self.enctype,
150            'error_html': error_html,
151            'actionname': self.actionname,
152            'ticket_html': ticket_html,
153            'user_html': self.get_form_html(buttons_html),
154        }
155
156        form_html = '''
157%(error_html)s
158<form action="%(url)s" method="%(method)s" enctype="%(enctype)s">
159<div>
160<input type="hidden" name="action" value="%(actionname)s">
161%(ticket_html)s
162%(user_html)s
163</div>
164</form>''' % d
165
166        return Dialog(self.request, content=form_html)
167
168    def render_msg(self, msg, msgtype):
169        """ Called to display some message (can also be the action form) """
170        self.request.theme.add_msg(msg, msgtype)
171        do_show(self.pagename, self.request)
172
173    def render_success(self, msg, msgtype):
174        """ Called to display some message when the action succeeded """
175        self.request.theme.add_msg(msg, msgtype)
176        do_show(self.pagename, self.request)
177
178    def render_cancel(self):
179        """ Called when user has hit the cancel button """
180        do_show(self.pagename, self.request)
181
182    def render(self):
183        """ Render action - this is the main function called by action's
184            execute() function.
185
186            We usually render a form here, check for posted forms, etc.
187        """
188        _ = self._
189        form = self.form
190
191        if self.form_cancel in form:
192            self.render_cancel()
193            return
194
195        # Validate allowance, user rights and other conditions.
196        error = None
197        if self.is_excluded():
198            error = _('Action %(actionname)s is excluded in this wiki!') % {'actionname': self.actionname }
199        else:
200            allowed = self.is_allowed()
201            if isinstance(allowed, tuple):
202                allowed, msg = allowed
203            else:
204                msg = _('You are not allowed to use action %(actionname)s on this page!') % {'actionname': self.actionname }
205            if not allowed:
206                error = msg
207        if error is None:
208            error = self.check_condition()
209        if error:
210            self.render_msg(error, "error")
211        elif self.form_trigger in form: # user hit the trigger button
212            if self.ticket_ok():
213                success, self.error = self.do_action()
214            else:
215                success = False
216                self.error = _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': self.actionname }
217            self.do_action_finish(success)
218        else:
219            # Return a new form
220            self.render_msg(self.make_form(), "dialog")
221
222    # Executing the action ---------------------------------------------------
223    def do_action(self):
224        """ Do the action and either return error msg or None, if there was no error. """
225        return None
226
227    # AFTER the action -------------------------------------------------------
228    def do_action_finish(self, success):
229        """ Override this to handle success or failure (with error in self.error) of your action.
230        """
231        if success:
232            self.render_success(self.error, "info")
233        else:
234            self.render_msg(self.make_form(), "dialog") # display the form again
235
236
237# Builtin Actions ------------------------------------------------------------
238
239MIMETYPE_CRE = re.compile('[a-zA-Z0-9.+\-]{1,100}/[a-zA-Z0-9.+\-]{1,100}')
240
241def do_raw(pagename, request):
242    """ send raw content of a page (e.g. wiki markup) """
243    if not request.user.may.read(pagename):
244        Page(request, pagename).send_page()
245    else:
246        rev = request.rev or 0
247        mimetype = request.values.get('mimetype', None)
248        if mimetype and not MIMETYPE_CRE.match(mimetype):
249            mimetype = None
250        Page(request, pagename, rev=rev).send_raw(mimetype=mimetype)
251
252def do_show(pagename, request, content_only=0, count_hit=1, cacheable=1, print_mode=0, mimetype=u'text/html'):
253    """ show a page, either current revision or the revision given by "rev=" value.
254        if count_hit is non-zero, we count the request for statistics.
255    """
256    # We must check if the current page has different ACLs.
257    if not request.user.may.read(pagename):
258        Page(request, pagename).send_page()
259    else:
260        mimetype = request.values.get('mimetype', mimetype)
261        rev = request.rev or 0
262        if rev == 0:
263            request.cacheable = cacheable
264        Page(request, pagename, rev=rev, formatter=mimetype).send_page(
265            count_hit=count_hit,
266            print_mode=print_mode,
267            content_only=content_only,
268        )
269
270def do_format(pagename, request):
271    """ send a page using a specific formatter given by "mimetype=" value.
272        Since 5.5.2006 this functionality is also done by do_show, but do_format
273        has a default of text/plain when no format is given.
274        It also does not count in statistics and also does not set the cacheable flag.
275        DEPRECATED: remove this action when we don't need it any more for compatibility.
276    """
277    do_show(pagename, request, count_hit=0, cacheable=0, mimetype=u'text/plain')
278
279def do_content(pagename, request):
280    """ same as do_show, but we only show the content """
281    # XXX temporary fix to make it work until Page.send_page gets refactored
282    request.mimetype = 'text/html'
283    request.status_code = 200
284    do_show(pagename, request, count_hit=0, content_only=1)
285
286def do_print(pagename, request):
287    """ same as do_show, but with print_mode set """
288    do_show(pagename, request, print_mode=1)
289
290def do_recall(pagename, request):
291    """ same as do_show, but never caches and never counts hits """
292    do_show(pagename, request, count_hit=0, cacheable=0)
293
294def do_refresh(pagename, request):
295    """ Handle refresh action """
296    # Without arguments, refresh action will refresh the page text_html cache.
297    arena = request.values.get('arena', 'Page.py')
298    if arena == 'Page.py':
299        arena = Page(request, pagename)
300    key = request.values.get('key', 'text_html')
301
302    # Remove cache entry (if exists), and send the page
303    from MoinMoin import caching
304    caching.CacheEntry(request, arena, key, scope='item').remove()
305    caching.CacheEntry(request, arena, "pagelinks", scope='item').remove()
306    do_show(pagename, request)
307
308def do_goto(pagename, request):
309    """ redirect to another page """
310    target = request.values.get('target', '')
311    request.http_redirect(Page(request, target).url(request))
312
313# Dispatching ----------------------------------------------------------------
314def get_names(config):
315    """ Get a list of known actions.
316
317    @param config: a config object
318    @rtype: set
319    @return: set of known actions
320    """
321    if not hasattr(config.cache, 'action_names'):
322        actions = names[:]
323        actions.extend(wikiutil.getPlugins('action', config))
324        actions = set([action for action in actions
325                      if not action in config.actions_excluded])
326        config.cache.action_names = actions # remember it
327    return config.cache.action_names
328
329def getHandler(request, action, identifier="execute"):
330    """ return a handler function for a given action or None.
331
332    TODO: remove request dependency
333    """
334    cfg = request.cfg
335    # check for excluded actions
336    if action in cfg.actions_excluded:
337        return None
338
339    if action in cfg.actions_superuser and not request.user.isSuperUser():
340        return None
341
342    try:
343        handler = wikiutil.importPlugin(cfg, "action", action, identifier)
344    except wikiutil.PluginMissingError:
345        handler = globals().get('do_' + action)
346
347    return handler
348
349def get_available_actions(config, page, user):
350        """ Get a list of actions available on a particular page
351        for a particular user.
352
353        The set does not contain actions that starts with lower case.
354        Themes use this set to display the actions to the user.
355
356        @param config: a config object (for the per-wiki actions)
357        @param page: the page to which the actions should apply
358        @param user: the user which wants to apply an action
359        @rtype: set
360        @return: set of avaiable actions
361        """
362        if not user.may.read(page.page_name):
363            return []
364
365
366        actions = get_names(config)
367
368        # Filter non ui actions (starts with lower case letter)
369        actions = [action for action in actions if not action[0].islower()]
370
371        # Filter actions by page type, acl and user state
372        excluded = []
373        if (page.isUnderlayPage() and not page.isStandardPage()) or \
374                not user.may.write(page.page_name) or \
375                not user.may.delete(page.page_name):
376                # Prevent modification of underlay only pages, or pages
377                # the user can't write and can't delete
378                excluded = [u'RenamePage', u'DeletePage', ] # AttachFile must NOT be here!
379        return set([action for action in actions if not action in excluded])
380
381
382