At this point, I am very close to done with this code! I made one big change at
[mediagoblin:mediagoblin.git] / mediagoblin / decorators.py
1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors.  See AUTHORS.
3 #
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU Affero General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 from functools import wraps
18
19 from urlparse import urljoin
20 from werkzeug.exceptions import Forbidden, NotFound
21 from oauthlib.oauth1 import ResourceEndpoint
22
23 from mediagoblin import mg_globals as mgg
24 from mediagoblin import messages
25 from mediagoblin.db.models import (MediaEntry, User, MediaComment,
26                                                         UserBan, Privilege)
27 from mediagoblin.tools.response import (redirect, render_404,
28                                                                 render_user_banned, json_response)
29 from mediagoblin.tools.translate import pass_to_ugettext as _
30
31 from mediagoblin.oauth.tools.request import decode_authorization_header
32 from mediagoblin.oauth.oauth import GMGRequestValidator
33
34
35 def user_not_banned(controller):
36     """
37     Requires that the user has not been banned. Otherwise redirects to the page
38     explaining why they have been banned
39     """
40     @wraps(controller)
41     def wrapper(request, *args, **kwargs):
42         if request.user:
43             if request.user.is_banned():
44                 return render_user_banned(request)
45         return controller(request, *args, **kwargs)
46
47     return wrapper
48
49
50 def require_active_login(controller):
51     """
52     Require an active login from the user. If the user is banned, redirects to
53     the "You are Banned" page.
54     """
55     @wraps(controller)
56     @user_not_banned
57     def new_controller_func(request, *args, **kwargs):
58         if request.user and \
59                 not request.user.has_privilege(u'active'):
60             return redirect(
61                 request, 'mediagoblin.user_pages.user_home',
62                 user=request.user.username)
63         elif not request.user or not request.user.has_privilege(u'active'):
64             next_url = urljoin(
65                     request.urlgen('mediagoblin.auth.login',
66                         qualified=True),
67                     request.url)
68
69             return redirect(request, 'mediagoblin.auth.login',
70                             next=next_url)
71
72         return controller(request, *args, **kwargs)
73
74     return new_controller_func
75
76
77 def user_has_privilege(privilege_name):
78     """
79     Requires that a user have a particular privilege in order to access a page.
80     In order to require that a user have multiple privileges, use this
81     decorator twice on the same view. This decorator also makes sure that the
82     user is not banned, or else it redirects them to the "You are Banned" page.
83
84         :param privilege_name       A unicode object that is that represents
85                                         the privilege object. This object is
86                                         the name of the privilege, as assigned
87                                         in the Privilege.privilege_name column
88     """
89
90     def user_has_privilege_decorator(controller):
91         @wraps(controller)
92         @require_active_login
93         def wrapper(request, *args, **kwargs):
94             user_id = request.user.id
95             if not request.user.has_privilege(privilege_name):
96                 raise Forbidden()
97
98             return controller(request, *args, **kwargs)
99
100         return wrapper
101     return user_has_privilege_decorator
102
103
104 def active_user_from_url(controller):
105     """Retrieve User() from <user> URL pattern and pass in as url_user=...
106
107     Returns a 404 if no such active user has been found"""
108     @wraps(controller)
109     def wrapper(request, *args, **kwargs):
110         user = User.query.filter_by(username=request.matchdict['user']).first()
111         if user is None:
112             return render_404(request)
113
114         return controller(request, *args, url_user=user, **kwargs)
115
116     return wrapper
117
118
119 def user_may_delete_media(controller):
120     """
121     Require user ownership of the MediaEntry to delete.
122     """
123     @wraps(controller)
124     def wrapper(request, *args, **kwargs):
125         uploader_id = kwargs['media'].uploader
126         if not (request.user.has_privilege(u'admin') or
127                 request.user.id == uploader_id):
128             raise Forbidden()
129
130         return controller(request, *args, **kwargs)
131
132     return wrapper
133
134
135 def user_may_alter_collection(controller):
136     """
137     Require user ownership of the Collection to modify.
138     """
139     @wraps(controller)
140     def wrapper(request, *args, **kwargs):
141         creator_id = request.db.User.query.filter_by(
142             username=request.matchdict['user']).first().id
143         if not (request.user.has_privilege(u'admin') or
144                 request.user.id == creator_id):
145             raise Forbidden()
146
147         return controller(request, *args, **kwargs)
148
149     return wrapper
150
151
152 def uses_pagination(controller):
153     """
154     Check request GET 'page' key for wrong values
155     """
156     @wraps(controller)
157     def wrapper(request, *args, **kwargs):
158         try:
159             page = int(request.GET.get('page', 1))
160             if page < 0:
161                 return render_404(request)
162         except ValueError:
163             return render_404(request)
164
165         return controller(request, page=page, *args, **kwargs)
166
167     return wrapper
168
169
170 def get_user_media_entry(controller):
171     """
172     Pass in a MediaEntry based off of a url component
173     """
174     @wraps(controller)
175     def wrapper(request, *args, **kwargs):
176         user = User.query.filter_by(username=request.matchdict['user']).first()
177         if not user:
178             raise NotFound()
179
180         media = None
181
182         # might not be a slug, might be an id, but whatever
183         media_slug = request.matchdict['media']
184
185         # if it starts with id: it actually isn't a slug, it's an id.
186         if media_slug.startswith(u'id:'):
187             try:
188                 media = MediaEntry.query.filter_by(
189                     id=int(media_slug[3:]),
190                     state=u'processed',
191                     uploader=user.id).first()
192             except ValueError:
193                 raise NotFound()
194         else:
195             # no magical id: stuff?  It's a slug!
196             media = MediaEntry.query.filter_by(
197                 slug=media_slug,
198                 state=u'processed',
199                 uploader=user.id).first()
200
201         if not media:
202             # Didn't find anything?  Okay, 404.
203             raise NotFound()
204
205         return controller(request, media=media, *args, **kwargs)
206
207     return wrapper
208
209
210 def get_user_collection(controller):
211     """
212     Pass in a Collection based off of a url component
213     """
214     @wraps(controller)
215     def wrapper(request, *args, **kwargs):
216         user = request.db.User.query.filter_by(
217             username=request.matchdict['user']).first()
218
219         if not user:
220             return render_404(request)
221
222         collection = request.db.Collection.query.filter_by(
223             slug=request.matchdict['collection'],
224             creator=user.id).first()
225
226         # Still no collection?  Okay, 404.
227         if not collection:
228             return render_404(request)
229
230         return controller(request, collection=collection, *args, **kwargs)
231
232     return wrapper
233
234
235 def get_user_collection_item(controller):
236     """
237     Pass in a CollectionItem based off of a url component
238     """
239     @wraps(controller)
240     def wrapper(request, *args, **kwargs):
241         user = request.db.User.query.filter_by(
242             username=request.matchdict['user']).first()
243
244         if not user:
245             return render_404(request)
246
247         collection_item = request.db.CollectionItem.query.filter_by(
248             id=request.matchdict['collection_item']).first()
249
250         # Still no collection item?  Okay, 404.
251         if not collection_item:
252             return render_404(request)
253
254         return controller(request, collection_item=collection_item, *args, **kwargs)
255
256     return wrapper
257
258
259 def get_media_entry_by_id(controller):
260     """
261     Pass in a MediaEntry based off of a url component
262     """
263     @wraps(controller)
264     def wrapper(request, *args, **kwargs):
265         media = MediaEntry.query.filter_by(
266                 id=request.matchdict['media_id'],
267                 state=u'processed').first()
268         # Still no media?  Okay, 404.
269         if not media:
270             return render_404(request)
271
272         given_username = request.matchdict.get('user')
273         if given_username and (given_username != media.get_uploader.username):
274             return render_404(request)
275
276         return controller(request, media=media, *args, **kwargs)
277
278     return wrapper
279
280
281 def get_workbench(func):
282     """Decorator, passing in a workbench as kwarg which is cleaned up afterwards"""
283
284     @wraps(func)
285     def new_func(*args, **kwargs):
286         with mgg.workbench_manager.create() as workbench:
287             return func(*args, workbench=workbench, **kwargs)
288
289     return new_func
290
291
292 def allow_registration(controller):
293     """ Decorator for if registration is enabled"""
294     @wraps(controller)
295     def wrapper(request, *args, **kwargs):
296         if not mgg.app_config["allow_registration"]:
297             messages.add_message(
298                 request,
299                 messages.WARNING,
300                 _('Sorry, registration is disabled on this instance.'))
301             return redirect(request, "index")
302
303         return controller(request, *args, **kwargs)
304
305     return wrapper
306
307 def allow_reporting(controller):
308     """ Decorator for if reporting is enabled"""
309     @wraps(controller)
310     def wrapper(request, *args, **kwargs):
311         if not mgg.app_config["allow_reporting"]:
312             messages.add_message(
313                 request,
314                 messages.WARNING,
315                 _('Sorry, reporting is disabled on this instance.'))
316             return redirect(request, 'index')
317
318         return controller(request, *args, **kwargs)
319
320     return wrapper
321
322 def get_optional_media_comment_by_id(controller):
323     """
324     Pass in a MediaComment based off of a url component. Because of this decor-
325     -ator's use in filing Media or Comment Reports, it has two valid outcomes.
326
327     :returns        The view function being wrapped with kwarg `comment` set to
328                         the MediaComment who's id is in the URL. If there is a
329                         comment id in the URL and if it is valid.
330     :returns        The view function being wrapped with kwarg `comment` set to
331                         None. If there is no comment id in the URL.
332     :returns        A 404 Error page, if there is a comment if in the URL and it
333                         is invalid.
334     """
335     @wraps(controller)
336     def wrapper(request, *args, **kwargs):
337         if 'comment' in request.matchdict:
338             comment = MediaComment.query.filter_by(
339                     id=request.matchdict['comment']).first()
340
341             if comment is None:
342                 return render_404(request)
343
344             return controller(request, comment=comment, *args, **kwargs)
345         else:
346             return controller(request, comment=None, *args, **kwargs)
347     return wrapper
348
349
350 def auth_enabled(controller):
351     """Decorator for if an auth plugin is enabled"""
352     @wraps(controller)
353     def wrapper(request, *args, **kwargs):
354         if not mgg.app.auth:
355             messages.add_message(
356                 request,
357                 messages.WARNING,
358                 _('Sorry, authentication is disabled on this instance.'))
359             return redirect(request, 'index')
360
361         return controller(request, *args, **kwargs)
362
363     return wrapper
364
365 def require_admin_or_moderator_login(controller):
366     """
367     Require a login from an administrator or a moderator.
368     """
369     @wraps(controller)
370     def new_controller_func(request, *args, **kwargs):
371         if request.user and \
372             not request.user.has_privilege(u'admin',u'moderator'):
373
374             raise Forbidden()
375         elif not request.user:
376             next_url = urljoin(
377                     request.urlgen('mediagoblin.auth.login',
378                         qualified=True),
379                     request.url)
380
381             return redirect(request, 'mediagoblin.auth.login',
382                             next=next_url)
383
384         return controller(request, *args, **kwargs)
385
386     return new_controller_func
387
388
389
390 def oauth_required(controller):
391     """ Used to wrap API endpoints where oauth is required """
392     @wraps(controller)
393     def wrapper(request, *args, **kwargs):
394         data = request.headers
395         authorization = decode_authorization_header(data)
396
397         if authorization == dict():
398             error = "Missing required parameter."
399             return json_response({"error": error}, status=400)
400
401
402         request_validator = GMGRequestValidator()
403         resource_endpoint = ResourceEndpoint(request_validator)
404         valid, request = resource_endpoint.validate_protected_resource_request(
405                 uri=request.url,
406                 http_method=request.method,
407                 body=request.get_data(),
408                 headers=dict(request.headers),
409                 )
410
411         if not valid:
412             error = "Invalid oauth prarameter."
413             return json_response({"error": error}, status=400)
414
415         return controller(request, *args, **kwargs)
416
417     return wrapper