Software Freedom Law Center

root/trunk/trac/trac/attachment.py

Revision 103, 30.1 kB (checked in by bkuhn, 8 months ago)

r129@hughes: bkuhn | 2008-05-01 21:46:34 -0400

  • Merged upstream trac via: svk smerge /loblaw/local/branches/trac.upstream-r6969 .
Line 
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2003-2008 Edgewall Software
4 # Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
5 # Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
6 # All rights reserved.
7 #
8 # This software is licensed as described in the file COPYING, which
9 # you should have received as part of this distribution. The terms
10 # are also available at http://trac.edgewall.org/wiki/TracLicense.
11 #
12 # This software consists of voluntary contributions made by many
13 # individuals. For the exact contribution history, see the revision
14 # history and logs, available at http://trac.edgewall.org/log/.
15 #
16 # Author: Jonas Borgström <jonas@edgewall.com>
17 #         Christopher Lenz <cmlenz@gmx.de>
18
19 from datetime import datetime
20 import os
21 import re
22 import shutil
23 import time
24 import unicodedata
25
26 from genshi.builder import tag
27
28 from trac import perm, util
29 from trac.config import BoolOption, IntOption
30 from trac.core import *
31 from trac.env import IEnvironmentSetupParticipant
32 from trac.mimeview import *
33 from trac.perm import PermissionError, PermissionSystem, IPermissionPolicy
34 from trac.resource import *
35 from trac.util import get_reporter_id, create_unique_file, content_disposition
36 from trac.util.datefmt import to_timestamp, utc
37 from trac.util.text import unicode_quote, unicode_unquote, pretty_size
38 from trac.util.translation import _
39 from trac.web import HTTPBadRequest, IRequestHandler
40 from trac.web.chrome import add_link, add_stylesheet, add_ctxtnav, \
41                             INavigationContributor
42 from trac.web.href import Href
43 from trac.wiki.api import IWikiSyntaxProvider
44 from trac.wiki.formatter import format_to_oneliner
45
46
47 class InvalidAttachment(TracError):
48     """Exception raised when attachment validation fails."""
49
50
51 class IAttachmentChangeListener(Interface):
52     """Extension point interface for components that require notification when
53     attachments are created or deleted."""
54
55     def attachment_added(attachment):
56         """Called when an attachment is added."""
57
58     def attachment_deleted(attachment):
59         """Called when an attachment is deleted."""
60
61
62 class IAttachmentManipulator(Interface):
63     """Extension point interface for components that need to manipulate
64     attachments.
65    
66     Unlike change listeners, a manipulator can reject changes being committed
67     to the database."""
68
69     def prepare_attachment(req, attachment, fields):
70         """Not currently called, but should be provided for future
71         compatibility."""
72
73     def validate_attachment(req, attachment):
74         """Validate an attachment after upload but before being stored in Trac
75         environment.
76        
77         Must return a list of `(field, message)` tuples, one for each problem
78         detected. `field` can be any of `description`, `username`, `filename`,
79         `content`, or `None` to indicate an overall problem with the
80         attachment. Therefore, a return value of `[]` means everything is
81         OK."""
82
83 class ILegacyAttachmentPolicyDelegate(Interface):
84     """Interface that can be used by plugins to seemlessly participate to the
85        legacy way of checking for attachment permissions.
86
87        This should no longer be necessary once it becomes easier to
88        setup fine-grained permissions in the default permission store.
89     """
90
91     def check_attachment_permission(action, username, resource, perm):
92         """Return the usual True/False/None security policy decision
93            appropriate for the requested action on an attachment.
94
95             :param action: one of ATTACHMENT_VIEW, ATTACHMENT_CREATE,
96                                   ATTACHMENT_DELETE
97             :param username: the user string
98             :param resource: the `Resource` for the attachment. Note that when
99                              ATTACHMENT_CREATE is checked, the resource `.id`
100                              will be `None`.
101             :param perm: the permission cache for that username and resource
102             """
103
104
105 class Attachment(object):
106
107     def __init__(self, env, parent_realm_or_attachment_resource,
108                  parent_id=None, filename=None, db=None):
109         if isinstance(parent_realm_or_attachment_resource, Resource):
110             self.resource = parent_realm_or_attachment_resource
111         else:
112             self.resource = Resource(parent_realm_or_attachment_resource,
113                                      parent_id).child('attachment', filename)
114         self.env = env
115         self.parent_realm = self.resource.parent.realm
116         self.parent_id = unicode(self.resource.parent.id)
117         if self.resource.id:
118             self._fetch(self.resource.id, db)
119         else:
120             self.filename = None
121             self.description = None
122             self.size = None
123             self.date = None
124             self.author = None
125             self.ipnr = None
126
127     def _set_filename(self, val):
128         self.resource.id = val
129
130     filename = property(lambda self: self.resource.id, _set_filename)
131
132     def _fetch(self, filename, db=None):
133         if not db:
134             db = self.env.get_db_cnx()
135         cursor = db.cursor()
136         cursor.execute("SELECT filename,description,size,time,author,ipnr "
137                        "FROM attachment WHERE type=%s AND id=%s "
138                        "AND filename=%s ORDER BY time",
139                        (self.parent_realm, unicode(self.parent_id), filename))
140         row = cursor.fetchone()
141         cursor.close()
142         if not row:
143             self.filename = filename
144             raise ResourceNotFound(_("Attachment '%(title)s' does not exist.",
145                                      title=self.title), _('Invalid Attachment'))
146         self.filename = row[0]
147         self.description = row[1]
148         self.size = row[2] and int(row[2]) or 0
149         time = row[3] and int(row[3]) or 0
150         self.date = datetime.fromtimestamp(time, utc)
151         self.author = row[4]
152         self.ipnr = row[5]
153
154     def _get_path(self):
155         path = os.path.join(self.env.path, 'attachments', self.parent_realm,
156                             unicode_quote(self.parent_id))
157         if self.filename:
158             path = os.path.join(path, unicode_quote(self.filename))
159         return os.path.normpath(path)
160     path = property(_get_path)
161
162     def _get_title(self):
163         return '%s:%s: %s' % (self.parent_realm,
164                               self.parent_id, self.filename)
165     title = property(_get_title)
166
167     def delete(self, db=None):
168         assert self.filename, 'Cannot delete non-existent attachment'
169         if not db:
170             db = self.env.get_db_cnx()
171             handle_ta = True
172         else:
173             handle_ta = False
174
175         cursor = db.cursor()
176         cursor.execute("DELETE FROM attachment WHERE type=%s AND id=%s "
177                        "AND filename=%s", (self.parent_realm, self.parent_id,
178                        self.filename))
179         if os.path.isfile(self.path):
180             try:
181                 os.unlink(self.path)
182             except OSError:
183                 self.env.log.error('Failed to delete attachment file %s',
184                                    self.path, exc_info=True)
185                 if handle_ta:
186                     db.rollback()
187                 raise TracError(_('Could not delete attachment'))
188
189         self.env.log.info('Attachment removed: %s' % self.title)
190         if handle_ta:
191             db.commit()
192
193         for listener in AttachmentModule(self.env).change_listeners:
194             listener.attachment_deleted(self)
195
196
197     def insert(self, filename, fileobj, size, t=None, db=None):
198         # FIXME: `t` should probably be switched to `datetime` too
199         if not db:
200             db = self.env.get_db_cnx()
201             handle_ta = True
202         else:
203             handle_ta = False
204
205         self.size = size and int(size) or 0
206         timestamp = int(t or time.time())
207         self.date = datetime.fromtimestamp(timestamp, utc)
208
209         # Make sure the path to the attachment is inside the environment
210         # attachments directory
211         attachments_dir = os.path.join(os.path.normpath(self.env.path),
212                                        'attachments')
213         commonprefix = os.path.commonprefix([attachments_dir, self.path])
214         assert commonprefix == attachments_dir
215
216         if not os.access(self.path, os.F_OK):
217             os.makedirs(self.path)
218         filename = unicode_quote(filename)
219         path, targetfile = create_unique_file(os.path.join(self.path,
220                                                            filename))
221         try:
222             # Note: `path` is an unicode string because `self.path` was one.
223             # As it contains only quoted chars and numbers, we can use `ascii`
224             basename = os.path.basename(path).encode('ascii')
225             filename = unicode_unquote(basename)
226
227             cursor = db.cursor()
228             cursor.execute("INSERT INTO attachment "
229                            "VALUES (%s,%s,%s,%s,%s,%s,%s,%s)",
230                            (self.parent_realm, self.parent_id, filename,
231                             self.size, timestamp, self.description,
232                             self.author, self.ipnr))
233             shutil.copyfileobj(fileobj, targetfile)
234             self.resource.id = self.filename = filename
235
236             self.env.log.info('New attachment: %s by %s', self.title,
237                               self.author)
238
239             if handle_ta:
240                 db.commit()
241
242             for listener in AttachmentModule(self.env).change_listeners:
243                 listener.attachment_added(self)
244
245         finally:
246             targetfile.close()
247
248     def select(cls, env, parent_realm, parent_id, db=None):
249         if not db:
250             db = env.get_db_cnx()
251         cursor = db.cursor()
252         cursor.execute("SELECT filename,description,size,time,author,ipnr "
253                        "FROM attachment WHERE type=%s AND id=%s ORDER BY time",
254                        (parent_realm, unicode(parent_id)))
255         for filename,description,size,time,author,ipnr in cursor:
256             attachment = Attachment(env, parent_realm, parent_id)
257             attachment.filename = filename
258             attachment.description = description
259             attachment.size = size and int(size) or 0
260             time = time and int(time) or 0
261             attachment.date = datetime.fromtimestamp(time, utc)
262             attachment.author = author
263             attachment.ipnr = ipnr
264             yield attachment
265
266     def delete_all(cls, env, parent_realm, parent_id, db):
267         """Delete all attachments of a given resource.
268
269         As this is usually done while deleting the parent resource,
270         the `db` argument is ''not'' optional here.
271         """
272         attachment_dir = None
273         for attachment in list(cls.select(env, parent_realm, parent_id, db)):
274             attachment_dir = os.path.dirname(attachment.path)
275             attachment.delete(db)
276         if attachment_dir:
277             try:
278                 os.rmdir(attachment_dir)
279             except OSError:
280                 env.log.error("Can't delete attachment directory %s",
281                               attachment_dir, exc_info=True)
282            
283     select = classmethod(select)
284     delete_all = classmethod(delete_all)
285
286     def open(self):
287         self.env.log.debug('Trying to open attachment at %s', self.path)
288         try:
289             fd = open(self.path, 'rb')
290         except IOError:
291             raise ResourceNotFound(_("Attachment '%(filename)s' not found",
292                                      filename=self.filename))
293         return fd
294
295
296 class AttachmentModule(Component):
297
298     implements(IEnvironmentSetupParticipant, IRequestHandler,
299                INavigationContributor, IWikiSyntaxProvider,
300                IResourceManager)
301
302     change_listeners = ExtensionPoint(IAttachmentChangeListener)
303     manipulators = ExtensionPoint(IAttachmentManipulator)
304
305     CHUNK_SIZE = 4096
306
307     max_size = IntOption('attachment', 'max_size', 262144,
308         """Maximum allowed file size (in bytes) for ticket and wiki
309         attachments.""")
310
311     render_unsafe_content = BoolOption('attachment', 'render_unsafe_content',
312                                        'false',
313         """Whether attachments should be rendered in the browser, or
314         only made downloadable.
315
316         Pretty much any file may be interpreted as HTML by the browser,
317         which allows a malicious user to attach a file containing cross-site
318         scripting attacks.
319
320         For public sites where anonymous users can create attachments it is
321         recommended to leave this option disabled (which is the default).""")
322
323     # IEnvironmentSetupParticipant methods
324
325     def environment_created(self):
326         """Create the attachments directory."""
327         if self.env.path:
328             os.mkdir(os.path.join(self.env.path, 'attachments'))
329
330     def environment_needs_upgrade(self, db):
331         return False
332
333     def upgrade_environment(self, db):
334         pass
335
336     # INavigationContributor methods
337
338     def get_active_navigation_item(self, req):
339         return req.args.get('realm')
340
341     def get_navigation_items(self, req):
342         return []
343
344     # IRequestHandler methods
345
346     def match_request(self, req):
347         match = re.match(r'^/(raw-)?attachment/([^/]+)(?:[/:](.*))?$',
348                          req.path_info)
349         if match:
350             raw, realm, path = match.groups()
351             if raw:
352                 req.args['format'] = 'raw'
353             req.args['realm'] = realm
354             if path:
355                 req.args['path'] = path.replace(':', '/')
356             return True
357
358     def process_request(self, req):
359         parent_id = None
360         parent_realm = req.args.get('realm')
361         path = req.args.get('path')
362         filename = None
363        
364         if not parent_realm or not path:
365             raise HTTPBadRequest(_('Bad request'))
366
367         parent_realm = Resource(parent_realm)
368         action = req.args.get('action', 'view')
369         if action == 'new':
370             parent_id = path.rstrip('/')
371         else:
372             segments = path.split('/')
373             parent_id = '/'.join(segments[:-1])
374             filename = len(segments) > 1 and segments[-1]
375
376         parent = parent_realm(id=parent_id)
377        
378         # Link the attachment page to parent resource
379         parent_name = get_resource_name(self.env, parent)
380         parent_url = get_resource_url(self.env, parent, req.href)
381         add_link(req, 'up', parent_url, parent_name)
382         add_ctxtnav(req, _('Back to %(parent)s', parent=parent_name),
383                     parent_url)
384        
385         if action != 'new' and not filename:
386             # there's a trailing '/', show the list
387             return self._render_list(req, parent)
388
389         attachment = Attachment(self.env, parent.child('attachment', filename))
390        
391         if req.method == 'POST':
392             if action == 'new':
393                 self._do_save(req, attachment)
394             elif action == 'delete':
395                 self._do_delete(req, attachment)
396         elif action == 'delete':
397             data = self._render_confirm_delete(req, attachment)
398         elif action == 'new':
399             data = self._render_form(req, attachment)
400         else:
401             data = self._render_view(req, attachment)
402
403         add_stylesheet(req, 'common/css/code.css')
404         return 'attachment.html', data, None
405
406     # IWikiSyntaxProvider methods
407    
408     def get_wiki_syntax(self):
409         return []
410
411     def get_link_resolvers(self):
412         yield ('raw-attachment', self._format_link)
413         yield ('attachment', self._format_link)
414
415     # Public methods
416
417     def attachment_data(self, context):
418         """Return the list of viewable attachments.
419
420         :param context: the rendering context corresponding to the parent
421                         `Resource` of the attachments
422         """
423         parent = context.resource
424         attachments = []
425         for attachment in Attachment.select(self.env, parent.realm, parent.id):
426             if 'ATTACHMENT_VIEW' in context.perm(attachment.resource):
427                 attachments.append(attachment)
428         new_att = parent.child('attachment')
429         return {'attach_href': get_resource_url(self.env, new_att,
430                                                 context.href, action='new'),
431                 'can_create': 'ATTACHMENT_CREATE' in context.perm(new_att),
432                 'attachments': attachments,
433                 'parent': context.resource}
434    
435     def get_history(self, start, stop, realm):
436         """Return an iterable of tuples describing changes to attachments on
437         a particular object realm.
438
439         The tuples are in the form (change, realm, id, filename, time,
440         description, author). `change` can currently only be `created`.
441         """
442         # Traverse attachment directory
443         db = self.env.get_db_cnx()
444         cursor = db.cursor()
445         cursor.execute("SELECT type, id, filename, time, description, author "
446                        "  FROM attachment "
447                        "  WHERE time > %s AND time < %s "
448                        "        AND type = %s",
449                        (to_timestamp(start), to_timestamp(stop), realm))
450         for realm, id, filename, ts, description, author in cursor:
451             time = datetime.fromtimestamp(ts, utc)
452             yield ('created', realm, id, filename, time, description, author)
453
454     def get_timeline_events(self, req, resource_realm, start, stop):
455         """Return an event generator suitable for ITimelineEventProvider.
456
457         Events are changes to attachments on resources of the given
458         `resource_realm.realm`.
459         """
460         for change, realm, id, filename, time, descr, author in \
461                 self.get_history(start, stop, resource_realm.realm):
462             attachment = resource_realm(id=id).child('attachment', filename)
463             if 'ATTACHMENT_VIEW' in req.perm(attachment):
464                 yield ('attachment', time, author, (attachment, descr), self)
465
466     def render_timeline_event(self, context, field, event):
467         attachment, descr = event[3]
468         if field == 'url':
469             return self.get_resource_url(attachment, context.href)
470         elif field == 'title':
471             name = get_resource_name(self.env, attachment.parent)
472             title = get_resource_summary(self.env, attachment.parent)
473             return tag(tag.em(os.path.basename(attachment.id)),
474                        _(" attached to "), tag.em(name, title=title))
475         elif field == 'description':
476             return format_to_oneliner(self.env, context(attachment.parent),
477                                       descr)
478    
479     # IResourceManager methods
480    
481     def get_resource_realms(self):
482         yield 'attachment'
483
484     def get_resource_url(self, resource, href, **kwargs):
485         """Return an URL to the attachment itself.
486
487         A `format` keyword argument equal to `'raw'` will be converted
488         to the raw-attachment prefix.
489         """
490         format = kwargs.get('format')
491         prefix = 'attachment'
492         if format == 'raw':
493             kwargs.pop('format')
494             prefix = 'raw-attachment'
495         parent_href = unicode_unquote(get_resource_url(self.env,
496                             resource.parent(version=None), Href('')))
497         if not resource.id:
498             # link to list of attachments, which must end with a trailing '/'
499             # (see process_request)
500             return href(prefix, parent_href) + '/'
501         else:
502             return href(prefix, parent_href, resource.id, **kwargs)
503
504     def get_resource_description(self, resource, format=None, **kwargs):
505         if format == 'compact':
506             return '%s:%s' % (get_resource_shortname(self.env,
507                                                      resource.parent),
508                               resource.filename)
509         elif format == 'summary':
510             return Attachment(self.env, resource).description
511         if resource.id:
512             return _("Attachment '%(id)s' in %(parent)s", id=resource.id,
513                      parent=get_resource_name(self.env, resource.parent))
514         else:
515             return _("Attachments of %(parent)s",
516                      parent=get_resource_name(self.env, resource.parent))
517
518     # Internal methods
519
520     def _do_save(self, req, attachment):
521         req.perm(attachment.resource).require('ATTACHMENT_CREATE')
522
523         if 'cancel' in req.args:
524             req.redirect(get_resource_url(self.env, attachment.resource.parent,
525                                           req.href))
526
527         upload = req.args['attachment']
528         if not hasattr(upload, 'filename') or not upload.filename:
529             raise TracError(_('No file uploaded'))
530         if hasattr(upload.file, 'fileno'):
531             size = os.fstat(upload.file.fileno())[6]
532         else:
533             upload.file.seek(0, 2) # seek to end of file
534             size = upload.file.tell()
535             upload.file.seek(0)
536         if size == 0:
537             raise TracError(_("Can't upload empty file"))
538
539         # Maximum attachment size (in bytes)
540         max_size = self.max_size
541         if max_size >= 0 and size > max_size:
542             raise TracError(_('Maximum attachment size: %(num)s bytes',
543                               num=max_size), _('Upload failed'))
544
545         # We try to normalize the filename to unicode NFC if we can.
546         # Files uploaded from OS X might be in NFD.
547         filename = unicodedata.normalize('NFC', unicode(upload.filename,
548                                                         'utf-8'))
549         filename = filename.replace('\\', '/').replace(':', '/')
550         filename = os.path.basename(filename)
551         if not filename:
552             raise TracError(_('No file uploaded'))
553         # Now the filename is known, update the attachment resource
554         # attachment.filename = filename
555         attachment.description = req.args.get('description', '')
556         attachment.author = get_reporter_id(req, 'author')
557         attachment.ipnr = req.remote_addr
558
559         # Validate attachment
560         for manipulator in self.manipulators:
561             for field, message in manipulator.validate_attachment(req,
562                                                                   attachment):
563                 if field:
564                     raise InvalidAttachment(_('Attachment field %(field)s is '
565                                               'invalid: %(message)s',
566                                               field=field, message=message))
567                 else:
568                     raise InvalidAttachment(_('Invalid attachment: %(message)s',
569                                               message=message))
570
571         if req.args.get('replace'):
572             try:
573                 old_attachment = Attachment(self.env,
574                                             attachment.resource(id=filename))
575                 if not (old_attachment.author and req.authname \
576                         and old_attachment.author == req.authname):
577                     req.perm(attachment.resource).require('ATTACHMENT_DELETE')
578                 old_attachment.delete()
579             except TracError:
580                 pass # don't worry if there's nothing to replace
581             attachment.filename = None
582         attachment.insert(filename, upload.file, size)
583
584         req.redirect(get_resource_url(self.env, attachment.resource(id=None),
585                                       req.href))
586
587     def _do_delete(self, req, attachment):
588         req.perm(attachment.resource).require('ATTACHMENT_DELETE')
589
590         parent_href = get_resource_url(self.env, attachment.resource.parent,
591                                        req.href)
592         if 'cancel' in req.args:
593             req.redirect(parent_href)
594
595         attachment.delete()
596         req.redirect(parent_href)
597
598     def _render_confirm_delete(self, req, attachment):
599         req.perm(attachment.resource).require('ATTACHMENT_DELETE')
600         return {'mode': 'delete',
601                 'title': _('%(attachment)s (delete)',
602                            attachment=get_resource_name(self.env,
603                                                         attachment.resource)),
604                 'attachment': attachment}
605
606     def _render_form(self, req, attachment):
607         req.perm(attachment.resource).require('ATTACHMENT_CREATE')
608         return {'mode': 'new', 'author': get_reporter_id(req),
609             'attachment': attachment, 'max_size': self.max_size}
610
611     def _render_list(self, req, parent):
612         attachment = parent.child('attachment')
613         data = {
614             'mode': 'list',
615             'attachment': None, # no specific attachment
616             'attachments': self.attachment_data(Context.from_request(req,
617                                                                      parent))
618         }
619
620         return 'attachment.html', data, None
621
622     def _render_view(self, req, attachment):
623         req.perm(attachment.resource).require('ATTACHMENT_VIEW')
624         can_delete = 'ATTACHMENT_DELETE' in req.perm(attachment.resource)
625         req.check_modified(attachment.date, str(can_delete))
626
627         data = {'mode': 'view',
628                 'title': get_resource_name(self.env, attachment.resource),
629                 'attachment': attachment}
630
631         fd = attachment.open()
632         try:
633             mimeview = Mimeview(self.env)
634
635             # MIME type detection
636             str_data = fd.read(1000)
637             fd.seek(0)
638            
639             mime_type = mimeview.get_mimetype(attachment.filename, str_data)
640
641             # Eventually send the file directly
642             format = req.args.get('format')
643             if format in ('raw', 'txt'):
644                 if not self.render_unsafe_content:
645                     # Force browser to download files instead of rendering
646                     # them, since they might contain malicious code enabling
647                     # XSS attacks
648                     req.send_header('Content-Disposition', 'attachment')
649                 if format == 'txt':
650                       mime_type = 'text/plain'
651                 elif not mime_type:
652                     mime_type = 'application/octet-stream'
653                 if 'charset=' not in mime_type:
654                     charset = mimeview.get_charset(str_data, mime_type)
655                     mime_type = mime_type + '; charset=' + charset
656                 req.send_file(attachment.path, mime_type)
657
658             # add ''Plain Text'' alternate link if needed
659             if (self.render_unsafe_content and
660                 mime_type and not mime_type.startswith('text/plain')):
661                 plaintext_href = get_resource_url(self.env,
662                                                   attachment.resource,
663                                                   req.href, format='txt')
664                 add_link(req, 'alternate', plaintext_href, _('Plain Text'),
665                          mime_type)
666
667             # add ''Original Format'' alternate link (always)
668             raw_href = get_resource_url(self.env, attachment.resource,
669                                         req.href, format='raw')
670             add_link(req, 'alternate', raw_href, _('Original Format'),
671                      mime_type)
672
673             self.log.debug("Rendering preview of file %s with mime-type %s"
674                            % (attachment.filename, mime_type))
675
676             data['preview'] = mimeview.preview_data(
677                 Context.from_request(req, attachment.resource), fd,
678                 os.fstat(fd.fileno()).st_size, mime_type,
679                 attachment.filename, raw_href, annotations=['lineno'])
680             return data
681         finally:
682             fd.close()
683
684     def _format_link(self, formatter, ns, target, label):
685         link, params, fragment = formatter.split_link(target)
686         ids = link.split(':', 2)
687         attachment = None
688         if len(ids) == 3:
689             known_realms = ResourceSystem(self.env).get_known_realms()
690             # new-style attachment: TracLinks (filename:realm:id)
691             if ids[1] in known_realms:
692                 attachment = Resource(ids[1], ids[2]).child('attachment',
693                                                             ids[0])
694             else: # try old-style attachment: TracLinks (realm:id:filename)
695                 if ids[0] in known_realms:
696                     attachment = Resource(ids[0], ids[1]).child('attachment',
697                                                                 ids[2])
698         else: # local attachment: TracLinks (filename)
699             attachment = formatter.resource.child('attachment', link)
700         if attachment:
701             try:
702                 model = Attachment(self.env, attachment)
703                 format = None
704                 if ns.startswith('raw'):
705                     format = 'raw'
706                 href = get_resource_url(self.env, attachment, formatter.href,
707                                         format=format)
708                 return tag.a(label, class_='attachment', href=href + params,
709                              title=get_resource_name(self.env, attachment))
710             except ResourceNotFound, e:
711                 pass
712             # FIXME: should be either:
713             #
714             # model = Attachment(self.env, attachment)
715             # if model.exists:
716             #     ...
717             #
718             # or directly:
719             #
720             # if attachment.exists:
721             #
722             # (related to #4130)
723         return tag.a(label, class_='missing attachment', rel='nofollow')
724
725
726 class LegacyAttachmentPolicy(Component):
727
728     implements(IPermissionPolicy)
729    
730     delegates = ExtensionPoint(ILegacyAttachmentPolicyDelegate)
731
732     # IPermissionPolicy methods
733
734     _perm_maps = {
735         'ATTACHMENT_CREATE': {'ticket': 'TICKET_APPEND', 'wiki': 'WIKI_MODIFY',
736                               'milestone': 'MILESTONE_MODIFY'},
737         'ATTACHMENT_VIEW': {'ticket': 'TICKET_VIEW', 'wiki': 'WIKI_VIEW',
738                             'milestone': 'MILESTONE_VIEW'},
739         'ATTACHMENT_DELETE': {'ticket': 'TICKET_ADMIN', 'wiki': 'WIKI_DELETE',
740                               'milestone': 'MILESTONE_DELETE'},
741     }
742
743     def check_permission(self, action, username, resource, perm):
744         perm_map = self._perm_maps.get(action)
745         if not perm_map or not resource or resource.realm != 'attachment':
746             return
747         legacy_action = perm_map.get(resource.parent.realm)
748         if legacy_action:
749             decision = legacy_action in perm
750             if not decision:
751                 self.env.log.debug('LegacyAttachmentPolicy denied %s '
752                                    'access to %s. User needs %s' %
753                                    (username, resource, legacy_action))
754             return decision
755         else:
756             for d in self.delegates:
757                 decision = d.check_attachment_permission(action, username,
758                         resource, perm)
759                 if decision is not None:
760                     return decision
Note: See TracBrowser for help on using the browser.

SFLC Main Page

[frdm] Support SFLC