← Back to team overview

openlp-core team mailing list archive

[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 &amp; 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 &amp; 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'