← 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:
  Raoul Snyman (raoul-snyman)

For more details, see:
https://code.launchpad.net/~trb143/openlp/cherrypy/+merge/155071

Test merge to see what it looks like
-- 
https://code.launchpad.net/~trb143/openlp/cherrypy/+merge/155071
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/lib/mediamanageritem.py'
--- openlp/core/lib/mediamanageritem.py	2013-03-20 19:38:20 +0000
+++ openlp/core/lib/mediamanageritem.py	2013-03-22 21:22:23 +0000
@@ -94,7 +94,7 @@
         self.remote_triggered = None
         self.single_service_item = True
         self.quick_preview_allowed = False
-        self.hasSearch = False
+        self.has_search = False
         self.page_layout = QtGui.QVBoxLayout(self)
         self.page_layout.setSpacing(0)
         self.page_layout.setMargin(0)
@@ -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,12 @@
         else:
             self.go_live()
 
+    def go_live_remote(self, message):
+        """
+        Remote Call wrapper
+        """
+        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 +532,12 @@
                 for item in items:
                     self.add_to_service(item)
 
+    def add_to_service_remote(self, message):
+        """
+        Remote Call wrapper
+        """
+        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/ui/exceptionform.py'
--- openlp/core/ui/exceptionform.py	2013-03-14 10:46:19 +0000
+++ openlp/core/ui/exceptionform.py	2013-03-22 21:22:23 +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-03-19 19:43:22 +0000
+++ openlp/core/ui/servicemanager.py	2013-03-22 21:22:23 +0000
@@ -270,7 +270,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):
         """
@@ -313,6 +312,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):
         """
@@ -1006,7 +1007,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/slidecontroller.py'
--- openlp/core/ui/slidecontroller.py	2013-03-19 19:43:22 +0000
+++ openlp/core/ui/slidecontroller.py	2013-03-22 21:22:23 +0000
@@ -99,7 +99,7 @@
             u'delay_spin_box'
         ]
         self.audio_list = [
-            u'audio_pause_item',
+            u'audioPauseItem',
             u'audio_time_label'
         ]
         self.wide_menu = [
@@ -357,8 +357,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:
@@ -370,13 +371,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):
         """
@@ -1076,6 +1080,7 @@
         """
         Go to the next slide.
         """
+        print "next"
         if not self.service_item:
             return
         Registry().execute(u'%s_next' % self.service_item.name.lower(), [self.service_item, self.is_live])
@@ -1103,6 +1108,7 @@
         """
         Go to the previous slide.
         """
+        print "prev"
         if not self.service_item:
             return
         Registry().execute(u'%s_previous' % self.service_item.name.lower(), [self.service_item, self.is_live])

=== modified file 'openlp/core/utils/applocation.py'
--- openlp/core/utils/applocation.py	2013-02-27 13:01:42 +0000
+++ openlp/core/utils/applocation.py	2013-03-22 21:22:23 +0000
@@ -63,6 +63,7 @@
     VersionDir = 5
     CacheDir = 6
     LanguageDir = 7
+    SharedData = 8
 
     # Base path where data/config/cache dir is located
     BaseDir = None
@@ -149,18 +150,18 @@
     if sys.platform == u'win32':
         if dir_type == AppLocation.DataDir:
             return os.path.join(unicode(os.getenv(u'APPDATA'), encoding), u'openlp', u'data')
-        elif dir_type == AppLocation.LanguageDir:
+        elif dir_type == AppLocation.LanguageDir or dir_type == AppLocation.SharedData:
             return os.path.split(openlp.__file__)[0]
         return os.path.join(unicode(os.getenv(u'APPDATA'), encoding), u'openlp')
     elif sys.platform == u'darwin':
         if dir_type == AppLocation.DataDir:
             return os.path.join(unicode(os.getenv(u'HOME'), encoding),
                                 u'Library', u'Application Support', u'openlp', u'Data')
-        elif dir_type == AppLocation.LanguageDir:
+        elif dir_type == AppLocation.LanguageDir or dir_type == AppLocation.SharedData:
             return os.path.split(openlp.__file__)[0]
         return os.path.join(unicode(os.getenv(u'HOME'), encoding), u'Library', u'Application Support', u'openlp')
     else:
-        if dir_type == AppLocation.LanguageDir:
+        if dir_type == AppLocation.LanguageDir or dir_type == AppLocation.SharedData:
             prefixes = [u'/usr/local', u'/usr']
             for prefix in prefixes:
                 directory = os.path.join(prefix, u'share', u'openlp')

=== modified file 'openlp/plugins/alerts/lib/alertsmanager.py'
--- openlp/plugins/alerts/lib/alertsmanager.py	2013-02-16 10:40:05 +0000
+++ openlp/plugins/alerts/lib/alertsmanager.py	2013-03-22 21:22:23 +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):
         """
