| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | """ |
|---|
| 3 | zine.application |
|---|
| 4 | ~~~~~~~~~~~~~~~~ |
|---|
| 5 | |
|---|
| 6 | This module implements the central application object :class:`Zine` |
|---|
| 7 | and a couple of helper functions and classes. |
|---|
| 8 | |
|---|
| 9 | |
|---|
| 10 | :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details. |
|---|
| 11 | :license: BSD, see LICENSE for more details. |
|---|
| 12 | """ |
|---|
| 13 | import sys |
|---|
| 14 | from os import path, remove, makedirs, walk, environ |
|---|
| 15 | from time import time |
|---|
| 16 | from urlparse import urlparse |
|---|
| 17 | from collections import deque |
|---|
| 18 | from inspect import getdoc |
|---|
| 19 | from traceback import format_exception |
|---|
| 20 | from StringIO import StringIO |
|---|
| 21 | |
|---|
| 22 | from babel import Locale |
|---|
| 23 | |
|---|
| 24 | from jinja2 import Environment, BaseLoader, TemplateNotFound |
|---|
| 25 | |
|---|
| 26 | from sqlalchemy.exceptions import SQLAlchemyError |
|---|
| 27 | |
|---|
| 28 | from werkzeug import Request as RequestBase, Response as ResponseBase, \ |
|---|
| 29 | SharedDataMiddleware, url_quote, routing, redirect as _redirect, \ |
|---|
| 30 | escape, cached_property, url_encode |
|---|
| 31 | from werkzeug.exceptions import HTTPException, Forbidden, \ |
|---|
| 32 | NotFound |
|---|
| 33 | from werkzeug.contrib.securecookie import SecureCookie |
|---|
| 34 | |
|---|
| 35 | from zine import _core |
|---|
| 36 | from zine.environment import SHARED_DATA, BUILTIN_TEMPLATE_PATH, \ |
|---|
| 37 | BUILTIN_PLUGIN_FOLDER |
|---|
| 38 | from zine.database import db, cleanup_session |
|---|
| 39 | from zine.cache import get_cache |
|---|
| 40 | from zine.utils import ClosingIterator, local, local_manager, dump_json, \ |
|---|
| 41 | htmlhelpers |
|---|
| 42 | from zine.utils.datastructures import ReadOnlyMultiMapping |
|---|
| 43 | from zine.utils.exceptions import UserException |
|---|
| 44 | |
|---|
| 45 | |
|---|
| 46 | #: the default theme settings |
|---|
| 47 | DEFAULT_THEME_SETTINGS = { |
|---|
| 48 | # pagination defaults |
|---|
| 49 | 'pagination.normal': u'<a href="%(url)s">%(page)d</a>', |
|---|
| 50 | 'pagination.active': u'<strong>%(page)d</strong>', |
|---|
| 51 | 'pagination.commata': u'<span class="commata">,\n</span>', |
|---|
| 52 | 'pagination.ellipsis': u'<span class="ellipsis"> …\n</span>', |
|---|
| 53 | 'pagination.threshold': 3, |
|---|
| 54 | 'pagination.left_threshold': 2, |
|---|
| 55 | 'pagination.right_threshold': 1, |
|---|
| 56 | 'pagination.prev_link': False, |
|---|
| 57 | 'pagination.next_link': False, |
|---|
| 58 | 'pagination.gray_prev_link': True, |
|---|
| 59 | 'pagination.gray_next_link': True, |
|---|
| 60 | 'pagination.simple': False, |
|---|
| 61 | |
|---|
| 62 | # how many posts per page? |
|---|
| 63 | 'author.per_page': 30, |
|---|
| 64 | 'archive.per_page': None, |
|---|
| 65 | 'category.per_page': None, |
|---|
| 66 | 'tag.per_page': None, |
|---|
| 67 | |
|---|
| 68 | # datetime formatting settings |
|---|
| 69 | 'date.date_format.default': 'medium', |
|---|
| 70 | 'date.datetime_format.default': 'medium', |
|---|
| 71 | 'date.date_format.short': None, |
|---|
| 72 | 'date.date_format.medium': None, |
|---|
| 73 | 'date.date_format.full': None, |
|---|
| 74 | 'date.date_format.long': None, |
|---|
| 75 | 'date.datetime_format.short': None, |
|---|
| 76 | 'date.datetime_format.medium': None, |
|---|
| 77 | 'date.datetime_format.full': None, |
|---|
| 78 | 'date.datetime_format.long': None, |
|---|
| 79 | |
|---|
| 80 | # query optimizations for overview pages. Themes can change the |
|---|
| 81 | # eager/lazy loading settings of some queries to remove unnecessary |
|---|
| 82 | # overhead that is not in use for what they want to display. For |
|---|
| 83 | # example a theme that wants to load a headline-overview of all the |
|---|
| 84 | # posts in a specific tag but no text at all it makes no sense to |
|---|
| 85 | # load the text and more just to throw away the information. |
|---|
| 86 | # for more information have a look at PostQuery.lightweight |
|---|
| 87 | 'sql.index.lazy': frozenset(['comments']), |
|---|
| 88 | 'sql.author.lazy': frozenset(['comments']), |
|---|
| 89 | 'sql.archive.lazy': frozenset(['comments']), |
|---|
| 90 | 'sql.category.lazy': frozenset(['comments']), |
|---|
| 91 | 'sql.tag.lazy': frozenset(['comments']), |
|---|
| 92 | 'sql.index.deferred': frozenset(), |
|---|
| 93 | 'sql.author.deferred': frozenset(), |
|---|
| 94 | 'sql.archive.deferred': frozenset(), |
|---|
| 95 | 'sql.category.deferred': frozenset(), |
|---|
| 96 | 'sql.tag.deferred': frozenset() |
|---|
| 97 | } |
|---|
| 98 | |
|---|
| 99 | |
|---|
| 100 | def get_request(): |
|---|
| 101 | """Return the current request. If no request is available this function |
|---|
| 102 | returns `None`. |
|---|
| 103 | """ |
|---|
| 104 | return getattr(local, 'request', None) |
|---|
| 105 | |
|---|
| 106 | |
|---|
| 107 | def get_application(): |
|---|
| 108 | """Get the application instance. If the application was not yet set up |
|---|
| 109 | the return value is `None` |
|---|
| 110 | """ |
|---|
| 111 | return _core._application |
|---|
| 112 | |
|---|
| 113 | |
|---|
| 114 | def url_for(endpoint, **args): |
|---|
| 115 | """Get the URL to an endpoint. The keyword arguments provided are used |
|---|
| 116 | as URL values. Unknown URL values are used as keyword argument. |
|---|
| 117 | Additionally there are some special keyword arguments: |
|---|
| 118 | |
|---|
| 119 | `_anchor` |
|---|
| 120 | This string is used as URL anchor. |
|---|
| 121 | |
|---|
| 122 | `_external` |
|---|
| 123 | If set to `True` the URL will be generated with the full server name |
|---|
| 124 | and `http://` prefix. |
|---|
| 125 | """ |
|---|
| 126 | if hasattr(endpoint, 'get_url_values'): |
|---|
| 127 | rv = endpoint.get_url_values() |
|---|
| 128 | if rv is not None: |
|---|
| 129 | if isinstance(rv, basestring): |
|---|
| 130 | return make_external_url(rv) |
|---|
| 131 | endpoint, updated_args = rv |
|---|
| 132 | args.update(updated_args) |
|---|
| 133 | anchor = args.pop('_anchor', None) |
|---|
| 134 | external = args.pop('_external', False) |
|---|
| 135 | rv = get_application().url_adapter.build(endpoint, args, |
|---|
| 136 | force_external=external) |
|---|
| 137 | if anchor is not None: |
|---|
| 138 | rv += '#' + url_quote(anchor) |
|---|
| 139 | return rv |
|---|
| 140 | |
|---|
| 141 | |
|---|
| 142 | def shared_url(spec): |
|---|
| 143 | """Returns a URL to a shared resource.""" |
|---|
| 144 | endpoint, filename = spec.split('::', 1) |
|---|
| 145 | return url_for(endpoint + '/shared', filename=filename) |
|---|
| 146 | |
|---|
| 147 | |
|---|
| 148 | def emit_event(event, *args, **kwargs): |
|---|
| 149 | """Emit a event and return a list of event results. Each called |
|---|
| 150 | function contributes one item to the returned list. |
|---|
| 151 | |
|---|
| 152 | This is equivalent to the following call to :func:`iter_listeners`:: |
|---|
| 153 | |
|---|
| 154 | result = [] |
|---|
| 155 | for listener in iter_listeners(event): |
|---|
| 156 | result.append(listener(*args, **kwargs)) |
|---|
| 157 | """ |
|---|
| 158 | return [x(*args, **kwargs) for x in |
|---|
| 159 | get_application()._event_manager.iter(event)] |
|---|
| 160 | |
|---|
| 161 | |
|---|
| 162 | def iter_listeners(event): |
|---|
| 163 | """Return an iterator for all the listeners for the event provided.""" |
|---|
| 164 | return get_application()._event_manager.iter(event) |
|---|
| 165 | |
|---|
| 166 | |
|---|
| 167 | def add_link(rel, href, type, title=None, charset=None, media=None): |
|---|
| 168 | """Add a new link to the metadata of the current page being processed.""" |
|---|
| 169 | local.page_metadata.append(('link', { |
|---|
| 170 | 'rel': rel, |
|---|
| 171 | 'href': href, |
|---|
| 172 | 'type': type, |
|---|
| 173 | 'title': title, |
|---|
| 174 | 'charset': charset, |
|---|
| 175 | 'media': media |
|---|
| 176 | })) |
|---|
| 177 | |
|---|
| 178 | |
|---|
| 179 | def add_meta(http_equiv=None, name=None, content=None): |
|---|
| 180 | """Add a new meta element to the metadata of the current page.""" |
|---|
| 181 | local.page_metadata.append(('meta', { |
|---|
| 182 | 'http_equiv': http_equiv, |
|---|
| 183 | 'name': name, |
|---|
| 184 | 'content': content |
|---|
| 185 | })) |
|---|
| 186 | |
|---|
| 187 | |
|---|
| 188 | def add_script(src, type='text/javascript'): |
|---|
| 189 | """Load a script.""" |
|---|
| 190 | local.page_metadata.append(('script', { |
|---|
| 191 | 'src': src, |
|---|
| 192 | 'type': type |
|---|
| 193 | })) |
|---|
| 194 | |
|---|
| 195 | |
|---|
| 196 | def add_header_snippet(html): |
|---|
| 197 | """Add some HTML as header snippet.""" |
|---|
| 198 | local.page_metadata.append(('snippet', { |
|---|
| 199 | 'html': html |
|---|
| 200 | })) |
|---|
| 201 | |
|---|
| 202 | |
|---|
| 203 | def select_template(templates): |
|---|
| 204 | """Selects the first template from a list of templates that exists.""" |
|---|
| 205 | env = get_application().template_env |
|---|
| 206 | for template in templates: |
|---|
| 207 | if template is not None: |
|---|
| 208 | try: |
|---|
| 209 | return env.get_template(template) |
|---|
| 210 | except TemplateNotFound: |
|---|
| 211 | pass |
|---|
| 212 | raise TemplateNotFound('<multiple-choices>') |
|---|
| 213 | |
|---|
| 214 | |
|---|
| 215 | def render_template(template_name, _stream=False, **context): |
|---|
| 216 | """Renders a template. If `_stream` is ``True`` the return value will be |
|---|
| 217 | a Jinja template stream and not an unicode object. |
|---|
| 218 | This is used by `render_response`. If the `template_name` is a list of |
|---|
| 219 | strings the first template that exists is selected. |
|---|
| 220 | """ |
|---|
| 221 | if not isinstance(template_name, basestring): |
|---|
| 222 | tmpl = select_template(template_name) |
|---|
| 223 | template_name = tmpl.name |
|---|
| 224 | else: |
|---|
| 225 | tmpl = get_application().template_env.get_template(template_name) |
|---|
| 226 | |
|---|
| 227 | #! called right before a template is rendered, the return value is |
|---|
| 228 | #! ignored but the context can be modified in place. |
|---|
| 229 | emit_event('before-render-template', template_name, _stream, context) |
|---|
| 230 | |
|---|
| 231 | if _stream: |
|---|
| 232 | return tmpl.stream(context) |
|---|
| 233 | return tmpl.render(context) |
|---|
| 234 | |
|---|
| 235 | |
|---|
| 236 | def render_response(template_name, **context): |
|---|
| 237 | """Like render_template but returns a response. If `_stream` is ``True`` |
|---|
| 238 | the response returned uses the Jinja stream processing. This is useful |
|---|
| 239 | for pages with lazy generated content or huge output where you don't |
|---|
| 240 | want the users to wait until the calculation ended. Use streaming only |
|---|
| 241 | in those situations because it's usually slower than bunch processing. |
|---|
| 242 | """ |
|---|
| 243 | return Response(render_template(template_name, **context)) |
|---|
| 244 | |
|---|
| 245 | |
|---|
| 246 | class InternalError(UserException): |
|---|
| 247 | """Subclasses of this exception are used to signal internal errors that |
|---|
| 248 | should not happen, but may do if the configuration is garbage. If an |
|---|
| 249 | internal error is raised during request handling they are converted into |
|---|
| 250 | normal server errors for anonymous users (but not logged!!!), but if the |
|---|
| 251 | current user is an administrator, the error is displayed. |
|---|
| 252 | """ |
|---|
| 253 | |
|---|
| 254 | help_text = None |
|---|
| 255 | |
|---|
| 256 | |
|---|
| 257 | class Request(RequestBase): |
|---|
| 258 | """This class holds the incoming request data.""" |
|---|
| 259 | |
|---|
| 260 | def __init__(self, environ, app=None): |
|---|
| 261 | RequestBase.__init__(self, environ) |
|---|
| 262 | self.queries = [] |
|---|
| 263 | if app is None: |
|---|
| 264 | app = get_application() |
|---|
| 265 | self.app = app |
|---|
| 266 | |
|---|
| 267 | engine = self.app.database_engine |
|---|
| 268 | |
|---|
| 269 | # get the session and try to get the user object for this request. |
|---|
| 270 | from zine.models import User |
|---|
| 271 | user = None |
|---|
| 272 | cookie_name = app.cfg['session_cookie_name'] |
|---|
| 273 | session = SecureCookie.load_cookie(self, cookie_name, |
|---|
| 274 | app.secret_key) |
|---|
| 275 | user_id = session.get('uid') |
|---|
| 276 | if user_id: |
|---|
| 277 | user = User.query.options(db.eagerload('groups'), |
|---|
| 278 | db.eagerload('groups', '_privileges')) \ |
|---|
| 279 | .get(user_id) |
|---|
| 280 | if user is None: |
|---|
| 281 | user = User.query.get_nobody() |
|---|
| 282 | self.user = user |
|---|
| 283 | self.session = session |
|---|
| 284 | |
|---|
| 285 | @property |
|---|
| 286 | def is_behind_proxy(self): |
|---|
| 287 | """Are we behind a proxy?""" |
|---|
| 288 | return environ.get('ZINE_BEHIND_PROXY') == '1' |
|---|
| 289 | |
|---|
| 290 | def login(self, user, permanent=False): |
|---|
| 291 | """Log the given user in. Can be user_id, username or |
|---|
| 292 | a full blown user object. |
|---|
| 293 | """ |
|---|
| 294 | from zine.models import User |
|---|
| 295 | if isinstance(user, (int, long)): |
|---|
| 296 | user = User.query.get(user) |
|---|
| 297 | elif isinstance(user, basestring): |
|---|
| 298 | user = User.query.filter_by(username=user).first() |
|---|
| 299 | if user is None: |
|---|
| 300 | raise RuntimeError('User does not exist') |
|---|
| 301 | self.user = user |
|---|
| 302 | #! called after a user was logged in successfully |
|---|
| 303 | emit_event('after-user-login', user) |
|---|
| 304 | self.session['uid'] = user.id |
|---|
| 305 | self.session['lt'] = time() |
|---|
| 306 | if permanent: |
|---|
| 307 | self.session['pmt'] = True |
|---|
| 308 | |
|---|
| 309 | def logout(self): |
|---|
| 310 | """Log the current user out.""" |
|---|
| 311 | from zine.models import User |
|---|
| 312 | user = self.user |
|---|
| 313 | self.user = User.query.get_nobody() |
|---|
| 314 | self.session.clear() |
|---|
| 315 | #! called after a user was logged out and the session cleared. |
|---|
| 316 | emit_event('after-user-logout', user) |
|---|
| 317 | |
|---|
| 318 | |
|---|
| 319 | class Response(ResponseBase): |
|---|
| 320 | """This class holds the resonse data. The default charset is utf-8 |
|---|
| 321 | and the default mimetype ``'text/html'``. |
|---|
| 322 | """ |
|---|
| 323 | default_mimetype = 'text/html' |
|---|
| 324 | |
|---|
| 325 | |
|---|
| 326 | class EventManager(object): |
|---|
| 327 | """Helper class that handles event listeners and event emitting. |
|---|
| 328 | |
|---|
| 329 | This is *not* a public interface. Always use the `emit_event` or |
|---|
| 330 | `iter_listeners` functions to access it or the `connect_event` or |
|---|
| 331 | `disconnect_event` methods on the application. |
|---|
| 332 | """ |
|---|
| 333 | |
|---|
| 334 | def __init__(self, app): |
|---|
| 335 | self.app = app |
|---|
| 336 | self._listeners = {} |
|---|
| 337 | self._last_listener = 0 |
|---|
| 338 | |
|---|
| 339 | def connect(self, event, callback, position='after'): |
|---|
| 340 | """Connect a callback to an event.""" |
|---|
| 341 | assert position in ('before', 'after'), 'invalid position' |
|---|
| 342 | listener_id = self._last_listener |
|---|
| 343 | event = intern(event) |
|---|
| 344 | if event not in self._listeners: |
|---|
| 345 | self._listeners[event] = deque([callback]) |
|---|
| 346 | elif position == 'after': |
|---|
| 347 | self._listeners[event].append(callback) |
|---|
| 348 | elif position == 'before': |
|---|
| 349 | self._listeners[event].appendleft(callback) |
|---|
| 350 | self._last_listener += 1 |
|---|
| 351 | return listener_id |
|---|
| 352 | |
|---|
| 353 | def remove(self, listener_id): |
|---|
| 354 | """Remove a callback again.""" |
|---|
| 355 | for event in self._listeners: |
|---|
| 356 | try: |
|---|
| 357 | event.remove(listener_id) |
|---|
| 358 | except ValueError: |
|---|
| 359 | pass |
|---|
| 360 | |
|---|
| 361 | def iter(self, event): |
|---|
| 362 | """Return an iterator for all listeners of a given name.""" |
|---|
| 363 | if event not in self._listeners: |
|---|
| 364 | return iter(()) |
|---|
| 365 | return iter(self._listeners[event]) |
|---|
| 366 | |
|---|
| 367 | def template_emit(self, event, *args, **kwargs): |
|---|
| 368 | """Emits events for the template context.""" |
|---|
| 369 | results = [] |
|---|
| 370 | for f in self.iter(event): |
|---|
| 371 | rv = f(*args, **kwargs) |
|---|
| 372 | if rv is not None: |
|---|
| 373 | results.append(rv) |
|---|
| 374 | return TemplateEventResult(results) |
|---|
| 375 | |
|---|
| 376 | |
|---|
| 377 | class TemplateEventResult(list): |
|---|
| 378 | """A list subclass for results returned by the event listener that |
|---|
| 379 | concatenates the results if converted to string, otherwise it works |
|---|
| 380 | exactly like any other list. |
|---|
| 381 | """ |
|---|
| 382 | |
|---|
| 383 | def __init__(self, items): |
|---|
| 384 | list.__init__(self, items) |
|---|
| 385 | |
|---|
| 386 | def __unicode__(self): |
|---|
| 387 | return u''.join(map(unicode, self)) |
|---|
| 388 | |
|---|
| 389 | def __str__(self): |
|---|
| 390 | return unicode(self).encode('utf-8') |
|---|
| 391 | |
|---|
| 392 | |
|---|
| 393 | class Theme(object): |
|---|
| 394 | """Represents a theme and is created automatically by `add_theme`.""" |
|---|
| 395 | app = None |
|---|
| 396 | |
|---|
| 397 | def __init__(self, name, template_path, metadata=None, |
|---|
| 398 | settings=None, configuration_page=None): |
|---|
| 399 | BaseLoader.__init__(self) |
|---|
| 400 | self.name = name |
|---|
| 401 | self.template_path = template_path |
|---|
| 402 | self.metadata = metadata or {} |
|---|
| 403 | self._settings = settings or {} |
|---|
| 404 | self.configuration_page = configuration_page |
|---|
| 405 | |
|---|
| 406 | @property |
|---|
| 407 | def configurable(self): |
|---|
| 408 | return self.configuration_page is not None |
|---|
| 409 | |
|---|
| 410 | @property |
|---|
| 411 | def preview_url(self): |
|---|
| 412 | if self.metadata.get('preview'): |
|---|
| 413 | return shared_url(self.metadata['preview']) |
|---|
| 414 | |
|---|
| 415 | @property |
|---|
| 416 | def has_preview(self): |
|---|
| 417 | return bool(self.metadata.get('preview')) |
|---|
| 418 | |
|---|
| 419 | @property |
|---|
| 420 | def is_current(self): |
|---|
| 421 | return self.name == self.app.cfg['theme'] |
|---|
| 422 | |
|---|
| 423 | @property |
|---|
| 424 | def display_name(self): |
|---|
| 425 | return self.metadata.get('name') or self.name.title() |
|---|
| 426 | |
|---|
| 427 | @property |
|---|
| 428 | def description(self): |
|---|
| 429 | """Return the description of the theme.""" |
|---|
| 430 | return self.metadata.get('description', u'') |
|---|
| 431 | |
|---|
| 432 | @property |
|---|
| 433 | def has_author(self): |
|---|
| 434 | """Does the theme has an author at all?""" |
|---|
| 435 | return 'author' in self.metadata |
|---|
| 436 | |
|---|
| 437 | @property |
|---|
| 438 | def author_info(self): |
|---|
| 439 | """The author, mail and author URL of the theme.""" |
|---|
| 440 | from zine.utils.mail import split_email |
|---|
| 441 | return split_email(self.metadata.get('author', u'Nobody')) + \ |
|---|
| 442 | (self.metadata.get('author_url'),) |
|---|
| 443 | |
|---|
| 444 | @property |
|---|
| 445 | def html_author_info(self): |
|---|
| 446 | """Return the author info as html link.""" |
|---|
| 447 | name, email, url = self.author_info |
|---|
| 448 | if not url: |
|---|
| 449 | if not email: |
|---|
| 450 | return escape(name) |
|---|
| 451 | url = 'mailto:%s' % url_quote(email) |
|---|
| 452 | return u'<a href="%s">%s</a>' % ( |
|---|
| 453 | escape(url), |
|---|
| 454 | escape(name) |
|---|
| 455 | ) |
|---|
| 456 | |
|---|
| 457 | @property |
|---|
| 458 | def author(self): |
|---|
| 459 | """Return the author of the plugin.""" |
|---|
| 460 | x = self.author_info |
|---|
| 461 | return x[0] or x[1] |
|---|
| 462 | |
|---|
| 463 | @property |
|---|
| 464 | def author_email(self): |
|---|
| 465 | """Return the author email address of the theme.""" |
|---|
| 466 | return self.author_info[1] |
|---|
| 467 | |
|---|
| 468 | @property |
|---|
| 469 | def author_url(self): |
|---|
| 470 | """Return the URL of the author of the theme.""" |
|---|
| 471 | return self.author_info[2] |
|---|
| 472 | |
|---|
| 473 | @property |
|---|
| 474 | def contributors(self): |
|---|
| 475 | """The Contributors of the plugin.""" |
|---|
| 476 | from zine.utils.mail import split_email |
|---|
| 477 | data = self.metadata.get('contributors', '') |
|---|
| 478 | if not data: |
|---|
| 479 | return [] |
|---|
| 480 | return [split_email(c.strip()) for c in |
|---|
| 481 | self.metadata.get('contributors', '').split(',')] |
|---|
| 482 | |
|---|
| 483 | @property |
|---|
| 484 | def html_contributors_info(self): |
|---|
| 485 | from zine.utils.mail import check, is_valid_email |
|---|
| 486 | result = [] |
|---|
| 487 | for contributor in self.contributors: |
|---|
| 488 | name, contact = contributor |
|---|
| 489 | if not contact: |
|---|
| 490 | result.append(escape(name)) |
|---|
| 491 | else: |
|---|
| 492 | result.append('<a href="%s">%s</a>' % ( |
|---|
| 493 | escape(check(is_valid_email, contact) and |
|---|
| 494 | 'mailto:' + contact or contact), |
|---|
| 495 | escape(name) |
|---|
| 496 | )) |
|---|
| 497 | return u', '.join(result) |
|---|
| 498 | |
|---|
| 499 | @cached_property |
|---|
| 500 | def settings(self): |
|---|
| 501 | return ReadOnlyMultiMapping(self._settings, DEFAULT_THEME_SETTINGS) |
|---|
| 502 | |
|---|
| 503 | def get_url_values(self): |
|---|
| 504 | if self.configurable: |
|---|
| 505 | return self.name + '/configure', {} |
|---|
| 506 | raise TypeError('can\'t link to unconfigurable theme') |
|---|
| 507 | |
|---|
| 508 | def get_source(self, name): |
|---|
| 509 | parts = [x for x in name.split('/') if not x == '..'] |
|---|
| 510 | for fn in self.get_searchpath(): |
|---|
| 511 | fn = path.join(fn, *parts) |
|---|
| 512 | if path.exists(fn): |
|---|
| 513 | f = file(fn) |
|---|
| 514 | try: |
|---|
| 515 | contents = f.read().decode('utf-8') |
|---|
| 516 | finally: |
|---|
| 517 | f.close() |
|---|
| 518 | mtime = path.getmtime(fn) |
|---|
| 519 | return contents, fn, lambda: mtime == path.getmtime(fn) |
|---|
| 520 | |
|---|
| 521 | def get_overlay_path(self, template): |
|---|
| 522 | """Return the path to an overlay for a template.""" |
|---|
| 523 | return path.join(self.app.instance_folder, 'overlays', |
|---|
| 524 | self.name, template) |
|---|
| 525 | |
|---|
| 526 | def overlay_exists(self, template): |
|---|
| 527 | """Check if an overlay for a given template exists.""" |
|---|
| 528 | return path.exists(self.get_overlay_path(template)) |
|---|
| 529 | |
|---|
| 530 | def get_overlay(self, template): |
|---|
| 531 | """Return the source of an overlay.""" |
|---|
| 532 | f = file(self.get_overlay_path(template)) |
|---|
| 533 | try: |
|---|
| 534 | lines = f.read().decode('utf-8', 'ignore').splitlines() |
|---|
| 535 | finally: |
|---|
| 536 | f.close() |
|---|
| 537 | return u'\n'.join(lines) |
|---|
| 538 | |
|---|
| 539 | def set_overlay(self, template, data): |
|---|
| 540 | """Set an overlay.""" |
|---|
| 541 | filename = self.get_overlay_path(template) |
|---|
| 542 | try: |
|---|
| 543 | makedirs(path.dirname(filename)) |
|---|
| 544 | except OSError: |
|---|
| 545 | pass |
|---|
| 546 | data = u'\n'.join(data.splitlines()) |
|---|
| 547 | if not data.endswith('\n'): |
|---|
| 548 | data += '\n' |
|---|
| 549 | f = file(filename, 'w') |
|---|
| 550 | try: |
|---|
| 551 | f.write(data.encode('utf-8')) |
|---|
| 552 | finally: |
|---|
| 553 | f.close() |
|---|
| 554 | |
|---|
| 555 | def remove_overlay(self, template, silent=False): |
|---|
| 556 | """Remove an overlay.""" |
|---|
| 557 | try: |
|---|
| 558 | remove(self.get_overlay_path(template)) |
|---|
| 559 | except OSError: |
|---|
| 560 | if not silent: |
|---|
| 561 | raise |
|---|
| 562 | |
|---|
| 563 | def get_searchpath(self): |
|---|
| 564 | """Get the searchpath for this theme including plugins and |
|---|
| 565 | all other template locations. |
|---|
| 566 | """ |
|---|
| 567 | # before loading the normal template paths we check for overlays |
|---|
| 568 | # in the instance overlay folder |
|---|
| 569 | searchpath = [path.join(self.app.instance_folder, 'overlays', |
|---|
| 570 | self.name)] |
|---|
| 571 | |
|---|
| 572 | # if we have a real theme add the template path to the searchpath |
|---|
| 573 | # on the highest position |
|---|
| 574 | if self.name != 'default': |
|---|
| 575 | searchpath.append(self.template_path) |
|---|
| 576 | |
|---|
| 577 | # add the template locations of the plugins |
|---|
| 578 | searchpath.extend(self.app._template_searchpath) |
|---|
| 579 | |
|---|
| 580 | # now after the plugin searchpaths add the builtin one |
|---|
| 581 | searchpath.append(BUILTIN_TEMPLATE_PATH) |
|---|
| 582 | |
|---|
| 583 | return searchpath |
|---|
| 584 | |
|---|
| 585 | def list_templates(self): |
|---|
| 586 | """Return a sorted list of all templates.""" |
|---|
| 587 | templates = set() |
|---|
| 588 | for p in self.get_searchpath(): |
|---|
| 589 | for dirpath, dirnames, filenames in walk(p): |
|---|
| 590 | dirpath = dirpath[len(p) + 1:] |
|---|
| 591 | if dirpath.startswith('.'): |
|---|
| 592 | continue |
|---|
| 593 | for filename in filenames: |
|---|
| 594 | if filename.startswith('.'): |
|---|
| 595 | continue |
|---|
| 596 | templates.add(path.join(dirpath, filename). |
|---|
| 597 | replace(path.sep, '/')) |
|---|
| 598 | return sorted(templates) |
|---|
| 599 | |
|---|
| 600 | def format_datetime(self, datetime=None, format=None): |
|---|
| 601 | """Datetime formatting for the template. the (`datetimeformat` |
|---|
| 602 | filter) |
|---|
| 603 | """ |
|---|
| 604 | format = self._get_babel_format('datetime', format) |
|---|
| 605 | return i18n.format_datetime(datetime, format) |
|---|
| 606 | |
|---|
| 607 | def format_date(self, date=None, format=None): |
|---|
| 608 | """Date formatting for the template. (the `dateformat` filter)""" |
|---|
| 609 | format = self._get_babel_format('date', format) |
|---|
| 610 | return i18n.format_date(date, format) |
|---|
| 611 | |
|---|
| 612 | def _get_babel_format(self, key, format): |
|---|
| 613 | """A small helper for the datetime formatting functions.""" |
|---|
| 614 | if format is None: |
|---|
| 615 | format = self.settings['date.%s_format.default' % key] |
|---|
| 616 | if format in ('short', 'medium', 'full', 'long'): |
|---|
| 617 | rv = self.settings['date.%s_format.%s' % (key, format)] |
|---|
| 618 | if rv is not None: |
|---|
| 619 | format = rv |
|---|
| 620 | return format |
|---|
| 621 | |
|---|
| 622 | |
|---|
| 623 | class ThemeLoader(BaseLoader): |
|---|
| 624 | """Forwards theme lookups to the current active theme.""" |
|---|
| 625 | |
|---|
| 626 | def __init__(self, app): |
|---|
| 627 | BaseLoader.__init__(self) |
|---|
| 628 | self.app = app |
|---|
| 629 | |
|---|
| 630 | def get_source(self, environment, name): |
|---|
| 631 | rv = self.app.theme.get_source(name) |
|---|
| 632 | if rv is None: |
|---|
| 633 | raise TemplateNotFound(name) |
|---|
| 634 | return rv |
|---|
| 635 | |
|---|
| 636 | |
|---|
| 637 | class Zine(object): |
|---|
| 638 | """The central application object. |
|---|
| 639 | |
|---|
| 640 | Even though the :class:`Zine` class is a regular Python class, you can't |
|---|
| 641 | create instances by using the regular constructor. The only documented way |
|---|
| 642 | to create this class is the :func:`zine._core.setup` function or by using |
|---|
| 643 | one of the dispatchers created by :func:`zine._core.get_wsgi_app`. |
|---|
| 644 | """ |
|---|
| 645 | |
|---|
| 646 | _setup_only = [] |
|---|
| 647 | def setuponly(f, container=_setup_only): |
|---|
| 648 | """Mark a function as "setup only". After the setup those |
|---|
| 649 | functions will be replaced with a dummy function that raises |
|---|
| 650 | an exception.""" |
|---|
| 651 | container.append(f.__name__) |
|---|
| 652 | f.__doc__ = (getdoc(f) or '') + '\n\n*This function can only be ' \ |
|---|
| 653 | 'called during application setup*' |
|---|
| 654 | return f |
|---|
| 655 | |
|---|
| 656 | def __init__(self, instance_folder): |
|---|
| 657 | # this check ensures that only setup() can create Zine instances |
|---|
| 658 | if get_application() is not self: |
|---|
| 659 | raise TypeError('cannot create %r instances. use the ' |
|---|
| 660 | 'zine._core.setup() factory function.' % |
|---|
| 661 | self.__class__.__name__) |
|---|
| 662 | self.instance_folder = path.abspath(instance_folder) |
|---|
| 663 | self.upgrade_lockfile = path.join(instance_folder, |
|---|
| 664 | '.upgrade_in_progress') |
|---|
| 665 | |
|---|
| 666 | # create the event manager, this is the first thing we have to |
|---|
| 667 | # do because it could happen that events are sent during setup |
|---|
| 668 | self.initialized = False |
|---|
| 669 | self._event_manager = EventManager(self) |
|---|
| 670 | |
|---|
| 671 | # and instanciate the configuration. this won't fail, |
|---|
| 672 | # even if the database is not connected. |
|---|
| 673 | from zine.config import Configuration |
|---|
| 674 | self.cfg = Configuration(path.join(instance_folder, 'zine.ini')) |
|---|
| 675 | if not self.cfg.exists: |
|---|
| 676 | raise _core.InstanceNotInitialized() |
|---|
| 677 | |
|---|
| 678 | # and hook in the logger |
|---|
| 679 | self.log = log.Logger(path.join(instance_folder, self.cfg['log_file']), |
|---|
| 680 | self.cfg['log_level']) |
|---|
| 681 | |
|---|
| 682 | # the iid of the application |
|---|
| 683 | self.iid = self.cfg['iid'].encode('utf-8') |
|---|
| 684 | if not self.iid: |
|---|
| 685 | self.iid = '%x' % id(self) |
|---|
| 686 | |
|---|
| 687 | # connect to the database |
|---|
| 688 | self.database_engine = db.create_engine(self.cfg['database_uri'], |
|---|
| 689 | self.instance_folder, |
|---|
| 690 | self.cfg['database_debug']) |
|---|
| 691 | |
|---|
| 692 | # now setup the cache system |
|---|
| 693 | self.cache = get_cache(self) |
|---|
| 694 | |
|---|
| 695 | # setup core package urls and shared stuff |
|---|
| 696 | import zine |
|---|
| 697 | from zine.urls import make_urls |
|---|
| 698 | from zine.views import all_views, content_type_handlers, \ |
|---|
| 699 | admin_content_type_handlers, absolute_url_handlers |
|---|
| 700 | from zine.services import all_services |
|---|
| 701 | from zine.parsers import all_parsers |
|---|
| 702 | self.views = all_views.copy() |
|---|
| 703 | self.content_type_handlers = content_type_handlers.copy() |
|---|
| 704 | self.admin_content_type_handlers = admin_content_type_handlers.copy() |
|---|
| 705 | self.parsers = dict((k, v(self)) for k, v in all_parsers.iteritems()) |
|---|
| 706 | self.markup_extensions = [] |
|---|
| 707 | self._url_rules = make_urls(self) |
|---|
| 708 | self._absolute_url_handlers = absolute_url_handlers[:] |
|---|
| 709 | self._services = all_services.copy() |
|---|
| 710 | self._shared_exports = {} |
|---|
| 711 | self._template_globals = {} |
|---|
| 712 | self._template_filters = {} |
|---|
| 713 | self._template_tests = {} |
|---|
| 714 | self._template_searchpath = [] |
|---|
| 715 | |
|---|
| 716 | # initialize i18n/l10n system |
|---|
| 717 | self.locale = Locale(self.cfg['language']) |
|---|
| 718 | self.translations = i18n.load_core_translations(self.locale) |
|---|
| 719 | |
|---|
| 720 | # init themes |
|---|
| 721 | _ = i18n.gettext |
|---|
| 722 | default_theme = Theme('default', BUILTIN_TEMPLATE_PATH, { |
|---|
| 723 | 'name': _(u'Default Theme'), |
|---|
| 724 | 'description': _(u'Simple default theme that doesn\'t ' |
|---|
| 725 | 'contain any style information.'), |
|---|
| 726 | 'preview': 'core::default_preview.png' |
|---|
| 727 | }) |
|---|
| 728 | default_theme.app = self |
|---|
| 729 | self.themes = {'default': default_theme} |
|---|
| 730 | |
|---|
| 731 | self.apis = {} |
|---|
| 732 | self.importers = {} |
|---|
| 733 | self.feed_importer_extensions = [] |
|---|
| 734 | |
|---|
| 735 | # the notification manager |
|---|
| 736 | from zine.notifications import NotificationManager, \ |
|---|
| 737 | DEFAULT_NOTIFICATION_SYSTEMS, DEFAULT_NOTIFICATION_TYPES |
|---|
| 738 | |
|---|
| 739 | self.notification_manager = NotificationManager() |
|---|
| 740 | for system in DEFAULT_NOTIFICATION_SYSTEMS: |
|---|
| 741 | self.add_notification_system(system) |
|---|
| 742 | self.notification_types = DEFAULT_NOTIFICATION_TYPES.copy() |
|---|
| 743 | |
|---|
| 744 | # register the pingback API. |
|---|
| 745 | from zine import pingback |
|---|
| 746 | self.add_api('pingback', True, pingback.service) |
|---|
| 747 | self.pingback_endpoints = pingback.endpoints.copy() |
|---|
| 748 | self.pingback_url_handlers = pingback.url_handlers[:] |
|---|
| 749 | |
|---|
| 750 | # register our builtin importers |
|---|
| 751 | from zine.importers import importers |
|---|
| 752 | for importer in importers: |
|---|
| 753 | self.add_importer(importer) |
|---|
| 754 | |
|---|
| 755 | # and the feed importer extensions |
|---|
| 756 | from zine.importers.feed import extensions |
|---|
| 757 | for extension in extensions: |
|---|
| 758 | self.add_feed_importer_extension(extension) |
|---|
| 759 | |
|---|
| 760 | # register the default privileges |
|---|
| 761 | from zine.privileges import DEFAULT_PRIVILEGES, CONTENT_TYPE_PRIVILEGES |
|---|
| 762 | self.privileges = DEFAULT_PRIVILEGES.copy() |
|---|
| 763 | self.content_type_privileges = CONTENT_TYPE_PRIVILEGES.copy() |
|---|
| 764 | |
|---|
| 765 | # insert list of widgets |
|---|
| 766 | from zine.widgets import all_widgets |
|---|
| 767 | self.widgets = dict((x.name, x) for x in all_widgets) |
|---|
| 768 | |
|---|
| 769 | # load plugins |
|---|
| 770 | from zine.pluginsystem import find_plugins, set_plugin_searchpath |
|---|
| 771 | self.plugin_folder = path.join(instance_folder, 'plugins') |
|---|
| 772 | self.plugin_searchpath = [self.plugin_folder] |
|---|
| 773 | for folder in self.cfg['plugin_searchpath']: |
|---|
| 774 | folder = folder.strip() |
|---|
| 775 | if folder: |
|---|
| 776 | self.plugin_searchpath.append( |
|---|
| 777 | path.join(self.instance_folder, folder)) |
|---|
| 778 | self.plugin_searchpath.append(BUILTIN_PLUGIN_FOLDER) |
|---|
| 779 | set_plugin_searchpath(self.plugin_searchpath) |
|---|
| 780 | |
|---|
| 781 | # load the plugins |
|---|
| 782 | self.plugins = {} |
|---|
| 783 | for plugin in find_plugins(self): |
|---|
| 784 | if plugin.active: |
|---|
| 785 | plugin.setup() |
|---|
| 786 | self.translations.merge(plugin.translations) |
|---|
| 787 | self.plugins[plugin.name] = plugin |
|---|
| 788 | |
|---|
| 789 | # set the active theme based on the config. |
|---|
| 790 | theme = self.cfg['theme'] |
|---|
| 791 | if theme not in self.themes: |
|---|
| 792 | log.warning(_(u'Theme “%s” is no longer available, falling back ' |
|---|
| 793 | u'to default theme.') % theme, 'core') |
|---|
| 794 | theme = 'default' |
|---|
| 795 | self.cfg.change_single('theme', theme) |
|---|
| 796 | self.theme = self.themes[theme] |
|---|
| 797 | |
|---|
| 798 | # init the template system with the core stuff |
|---|
| 799 | from zine import models |
|---|
| 800 | env = Environment(loader=ThemeLoader(self), |
|---|
| 801 | extensions=['jinja2.ext.i18n']) |
|---|
| 802 | env.globals.update( |
|---|
| 803 | cfg=self.cfg, |
|---|
| 804 | theme=self.theme, |
|---|
| 805 | h=htmlhelpers, |
|---|
| 806 | url_for=url_for, |
|---|
| 807 | shared_url=shared_url, |
|---|
| 808 | emit_event=self._event_manager.template_emit, |
|---|
| 809 | request=local('request'), |
|---|
| 810 | render_widgets=lambda: render_template('_widgets.html'), |
|---|
| 811 | get_page_metadata=self.get_page_metadata, |
|---|
| 812 | widgets=self.widgets, |
|---|
| 813 | zine={ |
|---|
| 814 | 'version': zine.__version__, |
|---|
| 815 | 'copyright': _(u'Copyright %(years)s by the Zine Team') |
|---|
| 816 | % {'years': '2008-2009'} |
|---|
| 817 | } |
|---|
| 818 | ) |
|---|
| 819 | |
|---|
| 820 | env.filters.update( |
|---|
| 821 | json=dump_json, |
|---|
| 822 | datetimeformat=self.theme.format_datetime, |
|---|
| 823 | dateformat=self.theme.format_date, |
|---|
| 824 | monthformat=i18n.format_month, |
|---|
| 825 | timedeltaformat=i18n.format_timedelta |
|---|
| 826 | ) |
|---|
| 827 | |
|---|
| 828 | env.install_gettext_translations(self.translations) |
|---|
| 829 | |
|---|
| 830 | # set up plugin template extensions |
|---|
| 831 | env.globals.update(self._template_globals) |
|---|
| 832 | env.filters.update(self._template_filters) |
|---|
| 833 | env.tests.update(self._template_tests) |
|---|
| 834 | del self._template_globals, self._template_filters, \ |
|---|
| 835 | self._template_tests |
|---|
| 836 | self.template_env = env |
|---|
| 837 | |
|---|
| 838 | # now add the middleware for static file serving |
|---|
| 839 | self.add_shared_exports('core', SHARED_DATA) |
|---|
| 840 | self.add_middleware(SharedDataMiddleware, self._shared_exports) |
|---|
| 841 | |
|---|
| 842 | # set up the urls |
|---|
| 843 | self.url_map = routing.Map(self._url_rules) |
|---|
| 844 | del self._url_rules |
|---|
| 845 | |
|---|
| 846 | # and create a url adapter |
|---|
| 847 | scheme, netloc, script_name = urlparse(self.cfg['blog_url'])[:3] |
|---|
| 848 | self.url_adapter = self.url_map.bind(netloc, script_name, |
|---|
| 849 | url_scheme=scheme) |
|---|
| 850 | |
|---|
| 851 | # mark the app as finished and override the setup functions |
|---|
| 852 | def _error(*args, **kwargs): |
|---|
| 853 | raise RuntimeError('Cannot register new callbacks after ' |
|---|
| 854 | 'application setup phase.') |
|---|
| 855 | self.__dict__.update(dict.fromkeys(self._setup_only, _error)) |
|---|
| 856 | |
|---|
| 857 | self.cfg.config_vars['default_parser'].choices = \ |
|---|
| 858 | self.cfg.config_vars['comment_parser'].choices = \ |
|---|
| 859 | self.list_parsers() |
|---|
| 860 | |
|---|
| 861 | # register Zine's upgrade repository |
|---|
| 862 | from zine.upgrades import REPOSITORY_PATH |
|---|
| 863 | self.register_upgrade_repository('Zine', REPOSITORY_PATH) |
|---|
| 864 | # allow plugins to register their upgrade repositories |
|---|
| 865 | emit_event('register-upgrade-repository') |
|---|
| 866 | |
|---|
| 867 | self.initialized = True |
|---|
| 868 | |
|---|
| 869 | #! called after the application and all plugins are initialized |
|---|
| 870 | emit_event('application-setup-done') |
|---|
| 871 | |
|---|
| 872 | def register_upgrade_repository(self, repo_id, repo_path): |
|---|
| 873 | """This function is responsible for adding upgrade repositories to the |
|---|
| 874 | database. |
|---|
| 875 | |
|---|
| 876 | repo_id can be either a string or a Plugin instance, in which case the |
|---|
| 877 | plugin name is used as the repository ID. |
|---|
| 878 | """ |
|---|
| 879 | from zine.models import SchemaVersion |
|---|
| 880 | from zine.pluginsystem import Plugin |
|---|
| 881 | from zine.upgrades.customisation import Repository |
|---|
| 882 | if isinstance(repo_id, Plugin): |
|---|
| 883 | repo_id = repo_id.metadata.get('name') |
|---|
| 884 | repo_path = path.abspath(repo_path) |
|---|
| 885 | try: |
|---|
| 886 | sv = SchemaVersion.query.filter_by(repository_id=repo_id).first() |
|---|
| 887 | if not sv: |
|---|
| 888 | # this always starts with version 0 |
|---|
| 889 | db.session.add(SchemaVersion(Repository(repo_path, repo_id))) |
|---|
| 890 | db.session.commit() |
|---|
| 891 | except (SQLAlchemyError, AttributeError): |
|---|
| 892 | # the schema_versions table does not yet exist, let's create it |
|---|
| 893 | db.session.rollback() |
|---|
| 894 | from zine.database import metadata, schema_versions |
|---|
| 895 | metadata.bind = self.database_engine |
|---|
| 896 | if not schema_versions.exists(): |
|---|
| 897 | schema_versions.create(self.database_engine) |
|---|
| 898 | db.session.add(SchemaVersion(Repository(repo_path, repo_id))) |
|---|
| 899 | db.session.commit() |
|---|
| 900 | |
|---|
| 901 | def check_if_upgrade_required(self): |
|---|
| 902 | """Check if all registered schema versions are the latest. |
|---|
| 903 | |
|---|
| 904 | If an upgrade is required, this will raise |
|---|
| 905 | zine._core.InstanceUpgradeRequired. |
|---|
| 906 | """ |
|---|
| 907 | from zine.models import SchemaVersion |
|---|
| 908 | from zine.upgrades.customisation import Repository |
|---|
| 909 | |
|---|
| 910 | to_upgrade = [] |
|---|
| 911 | |
|---|
| 912 | for sv in SchemaVersion.query.all(): |
|---|
| 913 | repository = Repository(sv.repository_path, sv.repository_id) |
|---|
| 914 | try: |
|---|
| 915 | self.repository_has_upgrade(repository, sv) |
|---|
| 916 | except _core.InstanceUpgradeRequired: |
|---|
| 917 | to_upgrade.append(sv.repository_id) |
|---|
| 918 | |
|---|
| 919 | if to_upgrade: |
|---|
| 920 | # set Zine in maintenance mode |
|---|
| 921 | cfg = self.cfg.edit() |
|---|
| 922 | cfg['maintenance_mode'] = True |
|---|
| 923 | cfg.commit() |
|---|
| 924 | raise _core.InstanceUpgradeRequired(to_upgrade) |
|---|
| 925 | |
|---|
| 926 | # we got here, let's check for a bad upgrade lockfile left behind |
|---|
| 927 | if path.isfile(self.upgrade_lockfile): |
|---|
| 928 | remove(self.upgrade_lockfile) |
|---|
| 929 | |
|---|
| 930 | def repository_has_upgrade(self, repository, schema_version): |
|---|
| 931 | """Check for available upgrades in one repository.""" |
|---|
| 932 | from zine.models import SchemaVersion |
|---|
| 933 | try: |
|---|
| 934 | if schema_version.version < repository.latest: |
|---|
| 935 | raise _core.InstanceUpgradeRequired() |
|---|
| 936 | except (SQLAlchemyError, AttributeError): |
|---|
| 937 | self.log.error('schema_versions table missing while checking ' |
|---|
| 938 | 'for upgrades?') |
|---|
| 939 | # the schema_versions table does not yet exist, let's create it |
|---|
| 940 | # XXX can this happen at all? |
|---|
| 941 | db.session.rollback() |
|---|
| 942 | from zine.database import metadata, schema_versions |
|---|
| 943 | metadata.bind = self.database_engine |
|---|
| 944 | if not schema_versions.exists(): |
|---|
| 945 | schema_versions.create(self.database_engine) |
|---|
| 946 | db.session.add(SchemaVersion(repository)) |
|---|
| 947 | db.session.commit() |
|---|
| 948 | raise _core.InstanceUpgradeRequired() |
|---|
| 949 | |
|---|
| 950 | @property |
|---|
| 951 | def wants_reload(self): |
|---|
| 952 | """True if the application requires a reload. This is `True` if |
|---|
| 953 | the config was changed on the file system. A dispatcher checks this |
|---|
| 954 | value every request and automatically unloads and reloads the |
|---|
| 955 | application if necessary. |
|---|
| 956 | """ |
|---|
| 957 | return self.cfg.changed_external |
|---|
| 958 | |
|---|
| 959 | @property |
|---|
| 960 | def secret_key(self): |
|---|
| 961 | """Returns the secret key for the instance (binary!)""" |
|---|
| 962 | return self.cfg['secret_key'].encode('utf-8') |
|---|
| 963 | |
|---|
| 964 | @setuponly |
|---|
| 965 | def add_template_filter(self, name, callback): |
|---|
| 966 | """Add a Jinja2 template filter.""" |
|---|
| 967 | self._template_filters[name] = callback |
|---|
| 968 | |
|---|
| 969 | @setuponly |
|---|
| 970 | def add_template_test(self, name, callback): |
|---|
| 971 | """Add a Jinja2 template test.""" |
|---|
| 972 | self._template_tests[name] = callback |
|---|
| 973 | |
|---|
| 974 | @setuponly |
|---|
| 975 | def add_template_global(self, name, value): |
|---|
| 976 | """Add a template global. Object's added that way are available in |
|---|
| 977 | the global template namespace. |
|---|
| 978 | """ |
|---|
| 979 | self._template_globals[name] = value |
|---|
| 980 | |
|---|
| 981 | @setuponly |
|---|
| 982 | def add_template_searchpath(self, path): |
|---|
| 983 | """Add a new template searchpath to the application. This searchpath |
|---|
| 984 | is queried *after* the themes but *before* the builtin templates are |
|---|
| 985 | looked up. |
|---|
| 986 | """ |
|---|
| 987 | self._template_searchpath.append(path) |
|---|
| 988 | |
|---|
| 989 | @setuponly |
|---|
| 990 | def add_api(self, name, preferred, callback, blog_id=1, url_key=None): |
|---|
| 991 | """Add a new API to the blog. The newly added API is available at |
|---|
| 992 | ``/_services/<name>`` and automatically exported in the RSD file. |
|---|
| 993 | The `blog_id` is an unused oddity of the RSD file, preferred an |
|---|
| 994 | indicator if this API is preferred or not. |
|---|
| 995 | The callback is called for all requests to the service URL. |
|---|
| 996 | """ |
|---|
| 997 | endpoint = 'services/' + name |
|---|
| 998 | self.apis[name] = (blog_id, preferred, endpoint) |
|---|
| 999 | if url_key is None: |
|---|
| 1000 | url_key = name.lower() |
|---|
| 1001 | url = '/_services/' + url_key |
|---|
| 1002 | self.add_url_rule(url, endpoint=endpoint) |
|---|
| 1003 | self.add_view(endpoint, callback) |
|---|
| 1004 | return url |
|---|
| 1005 | |
|---|
| 1006 | @setuponly |
|---|
| 1007 | def add_importer(self, importer): |
|---|
| 1008 | """Register an importer. For more information about importers |
|---|
| 1009 | see the :mod:`zine.importers`. |
|---|
| 1010 | """ |
|---|
| 1011 | importer = importer(self) |
|---|
| 1012 | endpoint = 'import/' + importer.name |
|---|
| 1013 | self.importers[importer.name] = importer |
|---|
| 1014 | self.add_url_rule('/maintenance/import/' + importer.name, |
|---|
| 1015 | prefix='admin', endpoint=endpoint) |
|---|
| 1016 | self.add_view(endpoint, importer) |
|---|
| 1017 | |
|---|
| 1018 | @setuponly |
|---|
| 1019 | def add_feed_importer_extension(self, extension): |
|---|
| 1020 | """Registers a feed importer extension. This is for example used |
|---|
| 1021 | for to implement the ZXA importing in the feed importer. |
|---|
| 1022 | |
|---|
| 1023 | All blogs that provide feeds that extend Atom (and in the future |
|---|
| 1024 | RSS) should be imported by registering an importer here. |
|---|
| 1025 | """ |
|---|
| 1026 | self.feed_importer_extensions.append(extension) |
|---|
| 1027 | |
|---|
| 1028 | @setuponly |
|---|
| 1029 | def add_pingback_endpoint(self, endpoint, callback): |
|---|
| 1030 | """Notify the pingback service that the endpoint provided supports |
|---|
| 1031 | pingbacks. The second parameter must be the callback function |
|---|
| 1032 | called on pingbacks. |
|---|
| 1033 | """ |
|---|
| 1034 | self.pingback_endpoints[endpoint] = callback |
|---|
| 1035 | |
|---|
| 1036 | @setuponly |
|---|
| 1037 | def add_pingback_url_handler(self, callback): |
|---|
| 1038 | """Register a new URL handler as a pingback URL handler. URL |
|---|
| 1039 | handlers for pingbacks are looked up after no endpoint could be |
|---|
| 1040 | found. |
|---|
| 1041 | """ |
|---|
| 1042 | self.pingback_url_handlers.append(callback) |
|---|
| 1043 | |
|---|
| 1044 | @setuponly |
|---|
| 1045 | def add_theme(self, name, template_path=None, metadata=None, |
|---|
| 1046 | settings=None, configuration_page=None): |
|---|
| 1047 | """Add a theme. You have to provide the shortname for the theme |
|---|
| 1048 | which will be used in the admin panel etc. Then you have to provide |
|---|
| 1049 | the path for the templates. Usually this path is relative to the |
|---|
| 1050 | directory of the plugin's `__file__`. |
|---|
| 1051 | |
|---|
| 1052 | The metadata can be ommited but in that case some information in |
|---|
| 1053 | the admin panel is not available. |
|---|
| 1054 | |
|---|
| 1055 | Alternatively a custom :class:`Theme` object can be passed to this |
|---|
| 1056 | function as only argument. This makes it possible to register |
|---|
| 1057 | custom theme subclasses too. |
|---|
| 1058 | """ |
|---|
| 1059 | if isinstance(name, Theme): |
|---|
| 1060 | if template_path is not metadata is not settings \ |
|---|
| 1061 | is not configuration_page is not None: |
|---|
| 1062 | raise TypeError('if a theme instance is provided extra ' |
|---|
| 1063 | 'arguments must be ommited or None.') |
|---|
| 1064 | theme = name |
|---|
| 1065 | else: |
|---|
| 1066 | theme = Theme(name, template_path, metadata, |
|---|
| 1067 | settings, configuration_page) |
|---|
| 1068 | if theme.app is not None: |
|---|
| 1069 | raise TypeError('theme is already registered to an application.') |
|---|
| 1070 | theme.app = self |
|---|
| 1071 | self.themes[theme.name] = theme |
|---|
| 1072 | |
|---|
| 1073 | @setuponly |
|---|
| 1074 | def add_shared_exports(self, name, path): |
|---|
| 1075 | """Add a shared export for name that points to a given path and |
|---|
| 1076 | creates an url rule for <name>/shared that takes a filename |
|---|
| 1077 | parameter. A shared export is some sort of static data from a |
|---|
| 1078 | plugin. Per default Zine will shared the data on it's own but |
|---|
| 1079 | in the future it would be possible to generate an Apache/nginx |
|---|
| 1080 | config on the fly for the static data. |
|---|
| 1081 | |
|---|
| 1082 | The static data is available at `/_shared/<name>` and points to |
|---|
| 1083 | `path` on the file system. This also generates a URL rule named |
|---|
| 1084 | `<name>/shared` that accepts a `filename` parameter. This can be |
|---|
| 1085 | used for URL generation. |
|---|
| 1086 | """ |
|---|
| 1087 | self._shared_exports['/_shared/' + name] = path |
|---|
| 1088 | self.add_url_rule('/_shared/%s/<string:filename>' % name, |
|---|
| 1089 | endpoint=name + '/shared', build_only=True) |
|---|
| 1090 | |
|---|
| 1091 | @setuponly |
|---|
| 1092 | def add_middleware(self, middleware_factory, *args, **kwargs): |
|---|
| 1093 | """Add a middleware to the application. The `middleware_factory` |
|---|
| 1094 | is a callable that is called with the active WSGI application as |
|---|
| 1095 | first argument, `args` as extra positional arguments and `kwargs` |
|---|
| 1096 | as keyword arguments. |
|---|
| 1097 | |
|---|
| 1098 | The newly applied middleware wraps an internal WSGI application. |
|---|
| 1099 | """ |
|---|
| 1100 | self.dispatch_wsgi = middleware_factory(self.dispatch_wsgi, |
|---|
| 1101 | *args, **kwargs) |
|---|
| 1102 | |
|---|
| 1103 | @setuponly |
|---|
| 1104 | def add_config_var(self, key, field): |
|---|
| 1105 | """Add a configuration variable to the application. The config |
|---|
| 1106 | variable should be named ``<plugin_name>/<variable_name>``. The |
|---|
| 1107 | `variable_name` itself must not contain another slash. Variables |
|---|
| 1108 | that are not prefixed are reserved for Zine' internal usage. |
|---|
| 1109 | The `field` is an instance of a field class from zine.utils.forms |
|---|
| 1110 | that is used to validate the variable. It has to contain the default |
|---|
| 1111 | value for that variable. |
|---|
| 1112 | |
|---|
| 1113 | Example usage:: |
|---|
| 1114 | |
|---|
| 1115 | app.add_config_var('my_plugin/my_var', BooleanField(default=True)) |
|---|
| 1116 | """ |
|---|
| 1117 | if key.count('/') > 1: |
|---|
| 1118 | raise ValueError('key might not have more than one slash') |
|---|
| 1119 | self.cfg.config_vars[key] = field |
|---|
| 1120 | |
|---|
| 1121 | @setuponly |
|---|
| 1122 | def add_url_rule(self, rule, **kwargs): |
|---|
| 1123 | """Add a new URL rule to the url map. This function accepts the same |
|---|
| 1124 | arguments as a werkzeug routing rule. Additionally a `prefix` |
|---|
| 1125 | parameter is accepted that can be used to add the common prefixes |
|---|
| 1126 | based on the configuration. Basically the following two calls |
|---|
| 1127 | do exactly the same:: |
|---|
| 1128 | |
|---|
| 1129 | app.add_url_rule('/foo', prefix='admin', ...) |
|---|
| 1130 | app.add_url_rule(app.cfg['admin_url_prefix'] + '/foo', ...) |
|---|
| 1131 | |
|---|
| 1132 | It also takes a `view` keyword argument that, if given registers |
|---|
| 1133 | a view for the url view:: |
|---|
| 1134 | |
|---|
| 1135 | app.add_url_rule(..., endpoint='bar', view=bar) |
|---|
| 1136 | |
|---|
| 1137 | is equivalent to:: |
|---|
| 1138 | |
|---|
| 1139 | app.add_url_rule(..., endpoint='bar') |
|---|
| 1140 | app.add_view('bar', bar) |
|---|
| 1141 | """ |
|---|
| 1142 | prefix = kwargs.pop('prefix', None) |
|---|
| 1143 | if prefix is not None: |
|---|
| 1144 | rule = self.cfg[prefix + '_url_prefix'] + rule |
|---|
| 1145 | view = kwargs.pop('view', None) |
|---|
| 1146 | self._url_rules.append(routing.Rule(rule, **kwargs)) |
|---|
| 1147 | if view is not None: |
|---|
| 1148 | self.views[kwargs['endpoint']] = view |
|---|
| 1149 | |
|---|
| 1150 | @setuponly |
|---|
| 1151 | def add_absolute_url(self, handler): |
|---|
| 1152 | """Adds a new callback as handler for absolute URLs. If the normal |
|---|
| 1153 | request handling was unable to find a proper response for the request |
|---|
| 1154 | the handler is called with the current request as argument and can |
|---|
| 1155 | return a response that is then used as normal response. |
|---|
| 1156 | |
|---|
| 1157 | If a handler doesn't want to handle the response it may raise a |
|---|
| 1158 | `NotFound` exception or return `None`. |
|---|
| 1159 | |
|---|
| 1160 | This is for example used to implement the pages support in Zine. |
|---|
| 1161 | """ |
|---|
| 1162 | self._absolute_url_handlers.append(handler) |
|---|
| 1163 | |
|---|
| 1164 | @setuponly |
|---|
| 1165 | def add_view(self, endpoint, callback): |
|---|
| 1166 | """Add a callback as view. The endpoint is the endpoint for the URL |
|---|
| 1167 | rule and has to be equivalent to the endpoint passed to |
|---|
| 1168 | :meth:`add_url_rule`. |
|---|
| 1169 | """ |
|---|
| 1170 | self.views[endpoint] = callback |
|---|
| 1171 | |
|---|
| 1172 | @setuponly |
|---|
| 1173 | def add_content_type(self, content_type, callback, admin_callbacks=None, |
|---|
| 1174 | create_privilege=None, edit_own_privilege=None, |
|---|
| 1175 | edit_other_privilege=None): |
|---|
| 1176 | """Register a view handler for a content type.""" |
|---|
| 1177 | self.content_type_handlers[content_type] = callback |
|---|
| 1178 | if admin_callbacks is not None: |
|---|
| 1179 | self.admin_content_type_handlers[content_type] = admin_callbacks |
|---|
| 1180 | self.content_type_privileges[content_type] = ( |
|---|
| 1181 | create_privilege, |
|---|
| 1182 | edit_own_privilege, |
|---|
| 1183 | edit_other_privilege |
|---|
| 1184 | ) |
|---|
| 1185 | |
|---|
| 1186 | @setuponly |
|---|
| 1187 | def add_parser(self, name, class_): |
|---|
| 1188 | """Add a new parser class. This parser has to be a subclass of |
|---|
| 1189 | :class:`zine.parsers.BaseParser`. |
|---|
| 1190 | """ |
|---|
| 1191 | self.parsers[name] = class_(self) |
|---|
| 1192 | |
|---|
| 1193 | @setuponly |
|---|
| 1194 | def add_markup_extension(self, extension): |
|---|
| 1195 | """Register a new markup extension.""" |
|---|
| 1196 | self.markup_extensions.append(extension(self)) |
|---|
| 1197 | |
|---|
| 1198 | @setuponly |
|---|
| 1199 | def add_widget(self, widget): |
|---|
| 1200 | """Add a widget.""" |
|---|
| 1201 | self.widgets[widget.name] = widget |
|---|
| 1202 | |
|---|
| 1203 | @setuponly |
|---|
| 1204 | def add_servicepoint(self, identifier, callback): |
|---|
| 1205 | """Add a new function as servicepoint. A service point is a function |
|---|
| 1206 | that is called by an external non-human interface such as an |
|---|
| 1207 | JavaScript or XMLRPC client. It's automatically exposed to all |
|---|
| 1208 | service interfaces. |
|---|
| 1209 | """ |
|---|
| 1210 | self._services[identifier] = callback |
|---|
| 1211 | |
|---|
| 1212 | @setuponly |
|---|
| 1213 | def add_privilege(self, privilege): |
|---|
| 1214 | """Registers a new privilege.""" |
|---|
| 1215 | self.privileges[privilege.name] = privilege |
|---|
| 1216 | |
|---|
| 1217 | @setuponly |
|---|
| 1218 | def connect_event(self, event, callback, position='after'): |
|---|
| 1219 | """Connect a callback to an event. Per default the callback is |
|---|
| 1220 | appended to the end of the handlers but handlers can ask for a higher |
|---|
| 1221 | privilege by setting `position` to ``'before'``. |
|---|
| 1222 | |
|---|
| 1223 | Example usage:: |
|---|
| 1224 | |
|---|
| 1225 | def on_before_metadata_assembled(metadata): |
|---|
| 1226 | metadata.append('<!-- IM IN UR METADATA -->') |
|---|
| 1227 | |
|---|
| 1228 | def setup(app): |
|---|
| 1229 | app.connect_event('before-metadata-assembled', |
|---|
| 1230 | on_before_metadata_assembled) |
|---|
| 1231 | """ |
|---|
| 1232 | self._event_manager.connect(event, callback, position) |
|---|
| 1233 | |
|---|
| 1234 | @setuponly |
|---|
| 1235 | def add_notification_system(self, system): |
|---|
| 1236 | """Add the notification system to the list of notification systems |
|---|
| 1237 | the NotificationManager holds. |
|---|
| 1238 | """ |
|---|
| 1239 | self.notification_manager.systems[system.key] = system(self) |
|---|
| 1240 | |
|---|
| 1241 | @setuponly |
|---|
| 1242 | def add_notification_type(self, type): |
|---|
| 1243 | """Registers a new notification type on the instance.""" |
|---|
| 1244 | self.notification_manager.add_notification_type(type) |
|---|
| 1245 | |
|---|
| 1246 | def list_parsers(self): |
|---|
| 1247 | """Return a sorted list of parsers (parser_id, parser_name).""" |
|---|
| 1248 | # we call unicode to resolve the translations once. parser.name |
|---|
| 1249 | # will very likely be a lazy translation |
|---|
| 1250 | return sorted([(key, unicode(parser.name)) for key, parser in |
|---|
| 1251 | self.parsers.iteritems()], key=lambda x: x[1].lower()) |
|---|
| 1252 | |
|---|
| 1253 | def list_privileges(self): |
|---|
| 1254 | """Return a sorted list of privileges.""" |
|---|
| 1255 | # TODO: somehow add grouping... |
|---|
| 1256 | result = [(x.name, unicode(x.explanation)) for x in |
|---|
| 1257 | self.privileges.values()] |
|---|
| 1258 | result.sort(key=lambda x: x[0] == 'BLOG_ADMIN' or x[1].lower()) |
|---|
| 1259 | return result |
|---|
| 1260 | |
|---|
| 1261 | def get_page_metadata(self): |
|---|
| 1262 | """Return the metadata as HTML part for templates. This is normally |
|---|
| 1263 | called by the layout template to get the metadata for the head section. |
|---|
| 1264 | """ |
|---|
| 1265 | from zine.utils import dump_json |
|---|
| 1266 | generators = {'script': htmlhelpers.script, 'meta': htmlhelpers.meta, |
|---|
| 1267 | 'link': htmlhelpers.link, 'snippet': lambda html: html} |
|---|
| 1268 | result = [ |
|---|
| 1269 | htmlhelpers.meta(name='generator', content='Zine'), |
|---|
| 1270 | htmlhelpers.link('EditURI', url_for('blog/service_rsd'), |
|---|
| 1271 | type='application/rsd+xml', title='RSD'), |
|---|
| 1272 | htmlhelpers.script(url_for('core/shared', filename='js/jQuery.js')), |
|---|
| 1273 | htmlhelpers.script(url_for('core/shared', filename='js/Zine.js')), |
|---|
| 1274 | htmlhelpers.script(url_for('blog/serve_translations')) |
|---|
| 1275 | ] |
|---|
| 1276 | |
|---|
| 1277 | # the url information. Only expose the admin url for admin users |
|---|
| 1278 | # or calls to this method without a request. |
|---|
| 1279 | base_url = self.cfg['blog_url'].rstrip('/') |
|---|
| 1280 | request = get_request() |
|---|
| 1281 | javascript = [ |
|---|
| 1282 | 'Zine.ROOT_URL = %s' % dump_json(base_url), |
|---|
| 1283 | 'Zine.BLOG_URL = %s' % dump_json(base_url + |
|---|
| 1284 | self.cfg['blog_url_prefix']) |
|---|
| 1285 | ] |
|---|
| 1286 | if request is None or request.user.is_manager: |
|---|
| 1287 | javascript.append('Zine.ADMIN_URL = %s' % |
|---|
| 1288 | dump_json(base_url + |
|---|
| 1289 | self.cfg['admin_url_prefix'])) |
|---|
| 1290 | result.append(u'<script type="text/javascript">%s;</script>' % |
|---|
| 1291 | '; '.join(javascript)) |
|---|
| 1292 | |
|---|
| 1293 | for type, attr in local.page_metadata: |
|---|
| 1294 | result.append(generators[type](**attr)) |
|---|
| 1295 | |
|---|
| 1296 | #! this is called before the page metadata is assembled with |
|---|
| 1297 | #! the list of already collected metadata. You can extend the |
|---|
| 1298 | #! list in place to add some more html snippets to the page header. |
|---|
| 1299 | emit_event('before-metadata-assembled', result) |
|---|
| 1300 | return u'\n'.join(result) |
|---|
| 1301 | |
|---|
| 1302 | def handle_not_found(self, request, exception): |
|---|
| 1303 | """Handle a not found exception. This also dispatches to plugins |
|---|
| 1304 | that listen for for absolute urls. See `add_absolute_url` for |
|---|
| 1305 | details. |
|---|
| 1306 | """ |
|---|
| 1307 | for handler in self._absolute_url_handlers: |
|---|
| 1308 | try: |
|---|
| 1309 | rv = handler(request) |
|---|
| 1310 | if rv is not None: |
|---|
| 1311 | return rv |
|---|
| 1312 | except NotFound: |
|---|
| 1313 | # a not found exception has the same effect as returning |
|---|
| 1314 | # None. The next handler is processed. All other http |
|---|
| 1315 | # exceptions are passed trough. |
|---|
| 1316 | pass |
|---|
| 1317 | response = render_response('404.html') |
|---|
| 1318 | response.status_code = 404 |
|---|
| 1319 | return response |
|---|
| 1320 | |
|---|
| 1321 | def send_error_notification(self, request, error): |
|---|
| 1322 | from zine.notifications import send_notification_template, ZINE_ERROR |
|---|
| 1323 | request_buffer = StringIO() |
|---|
| 1324 | request_buffer.seek(0) |
|---|
| 1325 | send_notification_template( |
|---|
| 1326 | ZINE_ERROR, 'notifications/on_server_error.zeml', |
|---|
| 1327 | user=request.user, summary=error.message, |
|---|
| 1328 | request_details=request_buffer.read(), |
|---|
| 1329 | longtext=''.join(format_exception(*sys.exc_info())) |
|---|
| 1330 | ) |
|---|
| 1331 | |
|---|
| 1332 | def handle_server_error(self, request, exc_info=None, suppress_log=False): |
|---|
| 1333 | """Called if a server error happens. Logs the error and returns a |
|---|
| 1334 | response with an error message. |
|---|
| 1335 | """ |
|---|
| 1336 | if not suppress_log: |
|---|
| 1337 | log.exception('Exception happened at "%s"' % request.path, |
|---|
| 1338 | 'core', exc_info) |
|---|
| 1339 | response = render_response('500.html') |
|---|
| 1340 | response.status_code = 500 |
|---|
| 1341 | return response |
|---|
| 1342 | |
|---|
| 1343 | def handle_internal_error(self, request, error, suppress_log=True): |
|---|
| 1344 | """Called if internal errors are caught.""" |
|---|
| 1345 | if request.user.is_admin: |
|---|
| 1346 | response = render_response('internal_error.html', error=error) |
|---|
| 1347 | response.status_code = 500 |
|---|
| 1348 | return response |
|---|
| 1349 | # We got here, meaning no admin has seen this error yet. Notify Them! |
|---|
| 1350 | self.send_error_notification(request, error) |
|---|
| 1351 | return self.handle_server_error(request, suppress_log=suppress_log) |
|---|
| 1352 | |
|---|
| 1353 | def dispatch_request(self, request): |
|---|
| 1354 | #! the after-request-setup event can return a response |
|---|
| 1355 | #! or modify the request object in place. If we have a |
|---|
| 1356 | #! response we just send it, no other modifications are done. |
|---|
| 1357 | for callback in iter_listeners('after-request-setup'): |
|---|
| 1358 | result = callback(request) |
|---|
| 1359 | if result is not None: |
|---|
| 1360 | return result |
|---|
| 1361 | |
|---|
| 1362 | # normal request dispatching |
|---|
| 1363 | try: |
|---|
| 1364 | try: |
|---|
| 1365 | endpoint, args = self.url_adapter.match(request.path) |
|---|
| 1366 | response = self.views[endpoint](request, **args) |
|---|
| 1367 | except NotFound, e: |
|---|
| 1368 | response = self.handle_not_found(request, e) |
|---|
| 1369 | except Forbidden, e: |
|---|
| 1370 | if request.user.is_somebody: |
|---|
| 1371 | response = render_response('403.html') |
|---|
| 1372 | response.status_code = 403 |
|---|
| 1373 | else: |
|---|
| 1374 | response = _redirect(url_for('account/login', |
|---|
| 1375 | next=request.path)) |
|---|
| 1376 | except HTTPException, e: |
|---|
| 1377 | response = e.get_response(request.environ) |
|---|
| 1378 | except SQLAlchemyError, e: |
|---|
| 1379 | # Some database screwup?! Don't let Zine stay dispatching 500's |
|---|
| 1380 | db.session.rollback() |
|---|
| 1381 | response = self.handle_internal_error(request, e, |
|---|
| 1382 | suppress_log=False) |
|---|
| 1383 | |
|---|
| 1384 | # in debug mode on HTML responses we inject the collected queries. |
|---|
| 1385 | if self.cfg['database_debug'] and \ |
|---|
| 1386 | getattr(response, 'mimetype', None) == 'text/html' and \ |
|---|
| 1387 | isinstance(response.response, (list, tuple)): |
|---|
| 1388 | from zine.utils.debug import inject_query_info |
|---|
| 1389 | inject_query_info(request, response) |
|---|
| 1390 | |
|---|
| 1391 | return response |
|---|
| 1392 | |
|---|
| 1393 | def dispatch_wsgi(self, environ, start_response): |
|---|
| 1394 | """This method is the internal WSGI request and is overridden by |
|---|
| 1395 | middlewares applied with :meth:`add_middleware`. It handles the |
|---|
| 1396 | actual request dispatching. |
|---|
| 1397 | """ |
|---|
| 1398 | # Create a new request object, register it with the application |
|---|
| 1399 | # and all the other stuff on the current thread but initialize |
|---|
| 1400 | # it afterwards. We do this so that the request object can query |
|---|
| 1401 | # the database in the initialization method. |
|---|
| 1402 | request = object.__new__(Request) |
|---|
| 1403 | local.request = request |
|---|
| 1404 | local.page_metadata = [] |
|---|
| 1405 | local.request_locals = {} |
|---|
| 1406 | request.__init__(environ, self) |
|---|
| 1407 | |
|---|
| 1408 | # check if the blog is in maintenance_mode and the user is |
|---|
| 1409 | # not an administrator. in that case just show a message that |
|---|
| 1410 | # the user is not privileged to view the blog right now. Exception: |
|---|
| 1411 | # the page is the login page for the blog. |
|---|
| 1412 | # XXX: Remove 'admin_prefix' references for Zine 0.3 |
|---|
| 1413 | # It still exists because some themes might depend on it. |
|---|
| 1414 | js_translations = url_for('blog/serve_translations') |
|---|
| 1415 | admin_prefix = self.cfg['admin_url_prefix'] |
|---|
| 1416 | account_prefix = self.cfg['account_url_prefix'] |
|---|
| 1417 | if self.cfg['maintenance_mode'] and \ |
|---|
| 1418 | request.path not in (account_prefix, admin_prefix, js_translations) \ |
|---|
| 1419 | and not (request.path.startswith(admin_prefix + '/') or |
|---|
| 1420 | request.path.startswith(account_prefix + '/')): |
|---|
| 1421 | if not request.user.has_privilege( |
|---|
| 1422 | self.privileges['ENTER_ADMIN_PANEL']): |
|---|
| 1423 | response = render_response('maintenance.html') |
|---|
| 1424 | response.status_code = 503 |
|---|
| 1425 | return response(environ, start_response) |
|---|
| 1426 | |
|---|
| 1427 | # if HTTPS enforcement is active, we redirect to HTTPS if |
|---|
| 1428 | # possibile without problems (no playload) |
|---|
| 1429 | if self.cfg['force_https'] and request.method in ('GET', 'HEAD') and \ |
|---|
| 1430 | environ['wsgi.url_scheme'] == 'http': |
|---|
| 1431 | response = _redirect('https' + request.url[4:], 301) |
|---|
| 1432 | return response(environ, start_response) |
|---|
| 1433 | |
|---|
| 1434 | # wrap the real dispatching in a try/except so that we can |
|---|
| 1435 | # intercept exceptions that happen in the application. |
|---|
| 1436 | try: |
|---|
| 1437 | response = self.dispatch_request(request) |
|---|
| 1438 | |
|---|
| 1439 | # make sure the response object is one of ours |
|---|
| 1440 | response = Response.force_type(response, environ) |
|---|
| 1441 | |
|---|
| 1442 | #! allow plugins to change the response object |
|---|
| 1443 | for callback in iter_listeners('before-response-processed'): |
|---|
| 1444 | result = callback(response) |
|---|
| 1445 | if result is not None: |
|---|
| 1446 | response = result |
|---|
| 1447 | except InternalError, e: |
|---|
| 1448 | response = self.handle_internal_error(request, e) |
|---|
| 1449 | except: |
|---|
| 1450 | if self.cfg['passthrough_errors']: |
|---|
| 1451 | raise |
|---|
| 1452 | response = self.handle_server_error(request) |
|---|
| 1453 | |
|---|
| 1454 | # update the session cookie at the request end if the |
|---|
| 1455 | # session data requires an update. |
|---|
| 1456 | if request.session.should_save: |
|---|
| 1457 | # set the secret key explicitly at the end of the request |
|---|
| 1458 | # to not log out the administrator if he changes the secret |
|---|
| 1459 | # key in the config editor. |
|---|
| 1460 | request.session.secret_key = self.secret_key |
|---|
| 1461 | cookie_name = self.cfg['session_cookie_name'] |
|---|
| 1462 | if request.session.get('pmt'): |
|---|
| 1463 | max_age = 60 * 60 * 24 * 31 |
|---|
| 1464 | expires = time() + max_age |
|---|
| 1465 | else: |
|---|
| 1466 | max_age = expires = None |
|---|
| 1467 | request.session.save_cookie(response, cookie_name, max_age=max_age, |
|---|
| 1468 | expires=expires, session_expires=expires) |
|---|
| 1469 | |
|---|
| 1470 | return response(environ, start_response) |
|---|
| 1471 | |
|---|
| 1472 | def perform_subrequest(self, path, query=None, method='GET', data=None, |
|---|
| 1473 | timeout=None, response_wrapper=Response): |
|---|
| 1474 | """Perform an internal subrequest against Zine. This method spawns a |
|---|
| 1475 | separate thread and lets an internal WSGI client answer the request. |
|---|
| 1476 | The return value is then converted into a zine response object and |
|---|
| 1477 | returned. |
|---|
| 1478 | |
|---|
| 1479 | A separate thread is spawned so that the internal request does not |
|---|
| 1480 | cause troubles for the current one in terms of persistent database |
|---|
| 1481 | objects. |
|---|
| 1482 | |
|---|
| 1483 | This is for example used in the `open_url` method to allow access to |
|---|
| 1484 | blog local resources without dead-locking if the WSGI server does not |
|---|
| 1485 | support concurrency (single threaded and just one process for example). |
|---|
| 1486 | """ |
|---|
| 1487 | from werkzeug import Client |
|---|
| 1488 | from threading import Event, Thread |
|---|
| 1489 | event = Event() |
|---|
| 1490 | response = [] |
|---|
| 1491 | input_stream = None |
|---|
| 1492 | if hasattr(data, 'read'): |
|---|
| 1493 | input_stream = data |
|---|
| 1494 | data = None |
|---|
| 1495 | |
|---|
| 1496 | def make_request(): |
|---|
| 1497 | try: |
|---|
| 1498 | client = Client(self, response_wrapper) |
|---|
| 1499 | response.append(client.open(path, self.cfg['blog_url'], |
|---|
| 1500 | method=method, data=data, |
|---|
| 1501 | query_string=url_encode(query), |
|---|
| 1502 | input_stream=input_stream)) |
|---|
| 1503 | except: |
|---|
| 1504 | response.append(sys.exc_info()) |
|---|
| 1505 | event.set() |
|---|
| 1506 | |
|---|
| 1507 | Thread(target=make_request).start() |
|---|
| 1508 | event.wait(timeout) |
|---|
| 1509 | if not response: |
|---|
| 1510 | raise NetException('Timeout on internal subrequest') |
|---|
| 1511 | if isinstance(response[0], tuple): |
|---|
| 1512 | exc_type, exc_value, tb = response[0] |
|---|
| 1513 | raise exc_type, exc_value, tb |
|---|
| 1514 | return response[0] |
|---|
| 1515 | |
|---|
| 1516 | def __call__(self, environ, start_response): |
|---|
| 1517 | """Make the application object a WSGI application.""" |
|---|
| 1518 | return ClosingIterator(self.dispatch_wsgi(environ, start_response), |
|---|
| 1519 | [local_manager.cleanup, cleanup_session]) |
|---|
| 1520 | |
|---|
| 1521 | def __repr__(self): |
|---|
| 1522 | return '<Zine %r [%s]>' % ( |
|---|
| 1523 | self.instance_folder, |
|---|
| 1524 | self.iid |
|---|
| 1525 | ) |
|---|
| 1526 | |
|---|
| 1527 | # remove our decorator |
|---|
| 1528 | del setuponly |
|---|
| 1529 | |
|---|
| 1530 | |
|---|
| 1531 | # import here because of circular dependencies |
|---|
| 1532 | from zine import i18n |
|---|
| 1533 | from zine.utils import log |
|---|
| 1534 | from zine.utils.net import NetException |
|---|
| 1535 | from zine.utils.http import make_external_url |
|---|