← 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:
  Andreas Preikschat (googol)
  Raoul Snyman (raoul-snyman)
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/158788

Test merge to see what it looks like.

Code now works 

Tests now added.

SSL and Login working correctly and works with an http client (test).

import urllib2
from BeautifulSoup import BeautifulSoup, NavigableString, Tag

theurl = "http://localhost:4316";
username = "openlp"
password = "password"

passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
passman.add_password(None, theurl, username, password)
authhandler = urllib2.HTTPBasicAuthHandler(passman)
opener = urllib2.build_opener(authhandler)
urllib2.install_opener(opener)

f = urllib2.urlopen(theurl)
print f.read()
-- 
https://code.launchpad.net/~trb143/openlp/cherrypy/+merge/158788
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/lib/mediamanageritem.py'
--- openlp/core/lib/mediamanageritem.py	2013-03-23 06:46:41 +0000
+++ openlp/core/lib/mediamanageritem.py	2013-04-14 16:01:29 +0000
@@ -103,6 +103,9 @@
         self.retranslateUi()
         self.auto_select_id = -1
         Registry().register_function(u'%s_service_load' % self.plugin.name, self.service_load)
+        # Need to use event as called across threads and UI is updated
+        QtCore.QObject.connect(self, QtCore.SIGNAL(u'%s_go_live' % self.plugin.name), self.go_live_remote)
+        QtCore.QObject.connect(self, QtCore.SIGNAL(u'%s_add_to_service' % self.plugin.name), self.add_to_service_remote)
 
     def required_icons(self):
         """
@@ -481,6 +484,15 @@
         else:
             self.go_live()
 
+    def go_live_remote(self, message):
+        """
+        Remote Call wrapper
+
+        ``message``
+            The passed data item_id:Remote.
+        """
+        self.go_live(message[0], remote=message[1])
+
     def go_live(self, item_id=None, remote=False):
         """
         Make the currently selected item go live.
@@ -523,6 +535,15 @@
                 for item in items:
                     self.add_to_service(item)
 
+    def add_to_service_remote(self, message):
+        """
+        Remote Call wrapper
+
+        ``message``
+            The passed data item:Remote.
+        """
+        self.add_to_service(message[0], remote=message[1])
+
     def add_to_service(self, item=None, replace=None, remote=False):
         """
         Add this item to the current service.

=== modified file 'openlp/core/lib/plugin.py'
--- openlp/core/lib/plugin.py	2013-03-19 22:00:50 +0000
+++ openlp/core/lib/plugin.py	2013-04-14 16:01:29 +0000
@@ -103,7 +103,7 @@
     ``add_export_menu_Item(export_menu)``
         Add an item to the Export menu.
 
-    ``create_settings_Tab()``
+    ``create_settings_tab()``
         Creates a new instance of SettingsTabItem to be used in the Settings
         dialog.
 
@@ -252,7 +252,7 @@
         """
         pass
 
-    def create_settings_Tab(self, parent):
+    def create_settings_tab(self, parent):
         """
         Create a tab for the settings window to display the configurable options
         for this plugin to the user.

=== modified file 'openlp/core/lib/pluginmanager.py'
--- openlp/core/lib/pluginmanager.py	2013-03-19 19:43:22 +0000
+++ openlp/core/lib/pluginmanager.py	2013-04-14 16:01:29 +0000
@@ -153,7 +153,7 @@
         """
         for plugin in self.plugins:
             if plugin.status is not PluginStatus.Disabled:
-                plugin.create_settings_Tab(self.settings_form)
+                plugin.create_settings_tab(self.settings_form)
 
     def hook_import_menu(self):
         """

=== modified file 'openlp/core/lib/serviceitem.py'
--- openlp/core/lib/serviceitem.py	2013-03-19 19:43:22 +0000
+++ openlp/core/lib/serviceitem.py	2013-04-14 16:01:29 +0000
@@ -62,12 +62,10 @@
             tab when making the previous item live.
 
     ``CanEdit``
-            The capability to allow the ServiceManager to allow the item to be
-             edited
+            The capability to allow the ServiceManager to allow the item to be edited
 
     ``CanMaintain``
-            The capability to allow the ServiceManager to allow the item to be
-             reordered.
+            The capability to allow the ServiceManager to allow the item to be reordered.
 
     ``RequiresMedia``
             Determines is the service_item needs a Media Player

=== modified file 'openlp/core/ui/exceptionform.py'
--- openlp/core/ui/exceptionform.py	2013-03-14 10:46:19 +0000
+++ openlp/core/ui/exceptionform.py	2013-04-14 16:01:29 +0000
@@ -70,6 +70,11 @@
 except ImportError:
     MAKO_VERSION = u'-'
 try:
+    import cherrypy
+    CHERRYPY_VERSION = cherrypy.__version__
+except ImportError:
+    CHERRYPY_VERSION = u'-'
+try:
     import uno
     arg = uno.createUnoStruct(u'com.sun.star.beans.PropertyValue')
     arg.Name = u'nodepath'
@@ -143,6 +148,7 @@
             u'PyEnchant: %s\n' % ENCHANT_VERSION + \
             u'PySQLite: %s\n' % SQLITE_VERSION + \
             u'Mako: %s\n' % MAKO_VERSION + \
+            u'CherryPy: %s\n' % CHERRYPY_VERSION + \
             u'pyUNO bridge: %s\n' % UNO_VERSION + \
             u'VLC: %s\n' % VLC_VERSION
         if platform.system() == u'Linux':

=== modified file 'openlp/core/ui/servicemanager.py'
--- openlp/core/ui/servicemanager.py	2013-04-05 09:04:38 +0000
+++ openlp/core/ui/servicemanager.py	2013-04-14 16:01:29 +0000
@@ -273,7 +273,6 @@
         Registry().register_function(u'config_screen_changed', self.regenerate_service_Items)
         Registry().register_function(u'theme_update_global', self.theme_change)
         Registry().register_function(u'mediaitem_suffix_reset', self.reset_supported_suffixes)
-        Registry().register_function(u'servicemanager_set_item', self.on_set_item)
 
     def drag_enter_event(self, event):
         """
@@ -315,6 +314,8 @@
         self.layout.setSpacing(0)
         self.layout.setMargin(0)
         self.setup_ui(self)
+        # Need to use event as called across threads and UI is updated
+        QtCore.QObject.connect(self, QtCore.SIGNAL(u'servicemanager_set_item'), self.on_set_item)
 
     def set_modified(self, modified=True):
         """
@@ -993,7 +994,7 @@
 
     def on_set_item(self, message):
         """
-        Called by a signal to select a specific item.
+        Called by a signal to select a specific item and make it live usually from remote.
         """
         self.set_item(int(message))
 

=== modified file 'openlp/core/ui/settingsform.py'
--- openlp/core/ui/settingsform.py	2013-03-19 19:43:22 +0000
+++ openlp/core/ui/settingsform.py	2013-04-14 16:01:29 +0000
@@ -96,6 +96,7 @@
         """
         Process the form saving the settings
         """
+        log.debug(u'Processing settings exit')
         for tabIndex in range(self.stacked_layout.count()):
             self.stacked_layout.widget(tabIndex).save()
         # if the display of image background are changing we need to regenerate the image cache

=== modified file 'openlp/core/ui/slidecontroller.py'
--- openlp/core/ui/slidecontroller.py	2013-04-05 13:41:42 +0000
+++ openlp/core/ui/slidecontroller.py	2013-04-14 16:01:29 +0000
@@ -360,8 +360,9 @@
         # Signals
         self.preview_list_widget.clicked.connect(self.onSlideSelected)
         if self.is_live:
+            # Need to use event as called across threads and UI is updated
+            QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_toggle_display'), self.toggle_display)
             Registry().register_function(u'slidecontroller_live_spin_delay', self.receive_spin_delay)
-            Registry().register_function(u'slidecontroller_toggle_display', self.toggle_display)
             self.toolbar.set_widget_visible(self.loop_list, False)
             self.toolbar.set_widget_visible(self.wide_menu, False)
         else:
@@ -373,13 +374,16 @@
         else:
             self.preview_list_widget.addActions([self.nextItem, self.previous_item])
         Registry().register_function(u'slidecontroller_%s_stop_loop' % self.type_prefix, self.on_stop_loop)
-        Registry().register_function(u'slidecontroller_%s_next' % self.type_prefix, self.on_slide_selected_next)
-        Registry().register_function(u'slidecontroller_%s_previous' % self.type_prefix, self.on_slide_selected_previous)
         Registry().register_function(u'slidecontroller_%s_change' % self.type_prefix, self.on_slide_change)
-        Registry().register_function(u'slidecontroller_%s_set' % self.type_prefix, self.on_slide_selected_index)
         Registry().register_function(u'slidecontroller_%s_blank' % self.type_prefix, self.on_slide_blank)
         Registry().register_function(u'slidecontroller_%s_unblank' % self.type_prefix, self.on_slide_unblank)
         Registry().register_function(u'slidecontroller_update_slide_limits', self.update_slide_limits)
+        QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_%s_set' % self.type_prefix),
+            self.on_slide_selected_index)
+        QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_%s_next' % self.type_prefix),
+            self.on_slide_selected_next)
+        QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_%s_previous' % self.type_prefix),
+            self.on_slide_selected_previous)
 
     def _slideShortcutActivated(self):
         """