@@ -87,11 +89,11 @@
         if not self.alert_list:
             return
         text = self.alert_list.pop(0)
-        alertTab = self.parent().settingsTab
-        self.live_controller.display.alert(text, alertTab.location)
+        alert_tab = self.parent().settings_tab
+        self.live_controller.display.alert(text, alert_tab.location)
         # Check to see if we have a timer running.
         if self.timer_id == 0:
-            self.timer_id = self.startTimer(int(alertTab.timeout) * 1000)
+            self.timer_id = self.startTimer(int(alert_tab.timeout) * 1000)
 
     def timerEvent(self, event):
         """
@@ -103,7 +105,7 @@
         """
         log.debug(u'timer event')
         if event.timerId() == self.timer_id:
-            alertTab = self.parent().settingsTab
+            alertTab = self.parent().settings_tab
             self.live_controller.display.alert(u'', alertTab.location)
         self.killTimer(self.timer_id)
         self.timer_id = 0

=== modified file 'openlp/plugins/bibles/lib/mediaitem.py'
--- openlp/plugins/bibles/lib/mediaitem.py	2013-03-19 22:00:50 +0000
+++ openlp/plugins/bibles/lib/mediaitem.py	2013-03-22 21:22:23 +0000
@@ -67,7 +67,7 @@
         # Place to store the search results for both bibles.
         self.settings = self.plugin.settings_tab
         self.quick_preview_allowed = True
-        self.hasSearch = True
+        self.has_search = True
         self.search_results = {}
         self.second_search_results = {}
         self.check_search_result()

=== modified file 'openlp/plugins/custom/lib/mediaitem.py'
--- openlp/plugins/custom/lib/mediaitem.py	2013-03-20 18:35:28 +0000
+++ openlp/plugins/custom/lib/mediaitem.py	2013-03-22 21:22:23 +0000
@@ -60,7 +60,7 @@
         self.edit_custom_form = EditCustomForm(self, self.main_window, self.plugin.manager)
         self.single_service_item = False
         self.quick_preview_allowed = True
-        self.hasSearch = True
+        self.has_search = True
         # Holds information about whether the edit is remotely triggered and
         # which Custom is required.
         self.remoteCustom = -1

=== modified file 'openlp/plugins/images/lib/mediaitem.py'
--- openlp/plugins/images/lib/mediaitem.py	2013-03-20 19:38:20 +0000
+++ openlp/plugins/images/lib/mediaitem.py	2013-03-22 21:22:23 +0000
@@ -53,7 +53,7 @@
         self.icon_path = u'images/image'
         MediaManagerItem.__init__(self, parent, plugin)
         self.quick_preview_allowed = True
-        self.hasSearch = True
+        self.has_search = True
         self.manager = plugin.manager
         self.choose_group_form = ChooseGroupForm(self)
         self.add_group_form = AddGroupForm(self)

=== modified file 'openlp/plugins/media/lib/mediaitem.py'
--- openlp/plugins/media/lib/mediaitem.py	2013-03-19 22:00:50 +0000
+++ openlp/plugins/media/lib/mediaitem.py	2013-03-22 21:22:23 +0000
@@ -59,7 +59,7 @@
         self.automatic = u''
         MediaManagerItem.__init__(self, parent, plugin)
         self.single_service_item = False
