diff options
Diffstat (limited to 'mediagoblin/tools')
-rw-r--r-- | mediagoblin/tools/crypto.py | 15 | ||||
-rw-r--r-- | mediagoblin/tools/exif.py | 2 | ||||
-rw-r--r-- | mediagoblin/tools/mail.py | 22 | ||||
-rw-r--r-- | mediagoblin/tools/pagination.py | 8 | ||||
-rw-r--r-- | mediagoblin/tools/pluginapi.py | 2 | ||||
-rw-r--r-- | mediagoblin/tools/request.py | 18 | ||||
-rw-r--r-- | mediagoblin/tools/response.py | 80 | ||||
-rw-r--r-- | mediagoblin/tools/session.py | 11 | ||||
-rw-r--r-- | mediagoblin/tools/staticdirect.py | 42 | ||||
-rw-r--r-- | mediagoblin/tools/template.py | 39 | ||||
-rw-r--r-- | mediagoblin/tools/validator.py | 46 |
11 files changed, 271 insertions, 14 deletions
diff --git a/mediagoblin/tools/crypto.py b/mediagoblin/tools/crypto.py index 1379d21b..917e674c 100644 --- a/mediagoblin/tools/crypto.py +++ b/mediagoblin/tools/crypto.py @@ -14,6 +14,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import base64 +import string import errno import itsdangerous import logging @@ -24,6 +26,9 @@ from mediagoblin import mg_globals _log = logging.getLogger(__name__) +# produces base64 alphabet +alphabet = string.ascii_letters + "-_" +base = len(alphabet) # Use the system (hardware-based) random number generator if it exists. # -- this optimization is lifted from Django @@ -111,3 +116,13 @@ def get_timed_signer_url(namespace): assert __itsda_secret is not None return itsdangerous.URLSafeTimedSerializer(__itsda_secret, salt=namespace) + +def random_string(length): + """ Returns a URL safe base64 encoded crypographically strong string """ + rstring = "" + for i in range(length): + n = getrandbits(6) # 6 bytes = 2^6 = 64 + n = divmod(n, base)[1] + rstring += alphabet[n] + + return rstring diff --git a/mediagoblin/tools/exif.py b/mediagoblin/tools/exif.py index d0f9d0a6..6b3639e8 100644 --- a/mediagoblin/tools/exif.py +++ b/mediagoblin/tools/exif.py @@ -134,7 +134,7 @@ def _ratio_to_list(ratio): def get_useful(tags): - return dict((key, tag) for (key, tag) in tags.iteritems() if key in USEFUL_TAGS) + return dict((key, tag) for (key, tag) in tags.iteritems()) def get_gps_data(tags): diff --git a/mediagoblin/tools/mail.py b/mediagoblin/tools/mail.py index 4fa02ce5..0fabc5a9 100644 --- a/mediagoblin/tools/mail.py +++ b/mediagoblin/tools/mail.py @@ -16,7 +16,7 @@ import smtplib from email.MIMEText import MIMEText -from mediagoblin import mg_globals +from mediagoblin import mg_globals, messages from mediagoblin.tools import common ### ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -90,7 +90,12 @@ def send_email(from_addr, to_addrs, subject, message_body): if common.TESTS_ENABLED or mg_globals.app_config['email_debug_mode']: mhost = FakeMhost() elif not mg_globals.app_config['email_debug_mode']: - mhost = smtplib.SMTP( + if mg_globals.app_config['email_smtp_use_ssl']: + smtp_init = smtplib.SMTP_SSL + else: + smtp_init = smtplib.SMTP + + mhost = smtp_init( mg_globals.app_config['email_smtp_host'], mg_globals.app_config['email_smtp_port']) @@ -135,3 +140,16 @@ def normalize_email(email): return None email = "@".join((em_user, em_dom.lower())) return email + + +def email_debug_message(request): + """ + If the server is running in email debug mode (which is + the current default), give a debug message to the user + so that they have an idea where to find their email. + """ + if mg_globals.app_config['email_debug_mode']: + # DEBUG message, no need to translate + messages.add_message(request, messages.DEBUG, + u"This instance is running in email debug mode. " + u"The email will be on the console of the server process.") diff --git a/mediagoblin/tools/pagination.py b/mediagoblin/tools/pagination.py index d0f08c94..855878e0 100644 --- a/mediagoblin/tools/pagination.py +++ b/mediagoblin/tools/pagination.py @@ -18,7 +18,7 @@ import urllib import copy from math import ceil, floor from itertools import izip, count - +from werkzeug.datastructures import MultiDict PAGINATION_DEFAULT_PER_PAGE = 30 @@ -98,7 +98,11 @@ class Pagination(object): """ Get a page url by adding a page= parameter to the base url """ - new_get_params = dict(get_params) or {} + if isinstance(get_params, MultiDict): + new_get_params = get_params.to_dict() + else: + new_get_params = dict(get_params) or {} + new_get_params['page'] = page_no return "%s?%s" % ( base_url, urllib.urlencode(new_get_params)) diff --git a/mediagoblin/tools/pluginapi.py b/mediagoblin/tools/pluginapi.py index 3f98aa8a..1eabe9f1 100644 --- a/mediagoblin/tools/pluginapi.py +++ b/mediagoblin/tools/pluginapi.py @@ -252,7 +252,7 @@ def get_hook_templates(hook_name): .. code-block:: html+jinja - {% template_hook "media_sidebar" %} + {% template_hook("media_sidebar") %} ... which will include all templates for you, partly using this method. diff --git a/mediagoblin/tools/request.py b/mediagoblin/tools/request.py index ee342eae..d4739039 100644 --- a/mediagoblin/tools/request.py +++ b/mediagoblin/tools/request.py @@ -14,12 +14,18 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import json import logging from mediagoblin.db.models import User _log = logging.getLogger(__name__) +# MIME-Types +form_encoded = "application/x-www-form-urlencoded" +json_encoded = "application/json" + + def setup_user_in_request(request): """ Examine a request and tack on a request.user parameter if that's @@ -36,3 +42,15 @@ def setup_user_in_request(request): # this session. _log.warn("Killing session for user id %r", request.session['user_id']) request.session.delete() + +def decode_request(request): + """ Decodes a request based on MIME-Type """ + data = request.get_data() + + if request.content_type == json_encoded: + data = json.loads(data) + elif request.content_type == form_encoded or request.content_type == "": + data = request.form + else: + data = "" + return data diff --git a/mediagoblin/tools/response.py b/mediagoblin/tools/response.py index 80df1f5a..cd99a230 100644 --- a/mediagoblin/tools/response.py +++ b/mediagoblin/tools/response.py @@ -14,11 +14,15 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import json + import werkzeug.utils from werkzeug.wrappers import Response as wz_Response from mediagoblin.tools.template import render_template from mediagoblin.tools.translate import (lazy_pass_to_ugettext as _, pass_to_ugettext) +from mediagoblin.db.models import UserBan, User +from datetime import date class Response(wz_Response): """Set default response mimetype to HTML, otherwise we get text/plain""" @@ -31,7 +35,6 @@ def render_to_response(request, template, context, status=200): render_template(request, template, context), status=status) - def render_error(request, status=500, title=_('Oops!'), err_msg=_('An error occured')): """Render any error page with a given error code, title and text body @@ -44,6 +47,15 @@ def render_error(request, status=500, title=_('Oops!'), {'err_code': status, 'title': title, 'err_msg': err_msg}), status=status) +def render_400(request, err_msg=None): + """ Render a standard 400 page""" + _ = pass_to_ugettext + title = _("Bad Request") + if err_msg is None: + err_msg = _("The request sent to the server is invalid, \ +please double check it") + + return render_error(request, 400, title, err_msg) def render_403(request): """Render a standard 403 page""" @@ -62,6 +74,21 @@ def render_404(request): "you're looking for has been moved or deleted.") return render_error(request, 404, err_msg=err_msg) +def render_user_banned(request): + """Renders the page which tells a user they have been banned, for how long + and the reason why they have been banned" + """ + user_ban = UserBan.query.get(request.user.id) + if (user_ban.expiration_date is not None and + date.today()>user_ban.expiration_date): + + user_ban.delete() + return redirect(request, + 'index') + return render_to_response(request, + 'mediagoblin/banned.html', + {'reason':user_ban.reason, + 'expiration_date':user_ban.expiration_date}) def render_http_exception(request, exc, description): """Return Response() given a werkzeug.HTTPException @@ -77,7 +104,7 @@ def render_http_exception(request, exc, description): elif stock_desc and exc.code == 404: return render_404(request) - return render_error(request, title=exc.args[0], + return render_error(request, title='{0} {1}'.format(exc.code, exc.name), err_msg=description, status=exc.code) @@ -99,3 +126,52 @@ def redirect(request, *args, **kwargs): if querystring: location += querystring return werkzeug.utils.redirect(location) + + +def redirect_obj(request, obj): + """Redirect to the page for the given object. + + Requires obj to have a .url_for_self method.""" + return redirect(request, location=obj.url_for_self(request.urlgen)) + +def json_response(serializable, _disable_cors=False, *args, **kw): + ''' + Serializes a json objects and returns a werkzeug Response object with the + serialized value as the response body and Content-Type: application/json. + + :param serializable: A json-serializable object + + Any extra arguments and keyword arguments are passed to the + Response.__init__ method. + ''' + + response = wz_Response(json.dumps(serializable), *args, content_type='application/json', **kw) + + if not _disable_cors: + cors_headers = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, X-Requested-With'} + for key, value in cors_headers.iteritems(): + response.headers.set(key, value) + + return response + +def form_response(data, *args, **kwargs): + """ + Responds using application/x-www-form-urlencoded and returns a werkzeug + Response object with the data argument as the body + and 'application/x-www-form-urlencoded' as the Content-Type. + + Any extra arguments and keyword arguments are passed to the + Response.__init__ method. + """ + + response = wz_Response( + data, + content_type="application/x-www-form-urlencoded", + *args, + **kwargs + ) + + return response diff --git a/mediagoblin/tools/session.py b/mediagoblin/tools/session.py index fdc32523..a57f69cc 100644 --- a/mediagoblin/tools/session.py +++ b/mediagoblin/tools/session.py @@ -17,10 +17,12 @@ import itsdangerous import logging -import crypto +from mediagoblin.tools import crypto _log = logging.getLogger(__name__) +MAX_AGE = 30 * 24 * 60 * 60 + class Session(dict): def __init__(self, *args, **kwargs): self.send_new_cookie = False @@ -64,5 +66,10 @@ class SessionManager(object): elif not session: response.delete_cookie(self.cookie_name) else: + if session.get('stay_logged_in', False): + max_age = MAX_AGE + else: + max_age = None + response.set_cookie(self.cookie_name, self.signer.dumps(session), - httponly=True) + max_age=max_age, httponly=True) diff --git a/mediagoblin/tools/staticdirect.py b/mediagoblin/tools/staticdirect.py index 31abc566..8381b8b6 100644 --- a/mediagoblin/tools/staticdirect.py +++ b/mediagoblin/tools/staticdirect.py @@ -35,7 +35,8 @@ class StaticDirect(object): staticdirect to. In general, you should supply a None domain, as that's the "default" domain. - Things work like this: + Things work like this:: + >>> staticdirect = StaticDirect( ... {None: "/static/", ... "theme": "http://example.org/themestatic/"}) @@ -61,3 +62,42 @@ class StaticDirect(object): def get(self, filepath, domain=None): return '%s/%s' % ( self.domains[domain], filepath.lstrip('/')) + + +class PluginStatic(object): + """Pass this into the ``'static_setup'`` hook to register your + plugin's static directory. + + This has two mandatory attributes that you must pass in on class + init: + + - *name:* this name will be both used for lookup in "urlgen" for + your plugin's static resources and for the subdirectory that + it'll be "mounted" to for serving via your web browser. It + *MUST* be unique. If writing a plugin bundled with MediaGoblin + please use the pattern 'coreplugin__foo' where 'foo' is your + plugin name. All external plugins should use their modulename, + so if your plugin is 'mg_bettertags' you should also call this + name 'mg_bettertags'. + - *file_path:* the directory your plugin's static resources are + located in. It's recommended that you use + pkg_resources.resource_filename() for this. + + An example of using this:: + + from pkg_resources import resource_filename + from mediagoblin.tools.staticdirect import PluginStatic + + hooks = { + 'static_setup': lambda: PluginStatic( + 'mg_bettertags', + resource_filename('mg_bettertags', 'static')) + } + + """ + def __init__(self, name, file_path): + self.name = name + self.file_path = file_path + + def __call__(self): + return self diff --git a/mediagoblin/tools/template.py b/mediagoblin/tools/template.py index 5d320f75..e5acdf45 100644 --- a/mediagoblin/tools/template.py +++ b/mediagoblin/tools/template.py @@ -29,12 +29,11 @@ from mediagoblin.tools import common from mediagoblin.tools.translate import is_rtl from mediagoblin.tools.translate import set_thread_locale from mediagoblin.tools.translate import get_locale_from_request -from mediagoblin.tools.pluginapi import get_hook_templates +from mediagoblin.tools.pluginapi import get_hook_templates, hook_transform from mediagoblin.tools.timesince import timesince from mediagoblin.meddleware.csrf import render_csrf_form_token - SETUP_JINJA_ENVS = {} @@ -52,6 +51,12 @@ def get_jinja_env(template_loader, locale): if locale in SETUP_JINJA_ENVS: return SETUP_JINJA_ENVS[locale] + # The default config does not require a [jinja2] block. + # You may create one if you wish to enable additional jinja2 extensions, + # see example in config_spec.ini + jinja2_config = mg_globals.global_config.get('jinja2', {}) + local_exts = jinja2_config.get('extensions', []) + # jinja2.StrictUndefined will give exceptions on references # to undefined/unknown variables in templates. template_env = jinja2.Environment( @@ -59,7 +64,7 @@ def get_jinja_env(template_loader, locale): undefined=jinja2.StrictUndefined, extensions=[ 'jinja2.ext.i18n', 'jinja2.ext.autoescape', - TemplateHookExtension]) + TemplateHookExtension] + local_exts) template_env.install_gettext_callables( mg_globals.thread_scope.translations.ugettext, @@ -74,6 +79,7 @@ def get_jinja_env(template_loader, locale): template_env.globals['app_config'] = mg_globals.app_config template_env.globals['global_config'] = mg_globals.global_config template_env.globals['version'] = _version.__version__ + template_env.globals['auth'] = mg_globals.app.auth template_env.globals['is_rtl'] = is_rtl(locale) template_env.filters['urlencode'] = url_quote_plus @@ -83,6 +89,19 @@ def get_jinja_env(template_loader, locale): # allow for hooking up plugin templates template_env.globals['get_hook_templates'] = get_hook_templates + template_env.globals = hook_transform( + 'template_global_context', template_env.globals) + + #### THIS IS TEMPORARY, PLEASE FIX IT + ## Notifications stuff is not yet a plugin (and we're not sure it will be), + ## but it needs to add stuff to the context. This is THE WRONG WAY TO DO IT + from mediagoblin import notifications + template_env.globals['get_notifications'] = notifications.get_notifications + template_env.globals[ + 'get_notification_count'] = notifications.get_notification_count + template_env.globals[ + 'get_comment_subscription'] = notifications.get_comment_subscription + if exists(locale): SETUP_JINJA_ENVS[locale] = template_env @@ -106,6 +125,20 @@ def render_template(request, template_path, context): rendered_csrf_token = render_csrf_form_token(request) if rendered_csrf_token is not None: context['csrf_token'] = render_csrf_form_token(request) + + # allow plugins to do things to the context + if request.controller_name: + context = hook_transform( + (request.controller_name, template_path), + context) + + # More evil: allow plugins to possibly do something to the context + # in every request ever with access to the request and other + # variables. Note: this is slower than using + # template_global_context + context = hook_transform( + 'template_context_prerender', context) + rendered = template.render(context) if common.TESTS_ENABLED: diff --git a/mediagoblin/tools/validator.py b/mediagoblin/tools/validator.py new file mode 100644 index 00000000..03598f9c --- /dev/null +++ b/mediagoblin/tools/validator.py @@ -0,0 +1,46 @@ +# GNU MediaGoblin -- federated, autonomous media hosting +# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from wtforms.validators import Email, URL + +def validate_email(email): + """ + Validates an email + + Returns True if valid and False if invalid + """ + + email_re = Email().regex + result = email_re.match(email) + if result is None: + return False + else: + return result.string + +def validate_url(url): + """ + Validates a url + + Returns True if valid and False if invalid + """ + + url_re = URL().regex + result = url_re.match(url) + if result is None: + return False + else: + return result.string + |