=== modified file 'openlp/plugins/alerts/lib/alertsmanager.py'
--- openlp/plugins/alerts/lib/alertsmanager.py	2013-03-28 20:08:07 +0000
+++ openlp/plugins/alerts/lib/alertsmanager.py	2013-04-14 16:01:29 +0000
@@ -49,10 +49,12 @@
 
     def __init__(self, parent):
         QtCore.QObject.__init__(self, parent)
+        Registry().register(u'alerts_manager', self)
         self.timer_id = 0
         self.alert_list = []
         Registry().register_function(u'live_display_active', self.generate_alert)
         Registry().register_function(u'alerts_text', self.alert_text)
+        QtCore.QObject.connect(self, QtCore.SIGNAL(u'alerts_text'), self.alert_text)
 
     def alert_text(self, message):
         """

=== modified file 'openlp/plugins/bibles/lib/http.py'
--- openlp/plugins/bibles/lib/http.py	2013-03-07 08:05:43 +0000
+++ openlp/plugins/bibles/lib/http.py	2013-04-14 16:01:29 +0000
@@ -55,6 +55,7 @@
 
 log = logging.getLogger(__name__)
 
+
 class BGExtract(object):
     """
     Extract verses from BibleGateway
@@ -671,6 +672,7 @@
 
     application = property(_get_application)
 
+
 def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None,
     pre_parse_substitute=None, cleaner=None):
     """
@@ -715,6 +717,7 @@
     Registry().get(u'application').process_events()
     return soup
 
+
 def send_error_message(error_type):
     """
     Send a standard error message informing the user of an issue.

=== modified file 'openlp/plugins/media/mediaplugin.py'
--- openlp/plugins/media/mediaplugin.py	2013-03-30 09:41:40 +0000
+++ openlp/plugins/media/mediaplugin.py	2013-04-14 16:01:29 +0000
@@ -54,7 +54,7 @@
         # passed with drag and drop messages
         self.dnd_id = u'Media'
 
-    def create_settings_Tab(self, parent):
+    def create_settings_tab(self, parent):
         """
         Create the settings Tab
         """

=== modified file 'openlp/plugins/presentations/presentationplugin.py'
--- openlp/plugins/presentations/presentationplugin.py	2013-03-19 19:43:22 +0000
+++ openlp/plugins/presentations/presentationplugin.py	2013-04-14 16:01:29 +0000
@@ -69,7 +69,7 @@
         self.icon_path = u':/plugins/plugin_presentations.png'
         self.icon = build_icon(self.icon_path)
 
-    def create_settings_Tab(self, parent):
+    def create_settings_tab(self, parent):
         """
         Create the settings Tab
         """

