openlp-core team mailing list archive
-
openlp-core team
-
Mailing list archive
-
Message #21893
[Merge] lp:~trb143/openlp/cherrypy into lp:openlp
Tim Bentley has proposed merging lp:~trb143/openlp/cherrypy into lp:openlp.
Requested reviews:
OpenLP Core (openlp-core)
Jeffrey Smith (whydoubt)
Related bugs:
Bug #826724 in OpenLP: "Make the Web Remote password protected"
https://bugs.launchpad.net/openlp/+bug/826724
Bug #826731 in OpenLP: "Add optional SSL to web remote"
https://bugs.launchpad.net/openlp/+bug/826731
For more details, see:
https://code.launchpad.net/~trb143/openlp/cherrypy/+merge/186587
Early merge request for the replacement of CherryPy
Web works on initial testing Android shows a failure but I will look at next.
Contains debugging code as still WIP but making available for others to look at the principles.
--
https://code.launchpad.net/~trb143/openlp/cherrypy/+merge/186587
Your team OpenLP Core is requested to review the proposed merge of lp:~trb143/openlp/cherrypy into lp:openlp.
=== modified file 'openlp/core/ui/exceptionform.py'
--- openlp/core/ui/exceptionform.py 2013-08-31 18:17:38 +0000
+++ openlp/core/ui/exceptionform.py 2013-09-19 17:06:40 +0000
@@ -76,12 +76,6 @@
except ImportError:
ICU_VERSION = '-'
try:
- import cherrypy
- CHERRYPY_VERSION = cherrypy.__version__
-except ImportError:
- CHERRYPY_VERSION = '-'
-
-try:
WEBKIT_VERSION = QtWebKit.qWebKitVersion()
except AttributeError:
WEBKIT_VERSION = '-'
@@ -140,7 +134,6 @@
'Chardet: %s\n' % CHARDET_VERSION + \
'PyEnchant: %s\n' % ENCHANT_VERSION + \
'Mako: %s\n' % MAKO_VERSION + \
- 'CherryPy: %s\n' % CHERRYPY_VERSION + \
'pyICU: %s\n' % ICU_VERSION + \
'pyUNO bridge: %s\n' % self._pyuno_import() + \
'VLC: %s\n' % VLC_VERSION
=== modified file 'openlp/plugins/remotes/html/openlp.js'
--- openlp/plugins/remotes/html/openlp.js 2013-04-23 20:31:19 +0000
+++ openlp/plugins/remotes/html/openlp.js 2013-09-19 17:06:40 +0000
@@ -40,6 +40,8 @@
// defeat Safari bug
targ = targ.parentNode;
}
+ var isSecure = false;
+ var isAuthorised = false;
return $(targ);
},
getSearchablePlugins: function () {
@@ -147,11 +149,13 @@
},
pollServer: function () {
$.getJSON(
- "/stage/poll",
+ "/api/poll",
function (data, status) {
var prevItem = OpenLP.currentItem;
OpenLP.currentSlide = data.results.slide;
OpenLP.currentItem = data.results.item;
+ OpenLP.isSecure = data.results.isSecure;
+ OpenLP.isAuthorised = data.results.isAuthorised;
if ($("#service-manager").is(":visible")) {
if (OpenLP.currentService != data.results.service) {
OpenLP.currentService = data.results.service;
=== modified file 'openlp/plugins/remotes/html/stage.js'
--- openlp/plugins/remotes/html/stage.js 2013-04-23 20:31:19 +0000
+++ openlp/plugins/remotes/html/stage.js 2013-09-19 17:06:40 +0000
@@ -26,7 +26,7 @@
window.OpenLP = {
loadService: function (event) {
$.getJSON(
- "/stage/service/list",
+ "/api/service/list",
function (data, status) {
OpenLP.nextSong = "";
$("#notes").html("");
@@ -46,7 +46,7 @@
},
loadSlides: function (event) {
$.getJSON(
- "/stage/controller/live/text",
+ "/api/controller/live/text",
function (data, status) {
OpenLP.currentSlides = data.results.slides;
OpenLP.currentSlide = 0;
@@ -137,7 +137,7 @@
},
pollServer: function () {
$.getJSON(
- "/stage/poll",
+ "/api/poll",
function (data, status) {
OpenLP.updateClock(data);
if (OpenLP.currentItem != data.results.item ||
=== modified file 'openlp/plugins/remotes/lib/__init__.py'
--- openlp/plugins/remotes/lib/__init__.py 2013-08-31 18:17:38 +0000
+++ openlp/plugins/remotes/lib/__init__.py 2013-09-19 17:06:40 +0000
@@ -28,6 +28,7 @@
###############################################################################
from .remotetab import RemoteTab
-from .httpserver import HttpServer
+from .httprouter import HttpRouter
+from .httpserver import OpenLPServer
-__all__ = ['RemoteTab', 'HttpServer']
+__all__ = ['RemoteTab', 'OpenLPServer', 'HttpRouter']
=== added file 'openlp/plugins/remotes/lib/httprouter.py'
--- openlp/plugins/remotes/lib/httprouter.py 1970-01-01 00:00:00 +0000
+++ openlp/plugins/remotes/lib/httprouter.py 2013-09-19 17:06:40 +0000
@@ -0,0 +1,627 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2013 Raoul Snyman #
+# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan #
+# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
+# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
+# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
+# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
+# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
+# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
+# --------------------------------------------------------------------------- #
+# 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; version 2 of the License. #
+# #
+# 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, write to the Free Software Foundation, Inc., 59 #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
+###############################################################################
+
+"""
+The :mod:`http` module contains the API web server. This is a lightweight web
+server used by remotes to interact with OpenLP. It uses JSON to communicate with
+the remotes.
+
+*Routes:*
+
+``/``
+ Go to the web interface.
+
+``/stage``
+ Show the stage view.
+
+``/files/{filename}``
+ Serve a static file.
+
+``/stage/api/poll``
+ Poll to see if there are any changes. Returns a JSON-encoded dict of
+ any changes that occurred::
+
+ {"results": {"type": "controller"}}
+
+ Or, if there were no results, False::
+
+ {"results": False}
+
+``/api/display/{hide|show}``
+ Blank or unblank the screen.
+
+``/api/alert``
+ Sends an alert message to the alerts plugin. This method expects a
+ JSON-encoded dict like this::
+
+ {"request": {"text": "<your alert text>"}}
+
+``/api/controller/{live|preview}/{action}``
+ Perform ``{action}`` on the live or preview controller. Valid actions
+ are:
+
+ ``next``
+ Load the next slide.
+
+ ``previous``
+ Load the previous slide.
+
+ ``set``
+ Set a specific slide. Requires an id return in a JSON-encoded dict like
+ this::
+
+ {"request": {"id": 1}}
+
+ ``first``
+ Load the first slide.
+
+ ``last``
+ Load the last slide.
+
+ ``text``
+ Fetches the text of the current song. The output is a JSON-encoded
+ dict which looks like this::
+
+ {"result": {"slides": ["...", "..."]}}
+
+``/api/service/{action}``
+ Perform ``{action}`` on the service manager (e.g. go live). Data is
+ passed as a json-encoded ``data`` parameter. Valid actions are:
+
+ ``next``
+ Load the next item in the service.
+
+ ``previous``
+ Load the previews item in the service.
+
+ ``set``
+ Set a specific item in the service. Requires an id returned in a
+ JSON-encoded dict like this::
+
+ {"request": {"id": 1}}
+
+ ``list``
+ Request a list of items in the service. Returns a list of items in the
+ current service in a JSON-encoded dict like this::
+
+ {"results": {"items": [{...}, {...}]}}
+"""
+import base64
+import json
+import logging
+import os
+import re
+import urllib.request
+import urllib.error
+from urllib.parse import urlparse, parse_qs
+
+
+from mako.template import Template
+from PyQt4 import QtCore
+
+from openlp.core.lib import Registry, Settings, PluginStatus, StringContent, image_to_byte
+from openlp.core.utils import AppLocation, translate
+
+log = logging.getLogger(__name__)
+
+
+class HttpRouter(object):
+ """
+ This code is called by the HttpServer upon a request and it processes it based on the routing table.
+ This code is stateless and is created on each request.
+ Some variables may look incorrect but this extends BaseHTTPRequestHandler.
+ """
+ def initialise(self):
+ """
+ Initialise the router stack and any other variables.
+ """
+ authcode = "%s:%s" % (Settings().value('remotes/user id'), Settings().value('remotes/password'))
+ try:
+ self.auth = base64.b64encode(authcode)
+ except TypeError:
+ self.auth = base64.b64encode(authcode.encode()).decode()
+ self.routes = [
+ ('^/$', {'function': self.serve_file, 'secure': False}),
+ ('^/(stage)$', {'function': self.serve_file, 'secure': False}),
+ ('^/(main)$', {'function': self.serve_file, 'secure': False}),
+ (r'^/files/(.*)$', {'function': self.serve_file, 'secure': False}),
+ (r'^/api/poll$', {'function': self.poll, 'secure': False}),
+ (r'^/main/poll$', {'function': self.poll, 'secure': False}),
+ (r'^/main/image$', {'function': self.main_poll, 'secure': False}),
+ (r'^/api/controller/(live|preview)/text$', {'function': self.controller_text, 'secure': False}),
+ (r'^/api/controller/(live|preview)/(.*)$', {'function': self.controller, 'secure': True}),
+ (r'^/api/service/list$', {'function': self.service_list, 'secure': False}),
+ (r'^/api/service/(.*)$', {'function': self.service, 'secure': True}),
+ (r'^/api/display/(hide|show|blank|theme|desktop)$', {'function': self.display, 'secure': True}),
+ (r'^/api/alert$', {'function': self.alert, 'secure': True}),
+ (r'^/api/plugin/(search)$', {'function': self.plugin_info, 'secure': False}),
+ (r'^/api/(.*)/search$', {'function': self.search, 'secure': False}),
+ (r'^/api/(.*)/live$', {'function': self.go_live, 'secure': True}),
+ (r'^/api/(.*)/add$', {'function': self.add_to_service, 'secure': True})
+ ]
+ self.settings_section = 'remotes'
+ self.translate()
+ self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), 'remotes', 'html')
+
+ def do_post_processor(self):
+ """
+ Handle the POST amd GET requests placed on the server.
+ """
+ if self.path == '/favicon.ico':
+ return
+ ###########
+ print(self.headers['content-type'])
+ if self.headers['content-type'] == 'application/json':
+ length = int(self.headers['content-length'])
+ postvars = parse_qs(self.rfile.read(length), keep_blank_values=1)
+ for var in postvars:
+ print(var.decode("utf-8"))
+ ##############
+ if not hasattr(self, 'auth'):
+ self.initialise()
+ function, args = self.process_http_request(self.path)
+ if not function:
+ self.do_http_error()
+ return
+ self.authorised = self.headers['Authorization'] is None
+ if function['secure'] and Settings().value(self.settings_section + '/authentication enabled'):
+ if self.headers['Authorization'] is None:
+ self.do_authorisation()
+ self.wfile.write(bytes('no auth header received', 'UTF-8'))
+ elif self.headers['Authorization'] == 'Basic %s' % self.auth:
+ self.do_http_success()
+ self.call_function(function, *args)
+ else:
+ self.do_authorisation()
+ self.wfile.write(bytes(self.headers['Authorization'], 'UTF-8'))
+ self.wfile.write(bytes(' not authenticated', 'UTF-8'))
+ else:
+ self.call_function(function, *args)
+
+ def call_function(self, function, *args):
+ """
+ Invoke the route function passing the relevant values
+
+ ``function``
+ The function to be calledL.
+
+ ``*args``
+ Any passed data.
+ """
+ response = function['function'](*args)
+ if response:
+ self.wfile.write(response)
+ return
+
+ def process_http_request(self, url_path, *args):
+ """
+ Common function to process HTTP requests
+
+ ``url_path``
+ The requested URL.
+
+ ``*args``
+ Any passed data.
+ """
+ self.request_data = None
+ url_path_split = urlparse(url_path)
+ url_query = parse_qs(url_path_split.query)
+ if 'data' in url_query.keys():
+ self.request_data = url_query['data'][0]
+ for route, func in self.routes:
+ match = re.match(route, url_path_split.path)
+ if match:
+ log.debug('Route "%s" matched "%s"', route, url_path)
+ args = []
+ for param in match.groups():
+ args.append(param)
+ return func, args
+ return None, None
+
+ def do_http_success(self):
+ """
+ Create a success http header.
+ """
+ self.send_response(200)
+ self.send_header('Content-type', 'text/html')
+ self.end_headers()
+
+ def do_http_error(self):
+ """
+ Create a error http header.
+ """
+ self.send_response(404)
+ self.send_header('Content-type', 'text/html')
+ self.end_headers()
+
+ def do_authorisation(self):
+ """
+ Create a needs authorisation http header.
+ """
+ self.send_response(401)
+ self.send_header('WWW-Authenticate', 'Basic realm=\"Test\"')
+ self.send_header('Content-type', 'text/html')
+ self.end_headers()
+
+ def do_not_found(self):
+ """
+ Create a not found http header.
+ """
+ self.send_response(404)
+ self.send_header('Content-type', 'text/html')
+ self.end_headers()
+ self.wfile.write(bytes('<html><body>Sorry, an error occurred </body></html>', 'UTF-8'))
+
+ def _get_service_items(self):
+ """
+ Read the service item in use and return the data as a json object
+ """
+ service_items = []
+ if self.live_controller.service_item:
+ current_unique_identifier = self.live_controller.service_item.unique_identifier
+ else:
+ current_unique_identifier = None
+ for item in self.service_manager.service_items:
+ service_item = item['service_item']
+ service_items.append({
+ 'id': str(service_item.unique_identifier),
+ 'title': str(service_item.get_display_title()),
+ 'plugin': str(service_item.name),
+ 'notes': str(service_item.notes),
+ 'selected': (service_item.unique_identifier == current_unique_identifier)
+ })
+ return service_items
+
+ def translate(self):
+ """
+ Translate various strings in the mobile app.
+ """
+ self.template_vars = {
+ 'app_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Remote'),
+ 'stage_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Stage View'),
+ 'live_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Live View'),
+ 'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
+ 'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
+ 'alerts': translate('RemotePlugin.Mobile', 'Alerts'),
+ 'search': translate('RemotePlugin.Mobile', 'Search'),
+ 'home': translate('RemotePlugin.Mobile', 'Home'),
+ 'refresh': translate('RemotePlugin.Mobile', 'Refresh'),
+ 'blank': translate('RemotePlugin.Mobile', 'Blank'),
+ 'theme': translate('RemotePlugin.Mobile', 'Theme'),
+ 'desktop': translate('RemotePlugin.Mobile', 'Desktop'),
+ 'show': translate('RemotePlugin.Mobile', 'Show'),
+ 'prev': translate('RemotePlugin.Mobile', 'Prev'),
+ 'next': translate('RemotePlugin.Mobile', 'Next'),
+ 'text': translate('RemotePlugin.Mobile', 'Text'),
+ 'show_alert': translate('RemotePlugin.Mobile', 'Show Alert'),
+ 'go_live': translate('RemotePlugin.Mobile', 'Go Live'),
+ 'add_to_service': translate('RemotePlugin.Mobile', 'Add to Service'),
+ 'add_and_go_to_service': translate('RemotePlugin.Mobile', 'Add & Go to Service'),
+ 'no_results': translate('RemotePlugin.Mobile', 'No Results'),
+ 'options': translate('RemotePlugin.Mobile', 'Options'),
+ 'service': translate('RemotePlugin.Mobile', 'Service'),
+ 'slides': translate('RemotePlugin.Mobile', 'Slides')
+ }
+
+ def serve_file(self, file_name=None):
+ """
+ Send a file to the socket. For now, just a subset of file types and must be top level inside the html folder.
+ If subfolders requested return 404, easier for security for the present.
+
+ Ultimately for i18n, this could first look for xx/file.html before falling back to file.html.
+ where xx is the language, e.g. 'en'
+ """
+ log.debug('serve file request %s' % file_name)
+ if not file_name:
+ file_name = 'index.html'
+ elif file_name == 'stage':
+ file_name = 'stage.html'
+ elif file_name == 'main':
+ file_name = 'main.html'
+ path = os.path.normpath(os.path.join(self.html_dir, file_name))
+ if not path.startswith(self.html_dir):
+ return self.do_not_found()
+ ext = os.path.splitext(file_name)[1]
+ html = None
+ if ext == '.html':
+ self.send_header('Content-type', 'text/html')
+ variables = self.template_vars
+ html = Template(filename=path, input_encoding='utf-8', output_encoding='utf-8').render(**variables)
+ elif ext == '.css':
+ self.send_header('Content-type', 'text/css')
+ elif ext == '.js':
+ self.send_header('Content-type', 'application/javascript')
+ elif ext == '.jpg':
+ self.send_header('Content-type', 'image/jpeg')
+ elif ext == '.gif':
+ self.send_header('Content-type', 'image/gif')
+ elif ext == '.ico':
+ self.send_header('Content-type', 'image/x-icon')
+ elif ext == '.png':
+ self.send_header('Content-type', 'image/png')
+ else:
+ self.send_header('Content-type', 'text/plain')
+ file_handle = None
+ try:
+ if html:
+ content = html
+ else:
+ file_handle = open(path, 'rb')
+ log.debug('Opened %s' % path)
+ content = file_handle.read()
+ except IOError:
+ log.exception('Failed to open %s' % path)
+ return self.do_not_found()
+ finally:
+ if file_handle:
+ file_handle.close()
+ return content
+
+ def poll(self):
+ """
+ Poll OpenLP to determine the current slide number and item name.
+ """
+ result = {
+ 'service': self.service_manager.service_id,
+ 'slide': self.live_controller.selected_row or 0,
+ 'item': self.live_controller.service_item.unique_identifier if self.live_controller.service_item else '',
+ 'twelve': Settings().value('remotes/twelve hour'),
+ 'blank': self.live_controller.blank_screen.isChecked(),
+ 'theme': self.live_controller.theme_screen.isChecked(),
+ 'display': self.live_controller.desktop_screen.isChecked(),
+ 'version': 2,
+ 'isSecure': Settings().value(self.settings_section + '/authentication enabled'),
+ 'isAuthorised': self.authorised
+ }
+ return json.dumps({'results': result}).encode()
+
+ def main_poll(self):
+ """
+ Poll OpenLP to determine the current slide count.
+ """
+ result = {
+ 'slide_count': self.live_controller.slide_count
+ }
+ return json.dumps({'results': result}).encode()
+
+ def main_image(self):
+ """
+ Return the latest display image as a byte stream.
+ """
+ result = {
+ 'slide_image': 'data:image/png;base64,' + str(image_to_byte(self.live_controller.slide_image))
+ }
+ return json.dumps({'results': result}).encode()
+
+ def display(self, action):
+ """
+ Hide or show the display screen.
+ This is a cross Thread call and UI is updated so Events need to be used.
+
+ ``action``
+ This is the action, either ``hide`` or ``show``.
+ """
+ self.live_controller.emit(QtCore.SIGNAL('slidecontroller_toggle_display'), action)
+ return json.dumps({'results': {'success': True}}).encode()
+
+ def alert(self):
+ """
+ Send an alert.
+ """
+ plugin = self.plugin_manager.get_plugin_by_name("alerts")
+ if plugin.status == PluginStatus.Active:
+ try:
+ text = json.loads(self.request_data)['request']['text']
+ except KeyError as ValueError:
+ return self.do_http_error()
+ text = urllib.parse.unquote(text)
+ self.alerts_manager.emit(QtCore.SIGNAL('alerts_text'), [text])
+ success = True
+ else:
+ success = False
+ return json.dumps({'results': {'success': success}}).encode()
+
+ def controller_text(self, var):
+ """
+ Perform an action on the slide controller.
+ """
+ current_item = self.live_controller.service_item
+ data = []
+ if current_item:
+ for index, frame in enumerate(current_item.get_frames()):
+ item = {}
+ if current_item.is_text():
+ if frame['verseTag']:
+ item['tag'] = str(frame['verseTag'])
+ else:
+ item['tag'] = str(index + 1)
+ item['text'] = str(frame['text'])
+ item['html'] = str(frame['html'])
+ else:
+ item['tag'] = str(index + 1)
+ item['text'] = str(frame['title'])
+ item['html'] = str(frame['title'])
+ item['selected'] = (self.live_controller.selected_row == index)
+ data.append(item)
+ json_data = {'results': {'slides': data}}
+ if current_item:
+ json_data['results']['item'] = self.live_controller.service_item.unique_identifier
+ return json.dumps(json_data).encode()
+
+ def controller(self, display_type, action):
+ """
+ Perform an action on the slide controller.
+
+ ``display_type``
+ This is the type of slide controller, either ``preview`` or ``live``.
+
+ ``action``
+ The action to perform.
+ """
+ event = 'slidecontroller_%s_%s' % (display_type, action)
+ if self.request_data:
+ try:
+ data = json.loads(self.request_data)['request']['id']
+ except KeyError as ValueError:
+ return self.do_http_error()
+ log.info(data)
+ # This slot expects an int within a list.
+ self.live_controller.emit(QtCore.SIGNAL(event), [data])
+ else:
+ self.live_controller.emit(QtCore.SIGNAL(event))
+ json_data = {'results': {'success': True}}
+ return json.dumps(json_data).encode()
+
+ def service_list(self):
+ """
+ Handles requests for service items in the service manager
+
+ ``action``
+ The action to perform.
+ """
+ return json.dumps({'results': {'items': self._get_service_items()}}).encode()
+
+ def service(self, action):
+ """
+ Handles requests for service items in the service manager
+
+ ``action``
+ The action to perform.
+ """
+ event = 'servicemanager_%s_item' % action
+ if self.request_data:
+ try:
+ data = json.loads(self.request_data)['request']['id']
+ except KeyError:
+ return self.do_http_error()
+ self.service_manager.emit(QtCore.SIGNAL(event), data)
+ else:
+ Registry().execute(event)
+ return json.dumps({'results': {'success': True}}).encode()
+
+ def plugin_info(self, action):
+ """
+ Return plugin related information, based on the action.
+
+ ``action``
+ The action to perform. If *search* return a list of plugin names
+ which support search.
+ """
+ if action == 'search':
+ searches = []
+ for plugin in self.plugin_manager.plugins:
+ if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
+ searches.append([plugin.name, str(plugin.text_strings[StringContent.Name]['plural'])])
+ return json.dumps({'results': {'items': searches}}).encode()
+
+ def search(self, plugin_name):
+ """
+ Return a list of items that match the search text.
+
+ ``plugin``
+ The plugin name to search in.
+ """
+ try:
+ text = json.loads(self.request_data)['request']['text']
+ except KeyError as ValueError:
+ return self.do_http_error()
+ text = urllib.parse.unquote(text)
+ plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
+ if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
+ results = plugin.media_item.search(text, False)
+ else:
+ results = []
+ return json.dumps({'results': {'items': results}}).encode()
+
+ def go_live(self, plugin_name):
+ """
+ Go live on an item of type ``plugin``.
+ """
+ try:
+ id = json.loads(self.request_data)['request']['id']
+ except KeyError as ValueError:
+ return self.do_http_error()
+ plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
+ if plugin.status == PluginStatus.Active and plugin.media_item:
+ plugin.media_item.emit(QtCore.SIGNAL('%s_go_live' % plugin_name), [id, True])
+ return self.do_http_success()
+
+ def add_to_service(self, plugin_name):
+ """
+ Add item of type ``plugin_name`` to the end of the service.
+ """
+ try:
+ id = json.loads(self.request_data)['request']['id']
+ except KeyError as ValueError:
+ return self.do_http_error()
+ plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
+ if plugin.status == PluginStatus.Active and plugin.media_item:
+ item_id = plugin.media_item.create_item_from_id(id)
+ plugin.media_item.emit(QtCore.SIGNAL('%s_add_to_service' % plugin_name), [item_id, True])
+ self.do_http_success()
+
+ def _get_service_manager(self):
+ """
+ Adds the service manager to the class dynamically
+ """
+ if not hasattr(self, '_service_manager'):
+ self._service_manager = Registry().get('service_manager')
+ return self._service_manager
+
+ service_manager = property(_get_service_manager)
+
+ def _get_live_controller(self):
+ """
+ Adds the live controller to the class dynamically
+ """
+ if not hasattr(self, '_live_controller'):
+ self._live_controller = Registry().get('live_controller')
+ return self._live_controller
+
+ live_controller = property(_get_live_controller)
+
+ def _get_plugin_manager(self):
+ """
+ Adds the plugin manager to the class dynamically
+ """
+ if not hasattr(self, '_plugin_manager'):
+ self._plugin_manager = Registry().get('plugin_manager')
+ return self._plugin_manager
+
+ plugin_manager = property(_get_plugin_manager)
+
+ def _get_alerts_manager(self):
+ """
+ Adds the alerts manager to the class dynamically
+ """
+ if not hasattr(self, '_alerts_manager'):
+ self._alerts_manager = Registry().get('alerts_manager')
+ return self._alerts_manager
+
+ alerts_manager = property(_get_alerts_manager)
=== modified file 'openlp/plugins/remotes/lib/httpserver.py'
--- openlp/plugins/remotes/lib/httpserver.py 2013-08-31 18:17:38 +0000
+++ openlp/plugins/remotes/lib/httpserver.py 2013-09-19 17:06:40 +0000
@@ -31,661 +31,122 @@
The :mod:`http` module contains the API web server. This is a lightweight web
server used by remotes to interact with OpenLP. It uses JSON to communicate with
the remotes.
-
-*Routes:*
-
-``/``
- Go to the web interface.
-
-``/stage``
- Show the stage view.
-
-``/files/{filename}``
- Serve a static file.
-
-``/stage/api/poll``
- Poll to see if there are any changes. Returns a JSON-encoded dict of
- any changes that occurred::
-
- {"results": {"type": "controller"}}
-
- Or, if there were no results, False::
-
- {"results": False}
-
-``/api/display/{hide|show}``
- Blank or unblank the screen.
-
-``/api/alert``
- Sends an alert message to the alerts plugin. This method expects a
- JSON-encoded dict like this::
-
- {"request": {"text": "<your alert text>"}}
-
-``/api/controller/{live|preview}/{action}``
- Perform ``{action}`` on the live or preview controller. Valid actions
- are:
-
- ``next``
- Load the next slide.
-
- ``previous``
- Load the previous slide.
-
- ``set``
- Set a specific slide. Requires an id return in a JSON-encoded dict like
- this::
-
- {"request": {"id": 1}}
-
- ``first``
- Load the first slide.
-
- ``last``
- Load the last slide.
-
- ``text``
- Fetches the text of the current song. The output is a JSON-encoded
- dict which looks like this::
-
- {"result": {"slides": ["...", "..."]}}
-
-``/api/service/{action}``
- Perform ``{action}`` on the service manager (e.g. go live). Data is
- passed as a json-encoded ``data`` parameter. Valid actions are:
-
- ``next``
- Load the next item in the service.
-
- ``previous``
- Load the previews item in the service.
-
- ``set``
- Set a specific item in the service. Requires an id returned in a
- JSON-encoded dict like this::
-
- {"request": {"id": 1}}
-
- ``list``
- Request a list of items in the service. Returns a list of items in the
- current service in a JSON-encoded dict like this::
-
- {"results": {"items": [{...}, {...}]}}
"""
-import json
+import ssl
+import socket
+import os
import logging
-import os
-import re
-import urllib.request, urllib.parse, urllib.error
-import urllib.parse
-import cherrypy
+from urllib.parse import urlparse, parse_qs
-from mako.template import Template
from PyQt4 import QtCore
-from openlp.core.lib import Registry, Settings, PluginStatus, StringContent, image_to_byte
-from openlp.core.utils import AppLocation, translate
-
-from hashlib import sha1
+from openlp.core.lib import Settings
+from openlp.core.utils import AppLocation
+
+from openlp.plugins.remotes.lib import HttpRouter
+
+from socketserver import BaseServer, ThreadingMixIn
+from http.server import BaseHTTPRequestHandler, HTTPServer
log = logging.getLogger(__name__)
-def make_sha_hash(password):
- """
- Create an encrypted password for the given password.
- """
- log.debug("make_sha_hash")
- return sha1(password.encode()).hexdigest()
-
-
-def fetch_password(username):
- """
- Fetch the password for a provided user.
- """
- log.debug("Fetch Password")
- if username != Settings().value('remotes/user id'):
- return None
- return make_sha_hash(Settings().value('remotes/password'))
-
-
-class HttpServer(object):
- """
- Ability to control OpenLP via a web browser.
- This class controls the Cherrypy server and configuration.
- """
- _cp_config = {
- 'tools.sessions.on': True,
- 'tools.auth.on': True
- }
-
+class CustomHandler(BaseHTTPRequestHandler, HttpRouter):
+ """
+ Stateless session handler to handle the HTTP request and process it.
+ This class handles just the overrides to the base methods and the logic to invoke the
+ methods within the HttpRouter class.
+ DO not try change the structure as this is as per the documentation.
+ """
+
+ def do_POST(self):
+ """
+ Present pages / data and invoke URL level user authentication.
+ """
+ self.do_post_processor()
+
+ def do_GET(self):
+ """
+ Present pages / data and invoke URL level user authentication.
+ """
+ self.do_post_processor()
+
+
+class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
+ pass
+
+
+class HttpThread(QtCore.QThread):
+ """
+ A special Qt thread class to allow the HTTP server to run at the same time as the UI.
+ """
+ def __init__(self, server):
+ """
+ Constructor for the thread class.
+
+ ``server``
+ The http server class.
+ """
+ super(HttpThread, self).__init__(None)
+ self.http_server = server
+
+ def run(self):
+ """
+ Run the thread.
+ """
+ self.http_server.start_server()
+
+
+class OpenLPServer():
def __init__(self):
"""
- Initialise the http server, and start the server.
+ Initialise the http server, and start the server of the correct type http / https
"""
log.debug('Initialise httpserver')
self.settings_section = 'remotes'
- self.router = HttpRouter()
+ self.http_thread = HttpThread(self)
+ self.http_thread.start()
def start_server(self):
"""
- Start the http server based on configuration.
- """
- log.debug('Start CherryPy server')
- # Define to security levels and inject the router code
- self.root = self.Public()
- self.root.files = self.Files()
- self.root.stage = self.Stage()
- self.root.main = self.Main()
- self.root.router = self.router
- self.root.files.router = self.router
- self.root.stage.router = self.router
- self.root.main.router = self.router
- cherrypy.tree.mount(self.root, '/', config=self.define_config())
- # Turn off the flood of access messages cause by poll
- cherrypy.log.access_log.propagate = False
- cherrypy.engine.start()
-
- def define_config(self):
- """
- Define the configuration of the server.
- """
+ Start the correct server and save the handler
+ """
+ address = Settings().value(self.settings_section + '/ip address')
if Settings().value(self.settings_section + '/https enabled'):
port = Settings().value(self.settings_section + '/https port')
- address = Settings().value(self.settings_section + '/ip address')
- local_data = AppLocation.get_directory(AppLocation.DataDir)
- cherrypy.config.update({'server.socket_host': str(address),
- 'server.socket_port': port,
- 'server.ssl_certificate': os.path.join(local_data, 'remotes', 'openlp.crt'),
- 'server.ssl_private_key': os.path.join(local_data, 'remotes', 'openlp.key')})
+ self.httpd = HTTPSServer((address, port), CustomHandler)
+ log.debug('Started ssl httpd...')
else:
port = Settings().value(self.settings_section + '/port')
- address = Settings().value(self.settings_section + '/ip address')
- cherrypy.config.update({'server.socket_host': str(address)})
- cherrypy.config.update({'server.socket_port': port})
- cherrypy.config.update({'environment': 'embedded'})
- cherrypy.config.update({'engine.autoreload_on': False})
- directory_config = {'/': {'tools.staticdir.on': True,
- 'tools.staticdir.dir': self.router.html_dir,
- 'tools.basic_auth.on': Settings().value('remotes/authentication enabled'),
- 'tools.basic_auth.realm': 'OpenLP Remote Login',
- 'tools.basic_auth.users': fetch_password,
- 'tools.basic_auth.encrypt': make_sha_hash},
- '/files': {'tools.staticdir.on': True,
- 'tools.staticdir.dir': self.router.html_dir,
- 'tools.basic_auth.on': False},
- '/stage': {'tools.staticdir.on': True,
- 'tools.staticdir.dir': self.router.html_dir,
- 'tools.basic_auth.on': False},
- '/main': {'tools.staticdir.on': True,
- 'tools.staticdir.dir': self.router.html_dir,
- 'tools.basic_auth.on': False}}
- return directory_config
-
- class Public(object):
- """
- Main access class with may have security enabled on it.
- """
- @cherrypy.expose
- def default(self, *args, **kwargs):
- self.router.request_data = None
- if isinstance(kwargs, dict):
- self.router.request_data = kwargs.get('data', None)
- url = urllib.parse.urlparse(cherrypy.url())
- return self.router.process_http_request(url.path, *args)
-
- class Files(object):
- """
- Provides access to files and has no security available. These are read only accesses
- """
- @cherrypy.expose
- def default(self, *args, **kwargs):
- url = urllib.parse.urlparse(cherrypy.url())
- return self.router.process_http_request(url.path, *args)
-
- class Stage(object):
- """
- Stage view is read only so security is not relevant and would reduce it's usability
- """
- @cherrypy.expose
- def default(self, *args, **kwargs):
- url = urllib.parse.urlparse(cherrypy.url())
- return self.router.process_http_request(url.path, *args)
-
- class Main(object):
- """
- Main view is read only so security is not relevant and would reduce it's usability
- """
- @cherrypy.expose
- def default(self, *args, **kwargs):
- url = urllib.parse.urlparse(cherrypy.url())
- return self.router.process_http_request(url.path, *args)
-
- def close(self):
- """
- Close down the http server.
- """
- log.debug('close http server')
- cherrypy.engine.exit()
-
-
-class HttpRouter(object):
- """
- This code is called by the HttpServer upon a request and it processes it based on the routing table.
- """
- def __init__(self):
- """
- Initialise the router
- """
- self.routes = [
- ('^/$', self.serve_file),
- ('^/(stage)$', self.serve_file),
- ('^/(main)$', self.serve_file),
- (r'^/files/(.*)$', self.serve_file),
- (r'^/api/poll$', self.poll),
- (r'^/stage/poll$', self.poll),
- (r'^/main/poll$', self.main_poll),
- (r'^/main/image$', self.main_image),
- (r'^/api/controller/(live|preview)/(.*)$', self.controller),
- (r'^/stage/controller/(live|preview)/(.*)$', self.controller),
- (r'^/api/service/(.*)$', self.service),
- (r'^/stage/service/(.*)$', self.service),
- (r'^/api/display/(hide|show|blank|theme|desktop)$', self.display),
- (r'^/api/alert$', self.alert),
- (r'^/api/plugin/(search)$', self.plugin_info),
- (r'^/api/(.*)/search$', self.search),
- (r'^/api/(.*)/live$', self.go_live),
- (r'^/api/(.*)/add$', self.add_to_service)
- ]
- self.translate()
- self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), 'remotes', 'html')
-
- def process_http_request(self, url_path, *args):
- """
- Common function to process HTTP requests
-
- ``url_path``
- The requested URL.
-
- ``*args``
- Any passed data.
- """
- response = None
- for route, func in self.routes:
- match = re.match(route, url_path)
- if match:
- log.debug('Route "%s" matched "%s"', route, url_path)
- args = []
- for param in match.groups():
- args.append(param)
- response = func(*args)
- break
- if response:
- return response
- else:
- log.debug('Path not found %s', url_path)
- return self._http_not_found()
-
- def _get_service_items(self):
- """
- Read the service item in use and return the data as a json object
- """
- service_items = []
- if self.live_controller.service_item:
- current_unique_identifier = self.live_controller.service_item.unique_identifier
- else:
- current_unique_identifier = None
- for item in self.service_manager.service_items:
- service_item = item['service_item']
- service_items.append({
- 'id': str(service_item.unique_identifier),
- 'title': str(service_item.get_display_title()),
- 'plugin': str(service_item.name),
- 'notes': str(service_item.notes),
- 'selected': (service_item.unique_identifier == current_unique_identifier)
- })
- return service_items
-
- def translate(self):
- """
- Translate various strings in the mobile app.
- """
- self.template_vars = {
- 'app_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Remote'),
- 'stage_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Stage View'),
- 'live_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Live View'),
- 'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
- 'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
- 'alerts': translate('RemotePlugin.Mobile', 'Alerts'),
- 'search': translate('RemotePlugin.Mobile', 'Search'),
- 'home': translate('RemotePlugin.Mobile', 'Home'),
- 'refresh': translate('RemotePlugin.Mobile', 'Refresh'),
- 'blank': translate('RemotePlugin.Mobile', 'Blank'),
- 'theme': translate('RemotePlugin.Mobile', 'Theme'),
- 'desktop': translate('RemotePlugin.Mobile', 'Desktop'),
- 'show': translate('RemotePlugin.Mobile', 'Show'),
- 'prev': translate('RemotePlugin.Mobile', 'Prev'),
- 'next': translate('RemotePlugin.Mobile', 'Next'),
- 'text': translate('RemotePlugin.Mobile', 'Text'),
- 'show_alert': translate('RemotePlugin.Mobile', 'Show Alert'),
- 'go_live': translate('RemotePlugin.Mobile', 'Go Live'),
- 'add_to_service': translate('RemotePlugin.Mobile', 'Add to Service'),
- 'add_and_go_to_service': translate('RemotePlugin.Mobile', 'Add & Go to Service'),
- 'no_results': translate('RemotePlugin.Mobile', 'No Results'),
- 'options': translate('RemotePlugin.Mobile', 'Options'),
- 'service': translate('RemotePlugin.Mobile', 'Service'),
- 'slides': translate('RemotePlugin.Mobile', 'Slides')
- }
-
- def serve_file(self, file_name=None):
- """
- Send a file to the socket. For now, just a subset of file types and must be top level inside the html folder.
- If subfolders requested return 404, easier for security for the present.
-
- Ultimately for i18n, this could first look for xx/file.html before falling back to file.html.
- where xx is the language, e.g. 'en'
- """
- log.debug('serve file request %s' % file_name)
- if not file_name:
- file_name = 'index.html'
- elif file_name == 'stage':
- file_name = 'stage.html'
- elif file_name == 'main':
- file_name = 'main.html'
- path = os.path.normpath(os.path.join(self.html_dir, file_name))
- if not path.startswith(self.html_dir):
- return self._http_not_found()
- ext = os.path.splitext(file_name)[1]
- html = None
- if ext == '.html':
- mimetype = 'text/html'
- variables = self.template_vars
- html = Template(filename=path, input_encoding='utf-8', output_encoding='utf-8').render(**variables)
- elif ext == '.css':
- mimetype = 'text/css'
- elif ext == '.js':
- mimetype = 'application/x-javascript'
- elif ext == '.jpg':
- mimetype = 'image/jpeg'
- elif ext == '.gif':
- mimetype = 'image/gif'
- elif ext == '.png':
- mimetype = 'image/png'
- else:
- mimetype = 'text/plain'
- file_handle = None
- try:
- if html:
- content = html
- else:
- file_handle = open(path, 'rb')
- log.debug('Opened %s' % path)
- content = file_handle.read()
- except IOError:
- log.exception('Failed to open %s' % path)
- return self._http_not_found()
- finally:
- if file_handle:
- file_handle.close()
- cherrypy.response.headers['Content-Type'] = mimetype
- return content
-
- def poll(self):
- """
- Poll OpenLP to determine the current slide number and item name.
- """
- result = {
- 'service': self.service_manager.service_id,
- 'slide': self.live_controller.selected_row or 0,
- 'item': self.live_controller.service_item.unique_identifier if self.live_controller.service_item else '',
- 'twelve': Settings().value('remotes/twelve hour'),
- 'blank': self.live_controller.blank_screen.isChecked(),
- 'theme': self.live_controller.theme_screen.isChecked(),
- 'display': self.live_controller.desktop_screen.isChecked()
- }
- cherrypy.response.headers['Content-Type'] = 'application/json'
- return json.dumps({'results': result}).encode()
-
- def main_poll(self):
- """
- Poll OpenLP to determine the current slide count.
- """
- result = {
- 'slide_count': self.live_controller.slide_count
- }
- cherrypy.response.headers['Content-Type'] = 'application/json'
- return json.dumps({'results': result}).encode()
-
- def main_image(self):
- """
- Return the latest display image as a byte stream.
- """
- result = {
- 'slide_image': 'data:image/png;base64,' + str(image_to_byte(self.live_controller.slide_image))
- }
- cherrypy.response.headers['Content-Type'] = 'application/json'
- return json.dumps({'results': result}).encode()
-
- def display(self, action):
- """
- Hide or show the display screen.
- This is a cross Thread call and UI is updated so Events need to be used.
-
- ``action``
- This is the action, either ``hide`` or ``show``.
- """
- self.live_controller.emit(QtCore.SIGNAL('slidecontroller_toggle_display'), action)
- cherrypy.response.headers['Content-Type'] = 'application/json'
- return json.dumps({'results': {'success': True}}).encode()
-
- def alert(self):
- """
- Send an alert.
- """
- plugin = self.plugin_manager.get_plugin_by_name("alerts")
- if plugin.status == PluginStatus.Active:
- try:
- text = json.loads(self.request_data)['request']['text']
- except KeyError as ValueError:
- return self._http_bad_request()
- text = urllib.parse.unquote(text)
- self.alerts_manager.emit(QtCore.SIGNAL('alerts_text'), [text])
- success = True
- else:
- success = False
- cherrypy.response.headers['Content-Type'] = 'application/json'
- return json.dumps({'results': {'success': success}}).encode()
-
- def controller(self, display_type, action):
- """
- Perform an action on the slide controller.
-
- ``display_type``
- This is the type of slide controller, either ``preview`` or ``live``.
-
- ``action``
- The action to perform.
- """
- event = 'slidecontroller_%s_%s' % (display_type, action)
- if action == 'text':
- current_item = self.live_controller.service_item
- data = []
- if current_item:
- for index, frame in enumerate(current_item.get_frames()):
- item = {}
- if current_item.is_text():
- if frame['verseTag']:
- item['tag'] = str(frame['verseTag'])
- else:
- item['tag'] = str(index + 1)
- item['text'] = str(frame['text'])
- item['html'] = str(frame['html'])
- else:
- item['tag'] = str(index + 1)
- item['text'] = str(frame['title'])
- item['html'] = str(frame['title'])
- item['selected'] = (self.live_controller.selected_row == index)
- data.append(item)
- json_data = {'results': {'slides': data}}
- if current_item:
- json_data['results']['item'] = self.live_controller.service_item.unique_identifier
- else:
- if self.request_data:
- try:
- data = json.loads(self.request_data)['request']['id']
- except KeyError as ValueError:
- return self._http_bad_request()
- log.info(data)
- # This slot expects an int within a list.
- self.live_controller.emit(QtCore.SIGNAL(event), [data])
- else:
- self.live_controller.emit(QtCore.SIGNAL(event))
- json_data = {'results': {'success': True}}
- cherrypy.response.headers['Content-Type'] = 'application/json'
- return json.dumps(json_data).encode()
-
- def service(self, action):
- """
- Handles requests for service items in the service manager
-
- ``action``
- The action to perform.
- """
- event = 'servicemanager_%s' % action
- if action == 'list':
- cherrypy.response.headers['Content-Type'] = 'application/json'
- return json.dumps({'results': {'items': self._get_service_items()}}).encode()
- event += '_item'
- if self.request_data:
- try:
- data = json.loads(self.request_data)['request']['id']
- except KeyError:
- return self._http_bad_request()
- self.service_manager.emit(QtCore.SIGNAL(event), data)
- else:
- Registry().execute(event)
- cherrypy.response.headers['Content-Type'] = 'application/json'
- return json.dumps({'results': {'success': True}}).encode()
-
- def plugin_info(self, action):
- """
- Return plugin related information, based on the action.
-
- ``action``
- The action to perform. If *search* return a list of plugin names
- which support search.
- """
- if action == 'search':
- searches = []
- for plugin in self.plugin_manager.plugins:
- if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
- searches.append([plugin.name, str(plugin.text_strings[StringContent.Name]['plural'])])
- cherrypy.response.headers['Content-Type'] = 'application/json'
- return json.dumps({'results': {'items': searches}}).encode()
-
- def search(self, plugin_name):
- """
- Return a list of items that match the search text.
-
- ``plugin``
- The plugin name to search in.
- """
- try:
- text = json.loads(self.request_data)['request']['text']
- except KeyError as ValueError:
- return self._http_bad_request()
- text = urllib.parse.unquote(text)
- plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
- if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
- results = plugin.media_item.search(text, False)
- else:
- results = []
- cherrypy.response.headers['Content-Type'] = 'application/json'
- return json.dumps({'results': {'items': results}}).encode()
-
- def go_live(self, plugin_name):
- """
- Go live on an item of type ``plugin``.
- """
- try:
- id = json.loads(self.request_data)['request']['id']
- except KeyError as ValueError:
- return self._http_bad_request()
- plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
- if plugin.status == PluginStatus.Active and plugin.media_item:
- plugin.media_item.emit(QtCore.SIGNAL('%s_go_live' % plugin_name), [id, True])
- return self._http_success()
-
- def add_to_service(self, plugin_name):
- """
- Add item of type ``plugin_name`` to the end of the service.
- """
- try:
- id = json.loads(self.request_data)['request']['id']
- except KeyError as ValueError:
- return self._http_bad_request()
- plugin = self.plugin_manager.get_plugin_by_name(plugin_name)
- if plugin.status == PluginStatus.Active and plugin.media_item:
- item_id = plugin.media_item.create_item_from_id(id)
- plugin.media_item.emit(QtCore.SIGNAL('%s_add_to_service' % plugin_name), [item_id, True])
- self._http_success()
-
- def _http_success(self):
- """
- Set the HTTP success return code.
- """
- cherrypy.response.status = 200
-
- def _http_bad_request(self):
- """
- Set the HTTP bad response return code.
- """
- cherrypy.response.status = 400
-
- def _http_not_found(self):
- """
- Set the HTTP not found return code.
- """
- cherrypy.response.status = 404
- cherrypy.response.body = [b'<html><body>Sorry, an error occurred </body></html>']
-
- def _get_service_manager(self):
- """
- Adds the service manager to the class dynamically
- """
- if not hasattr(self, '_service_manager'):
- self._service_manager = Registry().get('service_manager')
- return self._service_manager
-
- service_manager = property(_get_service_manager)
-
- def _get_live_controller(self):
- """
- Adds the live controller to the class dynamically
- """
- if not hasattr(self, '_live_controller'):
- self._live_controller = Registry().get('live_controller')
- return self._live_controller
-
- live_controller = property(_get_live_controller)
-
- def _get_plugin_manager(self):
- """
- Adds the plugin manager to the class dynamically
- """
- if not hasattr(self, '_plugin_manager'):
- self._plugin_manager = Registry().get('plugin_manager')
- return self._plugin_manager
-
- plugin_manager = property(_get_plugin_manager)
-
- def _get_alerts_manager(self):
- """
- Adds the alerts manager to the class dynamically
- """
- if not hasattr(self, '_alerts_manager'):
- self._alerts_manager = Registry().get('alerts_manager')
- return self._alerts_manager
-
- alerts_manager = property(_get_alerts_manager)
+ self.httpd = ThreadingHTTPServer((address, port), CustomHandler)
+ log.debug('Started non ssl httpd...')
+ self.httpd.serve_forever()
+
+ def stop_server(self):
+ """
+ Stop the server
+ """
+ self.http_thread.exit(0)
+ self.httpd = None
+ log.debug('Stopped the server.')
+
+
+class HTTPSServer(HTTPServer):
+ def __init__(self, address, handler):
+ """
+ Initialise the secure handlers for the SSL server if required.s
+ """
+ BaseServer.__init__(self, address, handler)
+ local_data = AppLocation.get_directory(AppLocation.DataDir)
+ self.socket = ssl.SSLSocket(
+ sock=socket.socket(self.address_family, self.socket_type),
+ ssl_version=ssl.PROTOCOL_TLSv1,
+ certfile=os.path.join(local_data, 'remotes', 'openlp.crt'),
+ keyfile=os.path.join(local_data, 'remotes', 'openlp.key'),
+ server_side=True)
+ self.server_bind()
+ self.server_activate()
+
+
+
=== modified file 'openlp/plugins/remotes/remoteplugin.py'
--- openlp/plugins/remotes/remoteplugin.py 2013-08-31 18:17:38 +0000
+++ openlp/plugins/remotes/remoteplugin.py 2013-09-19 17:06:40 +0000
@@ -28,11 +28,10 @@
###############################################################################
import logging
-
-from PyQt4 import QtGui
+import time
from openlp.core.lib import Plugin, StringContent, translate, build_icon
-from openlp.plugins.remotes.lib import RemoteTab, HttpServer
+from openlp.plugins.remotes.lib import RemoteTab, OpenLPServer
log = logging.getLogger(__name__)
@@ -67,8 +66,7 @@
"""
log.debug('initialise')
super(RemotesPlugin, self).initialise()
- self.server = HttpServer()
- self.server.start_server()
+ self.server = OpenLPServer()
def finalise(self):
"""
@@ -77,7 +75,7 @@
log.debug('finalise')
super(RemotesPlugin, self).finalise()
if self.server:
- self.server.close()
+ self.server.stop_server()
self.server = None
def about(self):
@@ -109,5 +107,6 @@
Called when Config is changed to restart the server on new address or port
"""
log.debug('remote config changed')
- self.main_window.information_message(translate('RemotePlugin', 'Configuration Change'),
- translate('RemotePlugin', 'OpenLP will need to be restarted for the Remote changes to become active.'))
+ self.finalise()
+ time.sleep(0.5)
+ self.initialise()
=== modified file 'scripts/check_dependencies.py'
--- scripts/check_dependencies.py 2013-09-07 21:29:31 +0000
+++ scripts/check_dependencies.py 2013-09-19 17:06:40 +0000
@@ -48,6 +48,7 @@
IS_WIN = sys.platform.startswith('win')
+
VERS = {
'Python': '3.0',
'PyQt4': '4.6',
@@ -84,7 +85,6 @@
'enchant',
'bs4',
'mako',
- 'cherrypy',
'uno',
]
@@ -98,6 +98,7 @@
w = sys.stdout.write
+
def check_vers(version, required, text):
if not isinstance(version, str):
version = '.'.join(map(str, version))
@@ -111,13 +112,16 @@
w('FAIL' + os.linesep)
return False
+
def print_vers_fail(required, text):
print(' %s >= %s ... FAIL' % (text, required))
+
def verify_python():
if not check_vers(list(sys.version_info), VERS['Python'], text='Python'):
exit(1)
+
def verify_versions():
print('Verifying version of modules...')
try:
@@ -138,6 +142,7 @@
except ImportError:
print_vers_fail(VERS['enchant'], 'enchant')
+
def check_module(mod, text='', indent=' '):
space = (30 - len(mod) - len(text)) * ' '
w(indent + '%s%s... ' % (mod, text) + space)
@@ -148,6 +153,7 @@
w('FAIL')
w(os.linesep)
+
def verify_pyenchant():
w('Enchant (spell checker)... ')
try:
@@ -160,6 +166,7 @@
except ImportError:
w('FAIL' + os.linesep)
+
def verify_pyqt():
w('Qt4 image formats... ')
try:
@@ -174,22 +181,19 @@
except ImportError:
w('FAIL' + os.linesep)
+
def main():
verify_python()
-
print('Checking for modules...')
for m in MODULES:
check_module(m)
-
print('Checking for optional modules...')
for m in OPTIONAL_MODULES:
check_module(m[0], text=m[1])
-
if IS_WIN:
print('Checking for Windows specific modules...')
for m in WIN32_MODULES:
check_module(m)
-
verify_versions()
verify_pyqt()
verify_pyenchant()
=== modified file 'tests/functional/openlp_plugins/remotes/test_router.py'
--- tests/functional/openlp_plugins/remotes/test_router.py 2013-08-31 18:17:38 +0000
+++ tests/functional/openlp_plugins/remotes/test_router.py 2013-09-19 17:06:40 +0000
@@ -8,7 +8,7 @@
from mock import MagicMock
from openlp.core.lib import Settings
-from openlp.plugins.remotes.lib.httpserver import HttpRouter, fetch_password, make_sha_hash
+from openlp.plugins.remotes.lib.httpserver import HttpRouter
from PyQt4 import QtGui
__default_settings__ = {
@@ -44,40 +44,22 @@
del self.application
os.unlink(self.ini_file)
- def fetch_password_unknown_test(self):
- """
- Test the fetch password code with an unknown userid
- """
- # GIVEN: A default configuration
- # WHEN: called with the defined userid
- password = fetch_password('itwinkle')
-
- # THEN: the function should return None
- self.assertEqual(password, None, 'The result for fetch_password should be None')
-
- def fetch_password_known_test(self):
- """
- Test the fetch password code with the defined userid
- """
- # GIVEN: A default configuration
- # WHEN: called with the defined userid
- password = fetch_password('openlp')
- required_password = make_sha_hash('password')
-
- # THEN: the function should return the correct password
- self.assertEqual(password, required_password, 'The result for fetch_password should be the defined password')
-
- def sha_password_encrypter_test(self):
- """
- Test hash password function
- """
- # GIVEN: A default configuration
- # WHEN: called with the defined userid
- required_password = make_sha_hash('password')
- test_value = '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8'
-
- # THEN: the function should return the correct password
- self.assertEqual(required_password, test_value,
+ def password_encrypter_test(self):
+ """
+ Test hash userid and password function
+ """
+ # GIVEN: A default configuration
+ Settings().setValue('remotes/user id', 'openlp')
+ Settings().setValue('remotes/password', 'password')
+
+ # WHEN: called with the defined userid
+ router = HttpRouter()
+ router.initialise()
+ test_value = 'b3BlbmxwOnBhc3N3b3Jk'
+ print(router.auth)
+
+ # THEN: the function should return the correct password
+ self.assertEqual(router.auth, test_value,
'The result for make_sha_hash should return the correct encrypted password')
def process_http_request_test(self):
@@ -85,15 +67,18 @@
Test the router control functionality
"""
# GIVEN: A testing set of Routes
+ router = HttpRouter()
mocked_function = MagicMock()
test_route = [
- (r'^/stage/api/poll$', mocked_function),
+ (r'^/stage/api/poll$', {'function': mocked_function, 'secure': False}),
]
- self.router.routes = test_route
+ router.routes = test_route
# WHEN: called with a poll route
- self.router.process_http_request('/stage/api/poll', None)
+ function, args = router.process_http_request('/stage/api/poll', None)
# THEN: the function should have been called only once
- assert mocked_function.call_count == 1, \
- 'The mocked function should have been matched and called once.'
+ assert function['function'] == mocked_function, \
+ 'The mocked function should match defined value.'
+ assert function['secure'] == False, \
+ 'The mocked function should not require any security.'
\ No newline at end of file
=== renamed file 'tests/interfaces/openlp_plugins/remotes/test_server.py' => 'tests/interfaces/openlp_plugins/remotes/test_server.py.THIS'