-        self.hasSearch = True
+        self.has_search = True
         self.media_object = None
         self.display_controller = DisplayController(parent)
         self.display_controller.controller_layout = QtGui.QVBoxLayout()

=== modified file 'openlp/plugins/presentations/lib/mediaitem.py'
--- openlp/plugins/presentations/lib/mediaitem.py	2013-03-19 22:00:50 +0000
+++ openlp/plugins/presentations/lib/mediaitem.py	2013-03-22 21:22:23 +0000
@@ -58,7 +58,7 @@
         self.Automatic = u''
         MediaManagerItem.__init__(self, parent, plugin)
         self.message_listener = MessageListener(self)
-        self.hasSearch = True
+        self.has_search = True
         self.single_service_item = False
         Registry().register_function(u'mediaitem_presentation_rebuild', self.populate_display_types)
         Registry().register_function(u'mediaitem_suffixes', self.build_file_mask_string)

=== added file 'openlp/plugins/remotes/html/login.css'
--- openlp/plugins/remotes/html/login.css	1970-01-01 00:00:00 +0000
+++ openlp/plugins/remotes/html/login.css	2013-03-22 21:22:23 +0000
@@ -0,0 +1,46 @@
+/******************************************************************************
+* 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                                             *
+* --------------------------------------------------------------------------- *
+* 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                          *
+******************************************************************************/
+
+.ui-icon-blank {
+    background-image: url(images/ui-icon-blank.png);
+}
+
+.ui-icon-unblank {
+    background-image: url(images/ui-icon-unblank.png);
+}
+
+/* Overwrite style from jquery-mobile.css */
+.ui-li .ui-btn-text a.ui-link-inherit{
+    white-space: normal;
+}
+
+.ui-page{
+    padding: 100px 100px 100px 100px;
+    width: 300px;
+}
+.ui-input-text{
+    width: 30px;
+}
\ No newline at end of file

=== added file 'openlp/plugins/remotes/html/login.html'
--- openlp/plugins/remotes/html/login.html	1970-01-01 00:00:00 +0000
+++ openlp/plugins/remotes/html/login.html	2013-03-22 21:22:23 +0000
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/html";>
+<!--
+###############################################################################
+# 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                          #
+###############################################################################
+-->
+<html>
+<head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, minimum-scale=1, maximum-scale=1" />
+    <title>${title}</title>
+    <link rel="stylesheet" href="/files/jquery.mobile.css" />
+    <link rel="stylesheet" href="/files/login.css" />
+    <link rel="shortcut icon" type="image/x-icon" href="/files/images/favicon.ico">
+    <script type="text/javascript" src="/files/jquery.js"></script>
+    <script type="text/javascript" src="/files/openlp.js"></script>
+    <script type="text/javascript" src="/files/jquery.mobile.js"></script>
+</head>
+<body>
+<form method="post" action="/auth/login">
+    <input type="hidden" name="from_page" value="${from_page}" />
+    <h2>${message}</h2>
+    </p>
+    <strong>User name: </strong><input type="text" name="username" value="${username}" /><br />
+    <strong>Password: </strong><input type="password" name="password" /><br />
+    <input type="submit" value="Log in" />
+</form>
+</body></html>
\ No newline at end of file

=== 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-03-22 21:22:23 +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-03-22 21:22:23 +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 ||