=== modified file 'openlp/plugins/remotes/html/openlp.js'
--- openlp/plugins/remotes/html/openlp.js	2012-12-29 20:56:56 +0000
+++ openlp/plugins/remotes/html/openlp.js	2013-04-14 16:01:29 +0000
@@ -147,7 +147,7 @@
   },
   pollServer: function () {
     $.getJSON(
-      "/api/poll",
+      "/stage/api/poll",
       function (data, status) {
         var prevItem = OpenLP.currentItem;
         OpenLP.currentSlide = data.results.slide;

=== modified file 'openlp/plugins/remotes/html/stage.js'
--- openlp/plugins/remotes/html/stage.js	2012-12-29 20:56:56 +0000
+++ openlp/plugins/remotes/html/stage.js	2013-04-14 16:01:29 +0000
@@ -26,7 +26,7 @@
 window.OpenLP = {
   loadService: function (event) {
     $.getJSON(
-      "/api/service/list",
+      "/stage/api/service/list",
       function (data, status) {
         OpenLP.nextSong = "";
         $("#notes").html("");
@@ -46,7 +46,7 @@
   },
   loadSlides: function (event) {
     $.getJSON(
-      "/api/controller/live/text",
+      "/stage/api/controller/live/text",
       function (data, status) {
         OpenLP.currentSlides = data.results.slides;
         OpenLP.currentSlide = 0;
@@ -137,7 +137,7 @@
   },
   pollServer: function () {
     $.getJSON(
-      "/api/poll",
+      "/stage/api/poll",
       function (data, status) {
         OpenLP.updateClock(data);
         if (OpenLP.currentItem != data.results.item ||

=== modified file 'openlp/plugins/remotes/lib/httpserver.py'
--- openlp/plugins/remotes/lib/httpserver.py	2013-03-26 07:17:29 +0000
+++ openlp/plugins/remotes/lib/httpserver.py	2013-04-14 16:01:29 +0000
@@ -43,7 +43,7 @@
 ``/files/{filename}``
     Serve a static file.
 
-``/api/poll``
+``/stage/api/poll``
     Poll to see if there are any changes. Returns a JSON-encoded dict of
     any changes that occurred::
 
@@ -119,122 +119,198 @@
 import re
 import urllib
 import urlparse
+import cherrypy
 
-from PyQt4 import QtCore, QtNetwork
 from mako.template import Template
+from PyQt4 import QtCore
 
 from openlp.core.lib import Registry, Settings, PluginStatus, StringContent
-
 from openlp.core.utils import AppLocation, translate
 
+from cherrypy._cpcompat import sha, ntob
+
 log = logging.getLogger(__name__)
 
 
-class HttpResponse(object):
-    """
-    A simple object to encapsulate a pseudo-http response.
-    """
-    code = '200 OK'
-    content = ''
-    headers = {
-        'Content-Type': 'text/html; charset="utf-8"\r\n'
-    }
-
-    def __init__(self, content='', headers=None, code=None):
-        if headers is None:
-            headers = {}
-        self.content = content
-        for key, value in headers.iteritems():
-            self.headers[key] = value
-        if code:
-            self.code = code
+def make_sha_hash(password):
+    """
+    Create an encrypted password for the given password.
+    """
+    return sha(ntob(password)).hexdigest()
+
+
+def fetch_password(username):
+    """
+    Fetch the password for a provided user.
+    """
+    if username != Settings().value(u'remotes/user id'):
+        return None
+    return make_sha_hash(Settings().value(u'remotes/password'))
 
 
 class HttpServer(object):
     """
     Ability to control OpenLP via a web browser.
+    This class controls the Cherrypy server and configuration.
     """
-    def __init__(self, plugin):
+    _cp_config = {
+        'tools.sessions.on': True,
+        'tools.auth.on': True
+    }
+
+    def __init__(self):
         """
-        Initialise the httpserver, and start the server.
+        Initialise the http server, and start the server.
         """
         log.debug(u'Initialise httpserver')
-        self.plugin = plugin
-        self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html')
-        self.connections = []
-        self.start_tcp()
-
-    def start_tcp(self):
-        """
-        Start the http server, use the port in the settings default to 4316.
-        Listen out for slide and song changes so they can be broadcast to
-        clients. Listen out for socket connections.
-        """
-        log.debug(u'Start TCP server')
-        port = Settings().value(self.plugin.settings_section + u'/port')
-        address = Settings().value(self.plugin.settings_section + u'/ip address')
-        self.server = QtNetwork.QTcpServer()
-        self.server.listen(QtNetwork.QHostAddress(address), port)
-        self.server.newConnection.connect(self.new_connection)
-        log.debug(u'TCP listening on port %d' % port)
-
-    def new_connection(self):
-        """
-        A new http connection has been made. Create a client object to handle
-        communication.
-        """
-        log.debug(u'new http connection')
-        socket = self.server.nextPendingConnection()
-        if socket:
-            self.connections.append(HttpConnection(self, socket))
-
-    def close_connection(self, connection):
-        """
-        The connection has been closed. Clean up
-        """
-        log.debug(u'close http connection')
-        if connection in self.connections:
-            self.connections.remove(connection)
+        self.settings_section = u'remotes'
+        self.router = HttpRouter()
+
+    def start_server(self):
+        """
+        Start the http server based on configuration.
+        """
+        log.debug(u'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.router = self.router
+        self.root.files.router = self.router
+        self.root.stage.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.
+        """
+        if Settings().value(self.settings_section + u'/https enabled'):
+            port = Settings().value(self.settings_section + u'/https port')
+            address = Settings().value(self.settings_section + u'/ip address')
+            local_data = AppLocation.get_directory(AppLocation.DataDir)
+            cherrypy.config.update({u'server.socket_host': str(address),
+                                    u'server.socket_port': port,
+                                    u'server.ssl_certificate': os.path.join(local_data, u'remotes', u'openlp.crt'),
+                                    u'server.ssl_private_key': os.path.join(local_data, u'remotes', u'openlp.key')})
+        else:
+            port = Settings().value(self.settings_section + u'/port')
+            address = Settings().value(self.settings_section + u'/ip address')
+            cherrypy.config.update({u'server.socket_host': str(address)})
+            cherrypy.config.update({u'server.socket_port': port})
+        cherrypy.config.update({u'environment': u'embedded'})
+        cherrypy.config.update({u'engine.autoreload_on': False})
+        directory_config = {u'/': {u'tools.staticdir.on': True,
+                                u'tools.staticdir.dir': self.router.html_dir,
+                                u'tools.basic_auth.on': Settings().value(u'remotes/authentication enabled'),
+                                u'tools.basic_auth.realm': u'OpenLP Remote Login',
+                                u'tools.basic_auth.users': fetch_password,
+                                u'tools.basic_auth.encrypt': make_sha_hash},
+                         u'/files': {u'tools.staticdir.on': True,
+                                     u'tools.staticdir.dir': self.router.html_dir,
+                                     u'tools.basic_auth.on': False},
+                         u'/stage': {u'tools.staticdir.on': True,
+                                     u'tools.staticdir.dir': self.router.html_dir,
+                                     u'tools.basic_auth.on': False}}
+        return directory_config
+
+    class Public:
+        """
+        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(u'data', None)
+            url = urlparse.urlparse(cherrypy.url())
+            return self.router.process_http_request(url.path, *args)
+
+    class Files:
+        """
+        Provides access to files and has no security available.  These are read only accesses
+        """
+        @cherrypy.expose
+        def default(self, *args, **kwargs):
+            url = urlparse.urlparse(cherrypy.url())
+            return self.router.process_http_request(url.path, *args)
+
+    class Stage:
+        """
+        Stageview is read only so security is not relevant and would reduce it's usability
+        """
+        @cherrypy.expose
+        def default(self, *args, **kwargs):
+            url = urlparse.urlparse(cherrypy.url())
+            return self.router.process_http_request(url.path, *args)
 
     def close(self):
         """
         Close down the http server.
         """
         log.debug(u'close http server')
-        self.server.close()
-
-
-class HttpConnection(object):
-    """
-    A single connection, this handles communication between the server
-    and the client.
-    """
-    def __init__(self, parent, socket):
-        """
-        Initialise the http connection. Listen out for socket signals.
-        """
-        log.debug(u'Initialise HttpConnection: %s' % socket.peerAddress())
-        self.socket = socket
-        self.parent = parent
+        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 = [
             (u'^/$', self.serve_file),
             (u'^/(stage)$', self.serve_file),
             (r'^/files/(.*)$', self.serve_file),
             (r'^/api/poll$', self.poll),
+            (r'^/stage/api/poll$', self.poll),
             (r'^/api/controller/(live|preview)/(.*)$', self.controller),
+            (r'^/stage/api/controller/(live|preview)/(.*)$', self.controller),
             (r'^/api/service/(.*)$', self.service),
+            (r'^/stage/api/service/(.*)$', self.service),
             (r'^/api/display/(hide|show|blank|theme|desktop)$', self.display),
             (r'^/api/alert$', self.alert),
-            (r'^/api/plugin/(search)$', self.pluginInfo),
+            (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.socket.readyRead.connect(self.ready_read)
-        self.socket.disconnected.connect(self.disconnected)
         self.translate()
+        self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'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:
+            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
@@ -281,40 +357,6 @@
             'slides': translate('RemotePlugin.Mobile', 'Slides')
         }
 
-    def ready_read(self):
-        """
-        Data has been sent from the client. Respond to it
-        """
-        log.debug(u'ready to read socket')
-        if self.socket.canReadLine():
-            data = str(self.socket.readLine())
-            try:
-                log.debug(u'received: ' + data)
-            except UnicodeDecodeError:
-                # Malicious request containing non-ASCII characters.
-                self.close()
-                return
-            words = data.split(' ')
-            response = None
-            if words[0] == u'GET':
-                url = urlparse.urlparse(words[1])
-                self.url_params = urlparse.parse_qs(url.query)
-                # Loop through the routes we set up earlier and execute them
-                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:
-                self.send_response(response)
-            else:
-                self.send_response(HttpResponse(code='404 Not Found'))
-            self.close()
-
     def serve_file(self, filename=None):
         """
         Send a file to the socket. For now, just a subset of file types
@@ -329,9 +371,9 @@
             filename = u'index.html'
         elif filename == u'stage':
             filename = u'stage.html'
-        path = os.path.normpath(os.path.join(self.parent.html_dir, filename))
-        if not path.startswith(self.parent.html_dir):
-            return HttpResponse(code=u'404 Not Found')
+        path = os.path.normpath(os.path.join(self.html_dir, filename))
+        if not path.startswith(self.html_dir):
+            return self._http_not_found()
         ext = os.path.splitext(filename)[1]
         html = None
         if ext == u'.html':
@@ -360,11 +402,12 @@
                 content = file_handle.read()
         except IOError:
             log.exception(u'Failed to open %s' % path)
-            return HttpResponse(code=u'404 Not Found')
+            return self._http_not_found()
         finally:
             if file_handle:
                 file_handle.close()
-        return HttpResponse(content, {u'Content-Type': mimetype})
+        cherrypy.response.headers['Content-Type'] = mimetype
+        return content
 
     def poll(self):
         """
@@ -379,18 +422,20 @@
             u'theme': self.live_controller.theme_screen.isChecked(),
             u'display': self.live_controller.desktop_screen.isChecked()
         }
-        return HttpResponse(json.dumps({u'results': result}), {u'Content-Type': u'application/json'})
+        cherrypy.response.headers['Content-Type'] = u'application/json'
+        return json.dumps({u'results': result})
 
     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``.
         """
-        Registry().execute(u'slidecontroller_toggle_display', action)
-        return HttpResponse(json.dumps({u'results': {u'success': True}}),
-            {u'Content-Type': u'application/json'})
+        self.live_controller.emit(QtCore.SIGNAL(u'slidecontroller_toggle_display'), action)
+        cherrypy.response.headers['Content-Type'] = u'application/json'
+        return json.dumps({u'results': {u'success': True}})
 
     def alert(self):
         """
@@ -399,16 +444,16 @@
         plugin = self.plugin_manager.get_plugin_by_name("alerts")
         if plugin.status == PluginStatus.Active:
             try:
-                text = json.loads(self.url_params[u'data'][0])[u'request'][u'text']
+                text = json.loads(self.request_data)[u'request'][u'text']
             except KeyError, ValueError:
-                return HttpResponse(code=u'400 Bad Request')
+                return self._http_bad_request()
             text = urllib.unquote(text)
-            Registry().execute(u'alerts_text', [text])
+            self.alerts_manager.emit(QtCore.SIGNAL(u'alerts_text'), [text])
             success = True
         else:
             success = False
-        return HttpResponse(json.dumps({u'results': {u'success': success}}),
-            {u'Content-Type': u'application/json'})
+        cherrypy.response.headers['Content-Type'] = u'application/json'
+        return json.dumps({u'results': {u'success': success}})
 
     def controller(self, display_type, action):
         """
@@ -444,44 +489,44 @@
             if current_item:
                 json_data[u'results'][u'item'] = self.live_controller.service_item.unique_identifier
         else:
-            if self.url_params and self.url_params.get(u'data'):
+            if self.request_data:
                 try:
-                    data = json.loads(self.url_params[u'data'][0])
+                    data = json.loads(self.request_data)[u'request'][u'id']
                 except KeyError, ValueError:
-                    return HttpResponse(code=u'400 Bad Request')
+                    return self._http_bad_request()
                 log.info(data)
                 # This slot expects an int within a list.
-                id = data[u'request'][u'id']
-                Registry().execute(event, [id])
+                self.live_controller.emit(QtCore.SIGNAL(event), [data])
             else:
-                Registry().execute(event)
+                self.live_controller.emit(QtCore.SIGNAL(event))
             json_data = {u'results': {u'success': True}}
-        return HttpResponse(json.dumps(json_data), {u'Content-Type': u'application/json'})
+        cherrypy.response.headers['Content-Type'] = u'application/json'
+        return json.dumps(json_data)
 
     def service(self, action):
         """
-        Handles requests for service items
+        Handles requests for service items in the service manager
 
         ``action``
             The action to perform.
         """
         event = u'servicemanager_%s' % action
         if action == u'list':
-            return HttpResponse(json.dumps({u'results': {u'items': self._get_service_items()}}),
-                {u'Content-Type': u'application/json'})
-        else:
-            event += u'_item'
-        if self.url_params and self.url_params.get(u'data'):
+            cherrypy.response.headers['Content-Type'] = u'application/json'
+            return json.dumps({u'results': {u'items': self._get_service_items()}})
+        event += u'_item'
+        if self.request_data:
             try:
-                data = json.loads(self.url_params[u'data'][0])
-            except KeyError, ValueError:
-                return HttpResponse(code=u'400 Bad Request')
-            Registry().execute(event, data[u'request'][u'id'])
+                data = json.loads(self.request_data)[u'request'][u'id']
+            except KeyError:
+                return self._http_bad_request()
+            self.service_manager.emit(QtCore.SIGNAL(event), data)
         else:
             Registry().execute(event)
-        return HttpResponse(json.dumps({u'results': {u'success': True}}), {u'Content-Type': u'application/json'})
+        cherrypy.response.headers['Content-Type'] = u'application/json'
+        return json.dumps({u'results': {u'success': True}})
 
-    def pluginInfo(self, action):
+    def plugin_info(self, action):
         """
         Return plugin related information, based on the action.
 
@@ -493,8 +538,9 @@
             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, unicode(plugin.textStrings[StringContent.Name][u'plural'])])
-            return HttpResponse(json.dumps({u'results': {u'items': searches}}), {u'Content-Type': u'application/json'})
+                    searches.append([plugin.name, unicode(plugin.text_strings[StringContent.Name][u'plural'])])
+            cherrypy.response.headers['Content-Type'] = u'application/json'
+            return json.dumps({u'results': {u'items': searches}})
 
     def search(self, plugin_name):
         """
@@ -504,69 +550,63 @@
             The plugin name to search in.
         """
         try:
-            text = json.loads(self.url_params[u'data'][0])[u'request'][u'text']
+            text = json.loads(self.request_data)[u'request'][u'text']
         except KeyError, ValueError:
-            return HttpResponse(code=u'400 Bad Request')
+            return self._http_bad_request()
         text = urllib.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 HttpResponse(json.dumps({u'results': {u'items': results}}), {u'Content-Type': u'application/json'})
+        cherrypy.response.headers['Content-Type'] = u'application/json'
+        return json.dumps({u'results': {u'items': results}})
 
     def go_live(self, plugin_name):
         """
         Go live on an item of type ``plugin``.
         """
         try:
-            id = json.loads(self.url_params[u'data'][0])[u'request'][u'id']
+            id = json.loads(self.request_data)[u'request'][u'id']
         except KeyError, ValueError:
-            return HttpResponse(code=u'400 Bad Request')
+            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.go_live(id, remote=True)
-        return HttpResponse(code=u'200 OK')
+            plugin.media_item.emit(QtCore.SIGNAL(u'%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.url_params[u'data'][0])[u'request'][u'id']
+            id = json.loads(self.request_data)[u'request'][u'id']
         except KeyError, ValueError:
-            return HttpResponse(code=u'400 Bad Request')
+            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.createItemFromId(id)
-            plugin.media_item.add_to_service(item_id, remote=True)
-        return HttpResponse(code=u'200 OK')
-
-    def send_response(self, response):
-        http = u'HTTP/1.1 %s\r\n' % response.code
-        for header, value in response.headers.iteritems():
-            http += '%s: %s\r\n' % (header, value)
-        http += '\r\n'
-        self.socket.write(http)
-        self.socket.write(response.content)
-
-    def disconnected(self):
-        """
-        The client has disconnected. Tidy up
-        """
-        log.debug(u'socket disconnected')
-        self.close()
-
-    def close(self):
-        """
-        The server has closed the connection. Tidy up
-        """
-        if not self.socket:
-            return
-        log.debug(u'close socket')
-        self.socket.close()
-        self.socket = None
-        self.parent.close_connection(self)
+            item_id = plugin.media_item.create_item_from_id(id)
+            plugin.media_item.emit(QtCore.SIGNAL(u'%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 = ["<html><body>Sorry, an error occurred </body></html>"]
 
     def _get_service_manager(self):
         """
@@ -597,3 +637,13 @@
         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, u'_alerts_manager'):
+            self._alerts_manager = Registry().get(u'alerts_manager')
+        return self._alerts_manager
+
+    alerts_manager = property(_get_alerts_manager)

=== modified file 'openlp/plugins/remotes/lib/remotetab.py'
--- openlp/plugins/remotes/lib/remotetab.py	2013-03-25 06:40:47 +0000
+++ openlp/plugins/remotes/lib/remotetab.py	2013-04-14 16:01:29 +0000
@@ -27,9 +27,12 @@
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
 
+import os.path
+
 from PyQt4 import QtCore, QtGui, QtNetwork
 
-from openlp.core.lib import Registry, Settings, SettingsTab, translate
+from openlp.core.lib import Settings, SettingsTab, translate
+from openlp.core.utils import AppLocation
 
 
 ZERO_URL = u'0.0.0.0'
@@ -53,32 +56,84 @@
         self.address_label.setObjectName(u'address_label')
         self.address_edit = QtGui.QLineEdit(self.server_settings_group_box)
         self.address_edit.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed)
-        self.address_edit.setValidator(QtGui.QRegExpValidator(QtCore.QRegExp(
-            u'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'), self))
+        self.address_edit.setValidator(QtGui.QRegExpValidator(QtCore.QRegExp(u'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'),
+            self))
         self.address_edit.setObjectName(u'address_edit')
         self.server_settings_layout.addRow(self.address_label, self.address_edit)
         self.twelve_hour_check_box = QtGui.QCheckBox(self.server_settings_group_box)
         self.twelve_hour_check_box.setObjectName(u'twelve_hour_check_box')
         self.server_settings_layout.addRow(self.twelve_hour_check_box)
-        self.port_label = QtGui.QLabel(self.server_settings_group_box)
+        self.left_layout.addWidget(self.server_settings_group_box)
+        self.http_settings_group_box = QtGui.QGroupBox(self.left_column)
+        self.http_settings_group_box.setObjectName(u'http_settings_group_box')
+        self.http_setting_layout = QtGui.QFormLayout(self.http_settings_group_box)
+        self.http_setting_layout.setObjectName(u'http_setting_layout')
+        self.port_label = QtGui.QLabel(self.http_settings_group_box)
         self.port_label.setObjectName(u'port_label')
-        self.port_spin_box = QtGui.QSpinBox(self.server_settings_group_box)
+        self.port_spin_box = QtGui.QSpinBox(self.http_settings_group_box)
         self.port_spin_box.setMaximum(32767)
         self.port_spin_box.setObjectName(u'port_spin_box')
-        self.server_settings_layout.addRow(self.port_label, self.port_spin_box)
-        self.remote_url_label = QtGui.QLabel(self.server_settings_group_box)
+        self.http_setting_layout.addRow(self.port_label, self.port_spin_box)
+        self.remote_url_label = QtGui.QLabel(self.http_settings_group_box)
         self.remote_url_label.setObjectName(u'remote_url_label')
-        self.remote_url = QtGui.QLabel(self.server_settings_group_box)
+        self.remote_url = QtGui.QLabel(self.http_settings_group_box)
         self.remote_url.setObjectName(u'remote_url')
         self.remote_url.setOpenExternalLinks(True)
-        self.server_settings_layout.addRow(self.remote_url_label, self.remote_url)
-        self.stage_url_label = QtGui.QLabel(self.server_settings_group_box)
+        self.http_setting_layout.addRow(self.remote_url_label, self.remote_url)
+        self.stage_url_label = QtGui.QLabel(self.http_settings_group_box)
         self.stage_url_label.setObjectName(u'stage_url_label')
-        self.stage_url = QtGui.QLabel(self.server_settings_group_box)
+        self.stage_url = QtGui.QLabel(self.http_settings_group_box)
         self.stage_url.setObjectName(u'stage_url')
         self.stage_url.setOpenExternalLinks(True)
-        self.server_settings_layout.addRow(self.stage_url_label, self.stage_url)
-        self.left_layout.addWidget(self.server_settings_group_box)
+        self.http_setting_layout.addRow(self.stage_url_label, self.stage_url)
+        self.left_layout.addWidget(self.http_settings_group_box)
+        self.https_settings_group_box = QtGui.QGroupBox(self.left_column)
+        self.https_settings_group_box.setCheckable(True)
+        self.https_settings_group_box.setChecked(False)
+        self.https_settings_group_box.setObjectName(u'https_settings_group_box')
+        self.https_settings_layout = QtGui.QFormLayout(self.https_settings_group_box)
+        self.https_settings_layout.setObjectName(u'https_settings_layout')
+        self.https_error_label = QtGui.QLabel(self.https_settings_group_box)
+        self.https_error_label.setVisible(False)
+        self.https_error_label.setWordWrap(True)
+        self.https_error_label.setObjectName(u'https_error_label')
+        self.https_settings_layout.addRow(self.https_error_label)
+        self.https_port_label = QtGui.QLabel(self.https_settings_group_box)
+        self.https_port_label.setObjectName(u'https_port_label')
+        self.https_port_spin_box = QtGui.QSpinBox(self.https_settings_group_box)
+        self.https_port_spin_box.setMaximum(32767)
+        self.https_port_spin_box.setObjectName(u'https_port_spin_box')
+        self.https_settings_layout.addRow(self.https_port_label, self.https_port_spin_box)
+        self.remote_https_url = QtGui.QLabel(self.https_settings_group_box)
+        self.remote_https_url.setObjectName(u'remote_http_url')
+        self.remote_https_url.setOpenExternalLinks(True)
+        self.remote_https_url_label = QtGui.QLabel(self.https_settings_group_box)
+        self.remote_https_url_label.setObjectName(u'remote_http_url_label')
+        self.https_settings_layout.addRow(self.remote_https_url_label, self.remote_https_url)
+        self.stage_https_url_label = QtGui.QLabel(self.http_settings_group_box)
+        self.stage_https_url_label.setObjectName(u'stage_https_url_label')
+        self.stage_https_url = QtGui.QLabel(self.https_settings_group_box)
+        self.stage_https_url.setObjectName(u'stage_https_url')
+        self.stage_https_url.setOpenExternalLinks(True)
+        self.https_settings_layout.addRow(self.stage_https_url_label, self.stage_https_url)
+        self.left_layout.addWidget(self.https_settings_group_box)
+        self.user_login_group_box = QtGui.QGroupBox(self.left_column)
+        self.user_login_group_box.setCheckable(True)
+        self.user_login_group_box.setChecked(False)
+        self.user_login_group_box.setObjectName(u'user_login_group_box')
+        self.user_login_layout = QtGui.QFormLayout(self.user_login_group_box)
+        self.user_login_layout.setObjectName(u'user_login_layout')
+        self.user_id_label = QtGui.QLabel(self.user_login_group_box)
+        self.user_id_label.setObjectName(u'user_id_label')
+        self.user_id = QtGui.QLineEdit(self.user_login_group_box)
+        self.user_id.setObjectName(u'user_id')
+        self.user_login_layout.addRow(self.user_id_label, self.user_id)
+        self.password_label = QtGui.QLabel(self.user_login_group_box)
+        self.password_label.setObjectName(u'password_label')
+        self.password = QtGui.QLineEdit(self.user_login_group_box)
+        self.password.setObjectName(u'password')
+        self.user_login_layout.addRow(self.password_label, self.password)
+        self.left_layout.addWidget(self.user_login_group_box)
         self.android_app_group_box = QtGui.QGroupBox(self.right_column)
         self.android_app_group_box.setObjectName(u'android_app_group_box')
         self.right_layout.addWidget(self.android_app_group_box)
@@ -96,9 +151,11 @@
         self.qr_layout.addWidget(self.qr_description_label)
         self.left_layout.addStretch()
         self.right_layout.addStretch()
-        self.twelve_hour_check_box.stateChanged.connect(self.onTwelveHourCheckBoxChanged)
+        self.twelve_hour_check_box.stateChanged.connect(self.on_twelve_hour_check_box_changed)
         self.address_edit.textChanged.connect(self.set_urls)
         self.port_spin_box.valueChanged.connect(self.set_urls)
+        self.https_port_spin_box.valueChanged.connect(self.set_urls)
+        self.https_settings_group_box.clicked.connect(self.https_changed)
 
     def retranslateUi(self):
         self.server_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Server Settings'))
@@ -112,8 +169,21 @@
             'Scan the QR code or click <a href="https://play.google.com/store/'
             'apps/details?id=org.openlp.android">download</a> to install the '
             'Android app from Google Play.'))
+        self.https_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'HTTPS Server'))
+        self.https_error_label.setText(translate('RemotePlugin.RemoteTab',
+            'Could not find an SSL certificate. The HTTPS server will not be available unless an SSL certificate '
+            'is found. Please see the manual for more information.'))
+        self.https_port_label.setText(self.port_label.text())
+        self.remote_https_url_label.setText(self.remote_url_label.text())
+        self.stage_https_url_label.setText(self.stage_url_label.text())
+        self.user_login_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'User Authentication'))
+        self.user_id_label.setText(translate('RemotePlugin.RemoteTab', 'User id:'))
+        self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:'))
 
     def set_urls(self):
+        """
+        Update the display based on the data input on the screen
+        """
         ip_address = u'localhost'
         if self.address_edit.text() == ZERO_URL:
             interfaces = QtNetwork.QNetworkInterface.allInterfaces()
@@ -129,31 +199,73 @@
                         break
         else:
             ip_address = self.address_edit.text()
-        url = u'http://%s:%s/' % (ip_address, self.port_spin_box.value())
-        self.remote_url.setText(u'<a href="%s">%s</a>' % (url, url))
-        url += u'stage'
-        self.stage_url.setText(u'<a href="%s">%s</a>' % (url, url))
+        http_url = u'http://%s:%s/' % (ip_address, self.port_spin_box.value())
+        https_url = u'https://%s:%s/' % (ip_address, self.https_port_spin_box.value())
+        self.remote_url.setText(u'<a href="%s">%s</a>' % (http_url, http_url))
+        self.remote_https_url.setText(u'<a href="%s">%s</a>' % (https_url, https_url))
+        http_url += u'stage'
+        https_url += u'stage'
+        self.stage_url.setText(u'<a href="%s">%s</a>' % (http_url, http_url))
+        self.stage_https_url.setText(u'<a href="%s">%s</a>' % (https_url, https_url))
 
     def load(self):
+        """
+        Load the configuration and update the server configuration if necessary
+        """
         self.port_spin_box.setValue(Settings().value(self.settings_section + u'/port'))
+        self.https_port_spin_box.setValue(Settings().value(self.settings_section + u'/https port'))
         self.address_edit.setText(Settings().value(self.settings_section + u'/ip address'))
         self.twelve_hour = Settings().value(self.settings_section + u'/twelve hour')
         self.twelve_hour_check_box.setChecked(self.twelve_hour)
+        local_data = AppLocation.get_directory(AppLocation.DataDir)
+        if not os.path.exists(os.path.join(local_data, u'remotes', u'openlp.crt')) or \
+                not os.path.exists(os.path.join(local_data, u'remotes', u'openlp.key')):
+            self.https_settings_group_box.setChecked(False)
+            self.https_settings_group_box.setEnabled(False)
+            self.https_error_label.setVisible(True)
+        else:
+            self.https_settings_group_box.setChecked(Settings().value(self.settings_section + u'/https enabled'))
+            self.https_settings_group_box.setEnabled(True)
+            self.https_error_label.setVisible(False)
+        self.user_login_group_box.setChecked(Settings().value(self.settings_section + u'/authentication enabled'))
+        self.user_id.setText(Settings().value(self.settings_section + u'/user id'))
+        self.password.setText(Settings().value(self.settings_section + u'/password'))
         self.set_urls()
+        self.https_changed()
 
     def save(self):
-        changed = False
+        """
+        Save the configuration and update the server configuration if necessary
+        """
         if Settings().value(self.settings_section + u'/ip address') != self.address_edit.text() or \
-                Settings().value(self.settings_section + u'/port') != self.port_spin_box.value():
-            changed = True
+                Settings().value(self.settings_section + u'/port') != self.port_spin_box.value() or \
+                Settings().value(self.settings_section + u'/https port') != self.https_port_spin_box.value() or \
+                Settings().value(self.settings_section + u'/https enabled') != \
+                        self.https_settings_group_box.isChecked() or \
+                Settings().value(self.settings_section + u'/authentication enabled') != \
+                        self.user_login_group_box.isChecked():
+            self.settings_form.register_post_process(u'remotes_config_updated')
         Settings().setValue(self.settings_section + u'/port', self.port_spin_box.value())
+        Settings().setValue(self.settings_section + u'/https port', self.https_port_spin_box.value())
+        Settings().setValue(self.settings_section + u'/https enabled', self.https_settings_group_box.isChecked())
         Settings().setValue(self.settings_section + u'/ip address', self.address_edit.text())
         Settings().setValue(self.settings_section + u'/twelve hour', self.twelve_hour)
-        if changed:
-            Registry().execute(u'remotes_config_updated')
+        Settings().setValue(self.settings_section + u'/authentication enabled', self.user_login_group_box.isChecked())
+        Settings().setValue(self.settings_section + u'/user id', self.user_id.text())
+        Settings().setValue(self.settings_section + u'/password', self.password.text())
 
-    def onTwelveHourCheckBoxChanged(self, check_state):
+    def on_twelve_hour_check_box_changed(self, check_state):
+        """
+        Toggle the 12 hour check box.
+        """
         self.twelve_hour = False
         # we have a set value convert to True/False
         if check_state == QtCore.Qt.Checked:
             self.twelve_hour = True
+
+    def https_changed(self):
+        """
+        Invert the HTTP group box based on Https group settings
+        """
+        self.http_settings_group_box.setEnabled(not self.https_settings_group_box.isChecked())
+

=== modified file 'openlp/plugins/remotes/remoteplugin.py'
--- openlp/plugins/remotes/remoteplugin.py	2013-03-19 19:43:22 +0000
+++ openlp/plugins/remotes/remoteplugin.py	2013-04-14 16:01:29 +0000
@@ -29,6 +29,8 @@
 
 import logging
 
+from PyQt4 import QtGui
+
 from openlp.core.lib import Plugin, StringContent, translate, build_icon
 from openlp.plugins.remotes.lib import RemoteTab, HttpServer
 
@@ -37,6 +39,11 @@
 __default_settings__ = {
         u'remotes/twelve hour': True,
         u'remotes/port': 4316,
+        u'remotes/https port': 4317,
+        u'remotes/https enabled': False,
+        u'remotes/user id': u'openlp',
+        u'remotes/password': u'password',
+        u'remotes/authentication enabled': False,
         u'remotes/ip address': u'0.0.0.0'
 }
 
@@ -60,7 +67,8 @@
         """
         log.debug(u'initialise')
         Plugin.initialise(self)
-        self.server = HttpServer(self)
+        self.server = HttpServer()
+        self.server.start_server()
 
     def finalise(self):
         """
@@ -70,6 +78,7 @@
         Plugin.finalise(self)
         if self.server:
             self.server.close()
+            self.server = None
 
     def about(self):
         """
@@ -99,5 +108,6 @@
         """
         Called when Config is changed to restart the server on new address or port
         """
-        self.finalise()
-        self.initialise()
+        log.debug(u'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.'))

=== modified file 'scripts/check_dependencies.py'
--- scripts/check_dependencies.py	2013-03-14 10:51:49 +0000
+++ scripts/check_dependencies.py	2013-04-14 16:01:29 +0000
@@ -81,6 +81,7 @@
     'enchant',
     'BeautifulSoup',
     'mako',
+    'cherrypy',
     'migrate',
     'uno',
 ]

=== modified file 'tests/functional/openlp_core_lib/test_pluginmanager.py'
--- tests/functional/openlp_core_lib/test_pluginmanager.py	2013-03-19 19:43:22 +0000
+++ tests/functional/openlp_core_lib/test_pluginmanager.py	2013-04-14 16:01:29 +0000
@@ -74,7 +74,7 @@
         # WHEN: We run hook_settings_tabs()
         plugin_manager.hook_settings_tabs()
 
-        # THEN: The create_settings_Tab() method should have been called
+        # THEN: The hook_settings_tabs() method should have been called
         assert mocked_plugin.create_media_manager_item.call_count == 0, \
             u'The create_media_manager_item() method should not have been called.'
 
@@ -94,8 +94,8 @@
         # WHEN: We run hook_settings_tabs()
         plugin_manager.hook_settings_tabs()
 
-        # THEN: The create_settings_Tab() method should not have been called, but the plugins lists should be the same
-        assert mocked_plugin.create_settings_Tab.call_count == 0, \
+        # THEN: The create_settings_tab() method should not have been called, but the plugins lists should be the same
+        assert mocked_plugin.create_settings_tab.call_count == 0, \
             u'The create_media_manager_item() method should not have been called.'
         self.assertEqual(mocked_settings_form.plugin_manager.plugins, plugin_manager.plugins,
             u'The plugins on the settings form should be the same as the plugins in the plugin manager')
@@ -117,7 +117,7 @@
         plugin_manager.hook_settings_tabs()
 
         # THEN: The create_media_manager_item() method should have been called with the mocked settings form
-        assert mocked_plugin.create_settings_Tab.call_count == 1, \
+        assert mocked_plugin.create_settings_tab.call_count == 1, \
             u'The create_media_manager_item() method should have been called once.'
         self.assertEqual(mocked_settings_form.plugin_manager.plugins, plugin_manager.plugins,
              u'The plugins on the settings form should be the same as the plugins in the plugin manager')
@@ -135,8 +135,8 @@
         # WHEN: We run hook_settings_tabs()
         plugin_manager.hook_settings_tabs()
 
-        # THEN: The create_settings_Tab() method should have been called
-        mocked_plugin.create_settings_Tab.assert_called_with(self.mocked_settings_form)
+        # THEN: The create_settings_tab() method should have been called
+        mocked_plugin.create_settings_tab.assert_called_with(self.mocked_settings_form)
 
     def hook_import_menu_with_disabled_plugin_test(self):
         """

=== modified file 'tests/functional/openlp_core_lib/test_settings.py'
--- tests/functional/openlp_core_lib/test_settings.py	2013-02-21 07:33:21 +0000
+++ tests/functional/openlp_core_lib/test_settings.py	2013-04-14 16:01:29 +0000
@@ -11,7 +11,9 @@
 
 
 class TestSettings(TestCase):
-
+    """
+    Test the functions in the Settings module
+    """
     def setUp(self):
         """
         Create the UI

=== modified file 'tests/functional/openlp_core_lib/test_uistrings.py'
--- tests/functional/openlp_core_lib/test_uistrings.py	2013-02-16 18:11:22 +0000
+++ tests/functional/openlp_core_lib/test_uistrings.py	2013-04-14 16:01:29 +0000
@@ -6,6 +6,7 @@
 
 from openlp.core.lib import UiStrings
 
+
 class TestUiStrings(TestCase):
 
     def check_same_instance_test(self):

=== modified file 'tests/functional/openlp_core_utils/test_applocation.py'
--- tests/functional/openlp_core_utils/test_applocation.py	2013-03-01 16:30:13 +0000
+++ tests/functional/openlp_core_utils/test_applocation.py	2013-04-14 16:01:29 +0000
@@ -30,8 +30,10 @@
             mocked_get_directory.return_value = u'test/dir'
             mocked_check_directory_exists.return_value = True
             mocked_os.path.normpath.return_value = u'test/dir'
+
             # WHEN: we call AppLocation.get_data_path()
             data_path = AppLocation.get_data_path()
+
             # THEN: check that all the correct methods were called, and the result is correct
             mocked_settings.contains.assert_called_with(u'advanced/data path')
             mocked_get_directory.assert_called_with(AppLocation.DataDir)
@@ -49,8 +51,10 @@
             mocked_settings.contains.return_value = True
             mocked_settings.value.return_value.toString.return_value = u'custom/dir'
             mocked_os.path.normpath.return_value = u'custom/dir'
+
             # WHEN: we call AppLocation.get_data_path()
             data_path = AppLocation.get_data_path()
+
             # THEN: the mocked Settings methods were called and the value returned was our set up value
             mocked_settings.contains.assert_called_with(u'advanced/data path')
             mocked_settings.value.assert_called_with(u'advanced/data path')
@@ -100,8 +104,10 @@
             # GIVEN: A mocked out AppLocation.get_data_path()
             mocked_get_data_path.return_value = u'test/dir'
             mocked_check_directory_exists.return_value = True
+
             # WHEN: we call AppLocation.get_data_path()
             data_path = AppLocation.get_section_data_path(u'section')
+
             # THEN: check that all the correct methods were called, and the result is correct
             mocked_check_directory_exists.assert_called_with(u'test/dir/section')
             assert data_path == u'test/dir/section', u'Result should be "test/dir/section"'
@@ -112,8 +118,10 @@
         """
         with patch(u'openlp.core.utils.applocation._get_frozen_path') as mocked_get_frozen_path:
             mocked_get_frozen_path.return_value = u'app/dir'
+
             # WHEN: We call AppLocation.get_directory
             directory = AppLocation.get_directory(AppLocation.AppDir)
+
             # THEN:
             assert directory == u'app/dir', u'Directory should be "app/dir"'
 
@@ -130,8 +138,10 @@
             mocked_get_frozen_path.return_value = u'plugins/dir'
             mocked_sys.frozen = 1
             mocked_sys.argv = ['openlp']
+
             # WHEN: We call AppLocation.get_directory
             directory = AppLocation.get_directory(AppLocation.PluginsDir)
+
             # THEN:
             assert directory == u'plugins/dir', u'Directory should be "plugins/dir"'
 

=== added directory 'tests/functional/openlp_plugins/remotes'
=== added file 'tests/functional/openlp_plugins/remotes/__init__.py'
=== added file 'tests/functional/openlp_plugins/remotes/test_remotetab.py'
--- tests/functional/openlp_plugins/remotes/test_remotetab.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/remotes/test_remotetab.py	2013-04-14 16:01:29 +0000
@@ -0,0 +1,108 @@
+"""
+This module contains tests for the lib submodule of the Remotes plugin.
+"""
+import os
+
+from unittest import TestCase
+from tempfile import mkstemp
+from mock import patch
+
+from openlp.core.lib import Settings
+from openlp.plugins.remotes.lib.remotetab import RemoteTab
+
+from PyQt4 import QtGui
+
+__default_settings__ = {
+    u'remotes/twelve hour': True,
+    u'remotes/port': 4316,
+    u'remotes/https port': 4317,
+    u'remotes/https enabled': False,
+    u'remotes/user id': u'openlp',
+    u'remotes/password': u'password',
+    u'remotes/authentication enabled': False,
+    u'remotes/ip address': u'0.0.0.0'
+}
+
+ZERO_URL = u'0.0.0.0'
+
+TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..', u'..', u'resources'))
+
+
+class TestRemoteTab(TestCase):
+    """
+    Test the functions in the :mod:`lib` module.
+    """
+    def setUp(self):
+        """
+        Create the UI
+        """
+        fd, self.ini_file = mkstemp(u'.ini')
+        Settings().set_filename(self.ini_file)
+        self.application = QtGui.QApplication.instance()
+        Settings().extend_default_settings(__default_settings__)
+        self.parent = QtGui.QMainWindow()
+        self.form = RemoteTab(self.parent, u'Remotes', None, None)
+
+    def tearDown(self):
+        """
+        Delete all the C++ objects at the end so that we don't have a segfault
+        """
+        del self.application
+        del self.parent
+        del self.form
+        os.unlink(self.ini_file)
+
+    def set_basic_urls_test(self):
+        """
+        Test the set_urls function with standard defaults
+        """
+        # GIVEN: A mocked location
+        with patch(u'openlp.core.utils.applocation.Settings') as mocked_class, \
+            patch(u'openlp.core.utils.AppLocation.get_directory') as mocked_get_directory, \
+            patch(u'openlp.core.utils.applocation.check_directory_exists') as mocked_check_directory_exists, \
+            patch(u'openlp.core.utils.applocation.os') as mocked_os:
+            # GIVEN: A mocked out Settings class and a mocked out AppLocation.get_directory()
+            mocked_settings = mocked_class.return_value
+            mocked_settings.contains.return_value = False
+            mocked_get_directory.return_value = u'test/dir'
+            mocked_check_directory_exists.return_value = True
+            mocked_os.path.normpath.return_value = u'test/dir'
+
+            # WHEN: when the set_urls is called having reloaded the form.
+            self.form.load()
+            self.form.set_urls()
+            # THEN: the following screen values should be set
+            self.assertEqual(self.form.address_edit.text(), ZERO_URL, u'The default URL should be set on the screen')
+            self.assertEqual(self.form.https_settings_group_box.isEnabled(), False,
+                            u'The Https box should not be enabled')
+            self.assertEqual(self.form.https_settings_group_box.isChecked(), False,
+                             u'The Https checked box should note be Checked')
+            self.assertEqual(self.form.user_login_group_box.isChecked(), False,
+                             u'The authentication box should not be enabled')
+
+    def set_certificate_urls_test(self):
+        """
+        Test the set_urls function with certificate available
+        """
+        # GIVEN: A mocked location
+        with patch(u'openlp.core.utils.applocation.Settings') as mocked_class, \
+            patch(u'openlp.core.utils.AppLocation.get_directory') as mocked_get_directory, \
+            patch(u'openlp.core.utils.applocation.check_directory_exists') as mocked_check_directory_exists, \
+            patch(u'openlp.core.utils.applocation.os') as mocked_os:
+            # GIVEN: A mocked out Settings class and a mocked out AppLocation.get_directory()
+            mocked_settings = mocked_class.return_value
+            mocked_settings.contains.return_value = False
+            mocked_get_directory.return_value = TEST_PATH
+            mocked_check_directory_exists.return_value = True
+            mocked_os.path.normpath.return_value = TEST_PATH
+
+            # WHEN: when the set_urls is called having reloaded the form.
+            self.form.load()
+            self.form.set_urls()
+            # THEN: the following screen values should be set
+            self.assertEqual(self.form.http_settings_group_box.isEnabled(), True,
+                             u'The Http group box should be enabled')
+            self.assertEqual(self.form.https_settings_group_box.isChecked(), False,
+                             u'The Https checked box should be Checked')
+            self.assertEqual(self.form.https_settings_group_box.isEnabled(), True,
+                             u'The Https box should be enabled')

=== added file 'tests/functional/openlp_plugins/remotes/test_router.py'
--- tests/functional/openlp_plugins/remotes/test_router.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/remotes/test_router.py	2013-04-14 16:01:29 +0000
@@ -0,0 +1,99 @@
+"""
+This module contains tests for the lib submodule of the Remotes plugin.
+"""
+import os
+
+from unittest import TestCase
+from tempfile import mkstemp
+from mock import MagicMock
+
+from openlp.core.lib import Settings
+from openlp.plugins.remotes.lib.httpserver import HttpRouter, fetch_password, make_sha_hash
+from PyQt4 import QtGui
+
+__default_settings__ = {
+    u'remotes/twelve hour': True,
+    u'remotes/port': 4316,
+    u'remotes/https port': 4317,
+    u'remotes/https enabled': False,
+    u'remotes/user id': u'openlp',
+    u'remotes/password': u'password',
+    u'remotes/authentication enabled': False,
+    u'remotes/ip address': u'0.0.0.0'
+}
+
+
+class TestRouter(TestCase):
+    """
+    Test the functions in the :mod:`lib` module.
+    """
+    def setUp(self):
+        """
+        Create the UI
+        """
+        fd, self.ini_file = mkstemp(u'.ini')
+        Settings().set_filename(self.ini_file)
+        self.application = QtGui.QApplication.instance()
+        Settings().extend_default_settings(__default_settings__)
+        self.router = HttpRouter()
+
+    def tearDown(self):
+        """
+        Delete all the C++ objects at the end so that we don't have a segfault
+        """
+        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(u'itwinkle')
+
+        # THEN: the function should return None
+        self.assertEqual(password, None, u'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(u'openlp')
+        required_password = make_sha_hash(u'password')
+
+        # THEN: the function should return the correct password
+        self.assertEqual(password, required_password, u'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(u'password')
+        test_value = u'5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8'
+
+        # THEN: the function should return the correct password
+        self.assertEqual(required_password, test_value,
+            u'The result for make_sha_hash should return the correct encrypted password')
+
+    def process_http_request_test(self):
+        """
+        Test the router control functionality
+        """
+        # GIVEN: A testing set of Routes
+        mocked_function = MagicMock()
+        test_route = [
+            (r'^/stage/api/poll$', mocked_function),
+        ]
+        self.router.routes = test_route
+
+        # WHEN: called with a poll route
+        self.router.process_http_request(u'/stage/api/poll', None)
+
+        # THEN: the function should have been called only once
+        assert mocked_function.call_count == 1, \
+            u'The mocked function should have been matched and called once.'

=== added directory 'tests/interfaces/openlp_plugins/remotes'
=== added file 'tests/interfaces/openlp_plugins/remotes/__init__.py'
=== added file 'tests/interfaces/openlp_plugins/remotes/test_server.py'
--- tests/interfaces/openlp_plugins/remotes/test_server.py	1970-01-01 00:00:00 +0000
+++ tests/interfaces/openlp_plugins/remotes/test_server.py	2013-04-14 16:01:29 +0000
@@ -0,0 +1,138 @@
+"""
+This module contains tests for the lib submodule of the Remotes plugin.
+"""
+import os
+
+from unittest import TestCase
+from tempfile import mkstemp
+from mock import MagicMock
+import urllib2
+import cherrypy
+
+from BeautifulSoup import BeautifulSoup
+
+from openlp.core.lib import Settings
+from openlp.plugins.remotes.lib.httpserver import HttpServer
+from PyQt4 import QtGui
+
+__default_settings__ = {
+    u'remotes/twelve hour': True,
+    u'remotes/port': 4316,
+    u'remotes/https port': 4317,
+    u'remotes/https enabled': False,
+    u'remotes/user id': u'openlp',
+    u'remotes/password': u'password',
+    u'remotes/authentication enabled': False,
+    u'remotes/ip address': u'0.0.0.0'
+}
+
+
+class TestRouter(TestCase):
+    """
+    Test the functions in the :mod:`lib` module.
+    """
+    def setUp(self):
+        """
+        Create the UI
+        """
+        fd, self.ini_file = mkstemp(u'.ini')
+        Settings().set_filename(self.ini_file)
+        self.application = QtGui.QApplication.instance()
+        Settings().extend_default_settings(__default_settings__)
+        self.server = HttpServer()
+
+    def tearDown(self):
+        """
+        Delete all the C++ objects at the end so that we don't have a segfault
+        """
+        del self.application
+        os.unlink(self.ini_file)
+        self.server.close()
+
+    def start_server(self):
+        """
+        Common function to start server then mock out the router.  CherryPy crashes if you mock before you start
+        """
+        self.server.start_server()
+        self.server.router = MagicMock()
+        self.server.router.process_http_request = process_http_request
+
+    def start_default_server_test(self):
+        """
+        Test the default server serves the correct initial page
+        """
+        # GIVEN: A default configuration
+        Settings().setValue(u'remotes/authentication enabled', False)
+        self.start_server()
+
+        # WHEN: called the route location
+        code, page = call_remote_server(u'http://localhost:4316')
+
+        # THEN: default title will be returned
+        self.assertEqual(BeautifulSoup(page).title.text, u'OpenLP 2.1 Remote',
+            u'The default menu should be returned')
+
+    def start_authenticating_server_test(self):
+        """
+        Test the default server serves the correctly with authentication
+        """
+        # GIVEN: A default authorised configuration
+        Settings().setValue(u'remotes/authentication enabled', True)
+        self.start_server()
+
+        # WHEN: called the route location with no user details
+        code, page = call_remote_server(u'http://localhost:4316')
+
+        # THEN: then server will ask for details
+        self.assertEqual(code, 401, u'The basic authorisation request should be returned')
+
+        # WHEN: called the route location with user details
+        code, page = call_remote_server(u'http://localhost:4316', u'openlp', u'password')
+
+        # THEN: default title will be returned
+        self.assertEqual(BeautifulSoup(page).title.text, u'OpenLP 2.1 Remote',
+                         u'The default menu should be returned')
+
+        # WHEN: called the route location with incorrect user details
+        code, page = call_remote_server(u'http://localhost:4316', u'itwinkle', u'password')
+
+        # THEN: then server will ask for details
+        self.assertEqual(code, 401, u'The basic authorisation request should be returned')
+
+
+def call_remote_server(url, username=None, password=None):
+    """
+    Helper function
+
+    ``username``
+        The username.
+
+    ``password``
+        The password.
+    """
+    if username:
+        passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
+        passman.add_password(None, url, username, password)
+        authhandler = urllib2.HTTPBasicAuthHandler(passman)
+        opener = urllib2.build_opener(authhandler)
+        urllib2.install_opener(opener)
+    try:
+        page = urllib2.urlopen(url)
+        return 0, page.read()
+    except urllib2.HTTPError, e:
+        return e.code, u''
+
+
+def process_http_request(url_path, *args):
+    """
+    Override function to make the Mock work but does nothing.
+
+    ``Url_path``
+        The url_path.
+
+    ``*args``
+        Some args.
+    """
+    cherrypy.response.status = 200
+    return None
+

=== added directory 'tests/resources/remotes'
=== added file 'tests/resources/remotes/openlp.crt'
=== added file 'tests/resources/remotes/openlp.key'

Follow ups