openlp-core team mailing list archive
-
openlp-core team
-
Mailing list archive
-
Message #20180
[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)
For more details, see:
https://code.launchpad.net/~trb143/openlp/cherrypy/+merge/156118
Test merge to see what it looks like.
Code now works but the tests now need to be sorted.
SSL and Login working correctly and works with an http client.
--
https://code.launchpad.net/~trb143/openlp/cherrypy/+merge/156118
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-03-29 08:36:25 +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,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-29 08:36:25 +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-29 08:36:25 +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/settingsform.py'
--- openlp/core/ui/settingsform.py 2013-03-19 19:43:22 +0000
+++ openlp/core/ui/settingsform.py 2013-03-29 08:36:25 +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-03-26 07:34:42 +0000
+++ openlp/core/ui/slidecontroller.py 2013-03-29 08:36:25 +0000
@@ -358,8 +358,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:
@@ -371,13 +372,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/core/utils/applocation.py'
--- openlp/core/utils/applocation.py 2013-02-27 13:01:42 +0000
+++ openlp/core/utils/applocation.py 2013-03-29 08:36:25 +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-03-28 20:08:07 +0000
+++ openlp/plugins/alerts/lib/alertsmanager.py 2013-03-29 08:36:25 +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):
"""
=== 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-29 08:36:25 +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-29 08:36:25 +0000
@@ -0,0 +1,48 @@
+<!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">
+</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>
=== 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-29 08:36:25 +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-29 08:36:25 +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-29 08:36:25 +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
+ """
+ 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_authentication(*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)
+ a = cherrypy.request
+ print a
+ 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_authentication)
+
+
+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'
+ cherrypy.response.status = 200
+ 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)
+ 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-26 07:17:29 +0000
+++ openlp/plugins/remotes/lib/httpserver.py 2013-03-29 08:36:25 +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,184 @@
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 md5, 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 sha_password_encrypter(password):
+
+ return sha(ntob(password)).hexdigest()
+
+
+def fetch_password(username):
+ if username != Settings().value(u'remotes/user id'):
+ return None
+ print "fetch password", username
+ return sha(ntob(Settings().value(u'remotes/password'))).hexdigest()
class HttpServer(object):
"""
Ability to control OpenLP via a web browser.
"""
+
+ _cp_config = {
+ 'tools.sessions.on': True,
+ 'tools.auth.on': True
+ }
+
def __init__(self, plugin):
"""
- 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.router = HttpRouter()
+
+ def start_server(self):
+ """
+ Start the http server based on configuration.
+ """
+ log.debug(u'Start CherryPy server')
+ # Define to security levels and add 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):
+ 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)
+ cherrypy.config.update({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')
+ 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': sha_password_encrypter},
+ 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
+
+ def reload_config(self):
+ cherrypy.tree.mount(self.root, '/', config=self.define_config())
+ cherrypy.config.reset()
+
+ class Public:
+ @cherrypy.expose
+ def default(self, *args, **kwargs):
+ print "public"
+ 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:
+ @cherrypy.expose
+ def default(self, *args, **kwargs):
+ print "files"
+ url = urlparse.urlparse(cherrypy.url())
+ return self.router.process_http_request(url.path, *args)
+
+ class Stage:
+ @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):
+ """
+ A single connection, this handles communication between the server and the client.
+ """
+
+ def __init__(self):
+ """
+ Initialise the CherryPy Server
+ """
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 where secure or insecure
+ """
+ url = urlparse.urlparse(cherrypy.url())
+ 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
@@ -278,43 +340,13 @@
'no_results': translate('RemotePlugin.Mobile', 'No Results'),
'options': translate('RemotePlugin.Mobile', 'Options'),
'service': translate('RemotePlugin.Mobile', 'Service'),
- 'slides': translate('RemotePlugin.Mobile', 'Slides')
+ 'slides': translate('RemotePlugin.Mobile', 'Slides'),
+ 'title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 User Login'),
+ 'from_page': "",
+ 'message': "",
+ 'username': "username"
}
- 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 +361,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 +392,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 +412,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 +434,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 +479,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 +528,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 +540,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.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 +618,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-03-29 08:36:25 +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,6 +169,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,31 +196,67 @@
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()
+ 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):
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-03-29 08:36:25 +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'
}
@@ -61,6 +68,7 @@
log.debug(u'initialise')
Plugin.initialise(self)
self.server = HttpServer(self)
+ 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-03-29 08:36:25 +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-29 08:36:25 +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-29 08:36:25 +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-29 08:36:25 +0000
@@ -0,0 +1,81 @@
+"""
+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, MagicMock
+
+from openlp.core.lib import Settings
+from openlp.plugins.remotes.lib.httpauth import check_credentials, check_authentication
+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'
+}
+
+SESSION_KEY = '_cp_openlp'
+
+
+class TestAuth(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 Authentication check routine with credentials.
+ """
+ # GIVEN: A user and password in settings
+ Settings().setValue(u'remotes/user id', u'twinkle')
+ Settings().setValue(u'remotes/password', u'mongoose')
+
+ # WHEN: We run the function with no input
+ authenticated = check_credentials(u'', u'')
+
+ # THEN: The authentication will fail with an error message
+ self.assertEqual(authenticated, u'Incorrect username or password.',
+ u'The return should be a error message string')
+
+ # WHEN: We run the function with the correct input
+ authenticated = check_credentials(u'twinkle', u'mongoose')
+
+ # THEN: The authentication will pass.
+ self.assertEqual(authenticated, None, u'The return should be a None string')
+
+ def check_auth_inactive_test(self):
+ """
+ Test the Authentication check routine when inactive.
+ """
+ # GIVEN: A access which is secure
+ Settings().setValue(u'remotes/authentication enabled', False)
+
+ # WHEN: We run the function with no input
+ with patch(u'cherrypy.request.config'):
+ authenticated = check_authentication(None, None)
+
+ # THEN: The authentication will fail with an error message
+ self.assertEqual(authenticated, None, u'The authentication should return None as not required')
=== added file 'tests/functional/openlp_plugins/remotes/test_server.py'
--- tests/functional/openlp_plugins/remotes/test_server.py 1970-01-01 00:00:00 +0000
+++ tests/functional/openlp_plugins/remotes/test_server.py 2013-03-29 08:36:25 +0000
@@ -0,0 +1,80 @@
+"""
+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, MagicMock
+import cherrypy
+
+from openlp.core.lib import Settings
+from openlp.plugins.remotes.lib.httpserver import HttpConnection
+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'
+}
+
+SESSION_KEY = '_cp_openlp'
+
+
+class TestAuth(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 = HttpConnection(None)
+
+ 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 process_http_request_test(self):
+ """
+ Test the Authentication check routine with credentials.
+ """
+ # GIVEN: A user and password in settings
+ cherrypy = MagicMock()
+ cherrypy.url.return_value = "nosetest/apl/poll"
+
+ print cherrypy.url()
+
+ with patch(u'url.path') as mocked_url:
+ mocked_url.return_value = "nosetest/apl/poll"
+ self.server._process_http_request(None, None)
+
+ self.assertFalse()
+
+ # WHEN: We run the function with no input
+ #authenticated = check_credentials(u'', u'')
+
+ # THEN: The authentication will fail with an error message
+ #self.assertEqual(authenticated, u'Incorrect username or password.',
+ # u'The return should be a error message string')
+
+ # WHEN: We run the function with the correct input
+ #authenticated = check_credentials(u'twinkle', u'mongoose')
+
+ # THEN: The authentication will pass.
+ #self.assertEqual(authenticated, None, u'The return should be a None string')
+
+
+
=== added directory 'tests/interfaces/openlp_plugins/remotes'
=== added file 'tests/interfaces/openlp_plugins/remotes/__init__.py'
=== added file 'tests/interfaces/openlp_plugins/remotes/test_remoteserver.py'
--- tests/interfaces/openlp_plugins/remotes/test_remoteserver.py 1970-01-01 00:00:00 +0000
+++ tests/interfaces/openlp_plugins/remotes/test_remoteserver.py 2013-03-29 08:36:25 +0000
@@ -0,0 +1,85 @@
+"""
+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, MagicMock
+
+
+import urllib
+from BeautifulSoup import BeautifulSoup, NavigableString, Tag
+
+from openlp.core.lib import Settings
+from openlp.plugins.remotes.lib 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'
+}
+
+SESSION_KEY = '_cp_openlp'
+
+
+class TestRemoteServer(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(self)
+
+ 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())
+ self.server.close()
+
+ def check_access_test(self):
+ """
+ Test the Authentication check routine.
+ """
+ # GIVEN: A user and password in settings
+ Settings().setValue(u'remotes/user id', u'twinkle')
+ Settings().setValue(u'remotes/password', u'mongoose')
+
+ # WHEN: We run the function with no input
+ authenticated = check_credentials(u'', u'')
+
+ # THEN: The authentication will fail with an error message
+ self.assertEqual(authenticated, u'Incorrect username or password.',
+ u'The return should be a error message string')
+
+ # WHEN: We run the function with the correct input
+ authenticated = check_credentials(u'twinkle', u'mongoose')
+
+ # THEN: The authentication will pass.
+ self.assertEqual(authenticated, None, u'The return should be a None string')
+
+ def check_auth_inactive_test(self):
+ """
+ Test the Authentication check routine.
+ """
+ # GIVEN: A access which is secure
+ Settings().setValue(u'remotes/authentication enabled', True)
+
+ # WHEN: We run the function with no input
+ f = urllib.urlopen("http://localhost:4316")
+ soup = BeautifulSoup(f.read())
+ print soup.title.string