=== added file 'openlp/plugins/remotes/lib/httpauth.py'
--- openlp/plugins/remotes/lib/httpauth.py	1970-01-01 00:00:00 +0000
+++ openlp/plugins/remotes/lib/httpauth.py	2013-03-22 21:22:23 +0000
@@ -0,0 +1,162 @@
+# -*- 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 manages the HTTP authorisation logic.  This code originates from
+http://tools.cherrypy.org/wiki/AuthenticationAndAccessRestrictions
+
+"""
+
+import cherrypy
+import logging
+import os
+
+from mako.template import Template
+
+from openlp.core.lib import Settings
+from openlp.core.utils import AppLocation, translate
+
+SESSION_KEY = '_cp_openlp'
+
+log = logging.getLogger(__name__)
+
+
+def check_credentials(user_name, password):
+    """
+    Verifies credentials for username and password.
+    Returns None on success or a string describing the error on failure
+    """
+    print "check"
+    if user_name == Settings().value(u'remotes/user id') and password == Settings().value(u'remotes/password'):
+        return None
+    else:
+        return translate('RemotePlugin.Mobile', 'Incorrect username or password.')
+
+
+def check_auth(*args, **kwargs):
+    """
+    A tool that looks in config for 'auth.require'. If found and it
+    is not None, a login is required and the entry is evaluated as a list of
+    conditions that the user must fulfill
+    """
+    conditions = cherrypy.request.config.get('auth.require', None)
+    if not Settings().value(u'remotes/authentication enabled'):
+        return None
+    if conditions is not None:
+        username = cherrypy.session.get(SESSION_KEY)
+        if username:
+            cherrypy.request.login = username
+            for condition in conditions:
+                # A condition is just a callable that returns true or false
+                if not condition():
+                    raise cherrypy.HTTPRedirect("/auth/login")
+        else:
+            raise cherrypy.HTTPRedirect("/auth/login")
+
+cherrypy.tools.auth = cherrypy.Tool('before_handler', check_auth)
+
+
+def require_auth(*conditions):
+    """
+    A decorator that appends conditions to the auth.require config variable.
+    """
+    def decorate(f):
+        """
+        Lets process a decoration.
+        """
+        if not hasattr(f, '_cp_config'):
+            f._cp_config = dict()
+        if 'auth.require' not in f._cp_config:
+            f._cp_config['auth.require'] = []
+        f._cp_config['auth.require'].extend(conditions)
+        return f
+    return decorate
+
+
+class AuthController(object):
+
+    def on_login(self, username):
+        """
+        Called on successful login
+        """
+        pass
+
+    def on_logout(self, username):
+        """
+        Called on logout
+        """
+        pass
+
+    def get_login_form(self, username, message=None, from_page="/"):
+        """
+        Provides a login form
+        """
+        if not message:
+            message = translate('RemotePlugin.Mobile', 'Enter login information')
+        variables = {
+            'title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 User Login'),
+            'from_page': from_page,
+            'message': message,
+            'username': username
+        }
+        directory = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html')
+        login_html = os.path.normpath(os.path.join(directory, u'login.html'))
+        html = Template(filename=login_html, input_encoding=u'utf-8', output_encoding=u'utf-8').render(**variables)
+        cherrypy.response.headers['Content-Type'] = u'text/html'
+        return html
+
+    @cherrypy.expose
+    def login(self, username=None, password=None, from_page="/"):
+        """
+        Provides the actual login control
+        """
+        if username is None or password is None:
+            return self.get_login_form("", from_page=from_page)
+        error_msg = check_credentials(username, password)
+        if error_msg:
+            return self.get_login_form(username, from_page, error_msg,)
+        else:
+            cherrypy.session[SESSION_KEY] = cherrypy.request.login = username
+            self.on_login(username)
+            print from_page
+            raise cherrypy.HTTPRedirect(from_page or "/")
+
+    @cherrypy.expose
+    def logout(self, from_page="/"):
+        """
+        Provides the actual logout functions
+        """
+        sess = cherrypy.session
+        username = sess.get(SESSION_KEY, None)
+        sess[SESSION_KEY] = None
+        if username:
+            cherrypy.request.login = None
+            self.on_logout(username)
+        raise cherrypy.HTTPRedirect(from_page or "/")
+

=== modified file 'openlp/plugins/remotes/lib/httpserver.py'
--- openlp/plugins/remotes/lib/httpserver.py	2013-03-19 22:00:50 +0000
+++ openlp/plugins/remotes/lib/httpserver.py	2013-03-22 21:22:23 +0000
@@ -119,41 +119,23 @@
 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 openlp.plugins.remotes.lib.httpauth import AuthController, require_auth
 
 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
-
-
 class HttpServer(object):
     """
     Ability to control OpenLP via a web browser.
     """
+
     def __init__(self, plugin):
         """
         Initialise the httpserver, and start the server.
