# -*- coding: utf-8 -*- # vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 # pylint: disable=logging-format-interpolation ########################################################################## # OpenLP - Open Source Lyrics Projection # # ---------------------------------------------------------------------- # # Copyright (c) 2008-2019 OpenLP Developers # # ---------------------------------------------------------------------- # # This program is free software: you can redistribute it and/or modify # # it under the terms of the GNU 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 General Public License for more details. # # # # You should have received a copy of the GNU General Public License # # along with this program. If not, see . # ########################################################################## """ App stuff """ import json import logging import re from webob import Request, Response from webob.static import DirectoryApp from openlp.core.api.http.errors import HttpError, NotFound, ServerError from openlp.core.common.applocation import AppLocation ARGS_REGEX = re.compile(r'''\{(\w+)(?::([^}]+))?\}''', re.VERBOSE) log = logging.getLogger(__name__) def _route_to_regex(route): r""" Convert a route to a regular expression For example: 'songs/{song_id}' becomes 'songs/(?P[^/]+)' and 'songs/{song_id:\d+}' becomes 'songs/(?P\d+)' """ route_regex = '' last_pos = 0 for match in ARGS_REGEX.finditer(route): route_regex += re.escape(route[last_pos:match.start()]) arg_name = match.group(1) expr = match.group(2) or '[^/]+' expr = '(?P<%s>%s)' % (arg_name, expr) route_regex += expr last_pos = match.end() route_regex += re.escape(route[last_pos:]) route_regex = '^%s$' % route_regex return route_regex def _make_response(view_result): """ Create a Response object from response """ log.debug("in Make response") if isinstance(view_result, Response): return view_result elif isinstance(view_result, tuple): content_type = 'text/html' body = view_result[0] if isinstance(body, dict): content_type = 'application/json' body = json.dumps(body) response = Response(body=body, status=view_result[1], content_type=content_type, charset='utf8') if len(view_result) >= 3: response.headers.update(view_result[2]) return response elif isinstance(view_result, dict): return Response(body=json.dumps(view_result), status=200, content_type='application/json', charset='utf8') elif isinstance(view_result, str): return Response(body=view_result, status=200, content_type='text/html', charset='utf8') else: return Response(body=view_result, status=200, content_type='text/plain', charset='utf8') def _handle_exception(error): """ Handle exceptions """ log.exception(error) if isinstance(error, HttpError): return error.to_response() else: return ServerError().to_response() class WSGIApplication(object): """ This is the core of the API, the WSGI app """ def __init__(self, name): """ Create the app object """ self.name = name self.static_routes = {} self.route_map = {} def initialise(self): """ Set up generic roots for the whole application :return: None """ self.add_static_route('/assets(.*)', '') self.add_static_route('/images(.*)', '') pass def add_route(self, route, view_func, method): """ Add a route """ route_regex = _route_to_regex(route) if route_regex not in self.route_map: self.route_map[route_regex] = {} self.route_map[route_regex][method.upper()] = view_func def add_static_route(self, route, static_dir): """ Add a static directory as a route """ if route not in self.static_routes: static_path = AppLocation.get_section_data_path('remotes') / static_dir if not static_path.exists(): log.error('Static path "%s" does not exist. Skipping creating static route/', static_path) return self.static_routes[route] = DirectoryApp(str(static_path.resolve())) def dispatch(self, request): """ Find the appropriate URL and run the view function """ # If not a static route, try the views for route, views in self.route_map.items(): match = re.match(route, request.path) if match and request.method.upper() in views: kwargs = match.groupdict() log.debug('Found {method} {url}'.format(method=request.method, url=request.path)) view_func = views[request.method.upper()] return _make_response(view_func(request, **kwargs)) # Look to see if this is a static file request for route, static_app in self.static_routes.items(): if re.match(route, request.path): return request.get_response(static_app) log.error('URL {url} - Not found'.format(url=request.path)) raise NotFound() def wsgi_app(self, environ, start_response): """ The actual WSGI application. """ request = Request(environ) try: response = self.dispatch(request) except Exception as e: response = _make_response(_handle_exception(e)) response.headers.add("cache-control", "no-cache, no-store, must-revalidate") response.headers.add("pragma", "no-cache") response.headers.add("expires", "0") return response(environ, start_response) def __call__(self, environ, start_response): """ Shortcut for wsgi_app. """ return self.wsgi_app(environ, start_response)