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