@@ -162,79 +144,140 @@
         self.plugin = plugin
         self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html')
         self.connections = []
-        self.start_tcp()
+        self.conf = {'/files': {u'tools.staticdir.on': True,
+                                u'tools.staticdir.dir': self.html_dir}}
+        self.start_server()
 
-    def start_tcp(self):
+    def start_server(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'Start CherryPy server')
+        if Settings().value(self.plugin.settings_section + u'/https enabled'):
+            port = Settings().value(self.plugin.settings_section + u'/https port')
+            address = Settings().value(self.plugin.settings_section + u'/ip address')
+            shared_data = AppLocation.get_directory(AppLocation.SharedData)
+            server_config = {u'server.socket_host': str(address),
+                             u'server.socket_port': port,
+                             u'server.ssl_certificate': os.path.join(shared_data, u'openlp.crt'),
+                             u'server.ssl_private_key': os.path.join(shared_data, u'openlp.key')}
+        else:
+            port = Settings().value(self.plugin.settings_section + u'/port')
+            address = Settings().value(self.plugin.settings_section + u'/ip address')
+            server_config = {u'server.socket_host': str(address),
+                             u'server.socket_port': port}
+        cherrypy.config.update(server_config)
+        cherrypy.config.update({'environment': 'embedded'})
+        cherrypy.config.update({'engine.autoreload_on': False})
+        cherrypy.tree.mount(HttpConnection(self), '/', config=self.conf)
+        # Turn off the flood of access messages cause by poll
+        cherrypy.log.access_log.propagate = False
+        cherrypy.engine.start()
         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)
-
     def close(self):
         """
         Close down the http server.
         """
         log.debug(u'close http server')
-        self.server.close()
+        cherrypy.engine.exit()
+        cherrypy.engine.stop()
 
 
 class HttpConnection(object):
     """
-    A single connection, this handles communication between the server
-    and the client.
+    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
+    _cp_config = {
+        'tools.sessions.on': True,
+        'tools.auth.on': True
+    }
+
+    auth = AuthController()
+
+    def __init__(self, parent):
+        """
+        Initialise the CherryPy Server
+        """
         self.parent = parent
         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()
 
+    @cherrypy.expose
+    @require_auth()
+    def default(self, *args, **kwargs):
+        """
+        Handles the requests for the main url.  This is secure depending on settings in config.
+        """
+        #url = urlparse.urlparse(cherrypy.url())
+        #self.url_params = urlparse.parse_qs(url.query)
+        self.request_data = None
+        if isinstance(kwargs, dict):
+            self.request_data = kwargs.get(u'data', None)
+        # Loop through the routes we set up earlier and execute them
+        return self._process_http_request(args, kwargs)
+
+    @cherrypy.expose
+    def stage(self, *args, **kwargs):
+        """
+        Handles the requests for the stage url.  This is not secure.
+        """
+        #url = urlparse.urlparse(cherrypy.url())
+        #self.url_params = urlparse.parse_qs(url.query)
+        return self._process_http_request(args, kwargs)
+
+    @cherrypy.expose
+    def files(self, *args, **kwargs):
+        """
+        Handles the requests for the files url.  This is not secure.
+        """
+        #url = urlparse.urlparse(cherrypy.url())
+        #self.url_params = urlparse.parse_qs(url.query)
+        return self._process_http_request(args, kwargs)
+
+    def _process_http_request(self, args, kwargs):
+        """
+        Common function to process HTTP requests where secure or insecure
+        """
+        url = urlparse.urlparse(cherrypy.url())
+        #self.url_params = kwargs
+        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 +324,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
@@ -331,7 +340,7 @@
             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')
+            return self._http_not_found()
         ext = os.path.splitext(filename)[1]
         html = None
         if ext == u'.html':
@@ -360,11 +369,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 +389,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 +411,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 +456,45 @@
             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'):
+            print event
+            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.
 
@@ -492,9 +505,10 @@
         if action == u'search':
             searches = []
             for plugin in self.plugin_manager.plugins:
-                if plugin.status == PluginStatus.Active and plugin.media_item and plugin.mediaItem.hasSearch:
-                    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'})
+                if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search:
+                    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 +518,54 @@
             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.mediaItem.hasSearch:
+        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):
+        cherrypy.response.status = 200
+
+    def _http_bad_request(self):
+        cherrypy.response.status = 400
+
+    def _http_not_found(self):
+        cherrypy.response.status = 404
+        cherrypy.response.body = ["<html><body>Sorry, an error occurred </body></html>"]
 
     def _get_service_manager(self):
         """
@@ -597,3 +596,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-16 20:52:59 +0000
+++ openlp/plugins/remotes/lib/remotetab.py	2013-03-22 21:22:23 +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.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,10 @@
         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)
 
     def retranslateUi(self):
         self.server_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Server Settings'))
@@ -112,6 +168,16 @@
             '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):
         ip_address = u'localhost'
@@ -129,30 +195,57 @@
                         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):
         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)
+        shared_data = AppLocation.get_directory(AppLocation.SharedData)
+        if not os.path.exists(os.path.join(shared_data, u'openlp.crt')) or \
+                not os.path.exists(os.path.join(shared_data, 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()
 
     def save(self):
         changed = False
         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():
+                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():
             changed = True
         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)
+        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())
         if changed:
-            Registry().register_function(u'remotes_config_updated')
-
-    def onTwelveHourCheckBoxChanged(self, check_state):
+            Registry().execute(u'remotes_config_updated')
+
+
+    def on_twelve_hour_check_box_changed(self, check_state):
         self.twelve_hour = False
         # we have a set value convert to True/False
         if check_state == QtCore.Qt.Checked:

=== 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-03-22 21:22:23 +0000
@@ -37,6 +37,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'
 }
 

=== modified file 'openlp/plugins/songs/lib/mediaitem.py'
--- openlp/plugins/songs/lib/mediaitem.py	2013-03-20 18:35:28 +0000
+++ openlp/plugins/songs/lib/mediaitem.py	2013-03-22 21:22:23 +0000
@@ -81,7 +81,7 @@
         self.remoteSong = -1
         self.editItem = None
         self.quick_preview_allowed = True
-        self.hasSearch = True
+        self.has_search = True
 
     def _updateBackgroundAudio(self, song, item):
         song.media_files = []

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

=== 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-03-22 21:22:23 +0000
@@ -11,7 +11,9 @@
 
 
 class TestSettings(TestCase):
-
+    """
+    Test the functions in the Settings module
+    """
     def setUp(self):
         """
         Create the UI

=== added directory 'tests/functional/openlp_plugins/remotes'
=== added file 'tests/functional/openlp_plugins/remotes/__init__.py'
--- tests/functional/openlp_plugins/remotes/__init__.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/remotes/__init__.py	2013-03-22 21:22:23 +0000
@@ -0,0 +1,1 @@
+__author__ = 'tim'

=== added file 'tests/functional/openlp_plugins/remotes/test_auth.py'
--- tests/functional/openlp_plugins/remotes/test_auth.py	1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/remotes/test_auth.py	2013-03-22 21:22:23 +0000
@@ -0,0 +1,65 @@
+"""
+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.httpauth import check_credentials
+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 TestLib(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__)
+
+    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)
+        os.unlink(Settings().fileName())
+
+    def check_credentials_test(self):
+        """
+        Test the clean_string() function
+        """
+        # GIVEN: A user and password
+        Settings().setValue(u'remotes/user id', u'twinkle')
+        Settings().setValue(u'remotes/password', u'mongoose')
+
+        # WHEN: We run the string through the function
+        authenticated = check_credentials(u'', u'')
+
+        # THEN: The string should be cleaned up and lower-cased
+        self.assertEqual(authenticated, u'Incorrect username or password.',
+                         u'The return should be a error message string')
+
+        # WHEN: We run the string through the function
+        authenticated = check_credentials(u'twinkle', u'mongoose')
+
+        # THEN: The string should be cleaned up and lower-cased
+        self.assertEqual(authenticated, None, u'The return should be a None string')


Follow ups