diff --git a/openlp/core/lib/mediamanageritem.py b/openlp/core/lib/mediamanageritem.py index f6c95aa9e..01329b842 100644 --- a/openlp/core/lib/mediamanageritem.py +++ b/openlp/core/lib/mediamanageritem.py @@ -103,6 +103,9 @@ class MediaManagerItem(QtGui.QWidget): self.retranslateUi() self.auto_select_id = -1 Registry().register_function(u'%s_service_load' % self.plugin.name, self.service_load) + # Need to use event as called across threads and UI is updated + QtCore.QObject.connect(self, QtCore.SIGNAL(u'%s_go_live' % self.plugin.name), self.go_live_remote) + QtCore.QObject.connect(self, QtCore.SIGNAL(u'%s_add_to_service' % self.plugin.name), self.add_to_service_remote) def required_icons(self): """ @@ -481,6 +484,15 @@ class MediaManagerItem(QtGui.QWidget): else: self.go_live() + def go_live_remote(self, message): + """ + Remote Call wrapper + + ``message`` + The passed data item_id:Remote. + """ + self.go_live(message[0], remote=message[1]) + def go_live(self, item_id=None, remote=False): """ Make the currently selected item go live. @@ -523,6 +535,15 @@ class MediaManagerItem(QtGui.QWidget): for item in items: self.add_to_service(item) + def add_to_service_remote(self, message): + """ + Remote Call wrapper + + ``message`` + The passed data item:Remote. + """ + self.add_to_service(message[0], remote=message[1]) + def add_to_service(self, item=None, replace=None, remote=False): """ Add this item to the current service. diff --git a/openlp/core/lib/plugin.py b/openlp/core/lib/plugin.py index dd9843930..b4f851b24 100644 --- a/openlp/core/lib/plugin.py +++ b/openlp/core/lib/plugin.py @@ -103,7 +103,7 @@ class Plugin(QtCore.QObject): ``add_export_menu_Item(export_menu)`` Add an item to the Export menu. - ``create_settings_Tab()`` + ``create_settings_tab()`` Creates a new instance of SettingsTabItem to be used in the Settings dialog. @@ -252,7 +252,7 @@ class Plugin(QtCore.QObject): """ pass - def create_settings_Tab(self, parent): + def create_settings_tab(self, parent): """ Create a tab for the settings window to display the configurable options for this plugin to the user. diff --git a/openlp/core/lib/pluginmanager.py b/openlp/core/lib/pluginmanager.py index 8fc294ea6..db96e3fa7 100644 --- a/openlp/core/lib/pluginmanager.py +++ b/openlp/core/lib/pluginmanager.py @@ -153,7 +153,7 @@ class PluginManager(object): """ for plugin in self.plugins: if plugin.status is not PluginStatus.Disabled: - plugin.create_settings_Tab(self.settings_form) + plugin.create_settings_tab(self.settings_form) def hook_import_menu(self): """ diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index f57243818..c4ac846c9 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -62,12 +62,10 @@ class ItemCapabilities(object): tab when making the previous item live. ``CanEdit`` - The capability to allow the ServiceManager to allow the item to be - edited + The capability to allow the ServiceManager to allow the item to be edited ``CanMaintain`` - The capability to allow the ServiceManager to allow the item to be - reordered. + The capability to allow the ServiceManager to allow the item to be reordered. ``RequiresMedia`` Determines is the service_item needs a Media Player diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index 95ea5694e..9f8cf8093 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -72,11 +72,16 @@ except ImportError: try: import icu try: - ICU_VERSION = icu.VERSION + ICU_VERSION = icu.VERSION except AttributeError: - ICU_VERSION = u'OK' + ICU_VERSION = u'OK' except ImportError: ICU_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') @@ -151,6 +156,7 @@ class ExceptionForm(QtGui.QDialog, Ui_ExceptionDialog): 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'pyICU: %s\n' % ICU_VERSION + \ u'pyUNO bridge: %s\n' % UNO_VERSION + \ u'VLC: %s\n' % VLC_VERSION diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 9092af8ed..9c8e98aec 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -273,7 +273,6 @@ class ServiceManagerDialog(object): Registry().register_function(u'config_screen_changed', self.regenerate_service_Items) Registry().register_function(u'theme_update_global', self.theme_change) Registry().register_function(u'mediaitem_suffix_reset', self.reset_supported_suffixes) - Registry().register_function(u'servicemanager_set_item', self.on_set_item) def drag_enter_event(self, event): """ @@ -315,6 +314,8 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): self.layout.setSpacing(0) self.layout.setMargin(0) self.setup_ui(self) + # Need to use event as called across threads and UI is updated + QtCore.QObject.connect(self, QtCore.SIGNAL(u'servicemanager_set_item'), self.on_set_item) def set_modified(self, modified=True): """ @@ -993,7 +994,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog): 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)) diff --git a/openlp/core/ui/settingsform.py b/openlp/core/ui/settingsform.py index eeb85fa66..bc40539cf 100644 --- a/openlp/core/ui/settingsform.py +++ b/openlp/core/ui/settingsform.py @@ -96,6 +96,7 @@ class SettingsForm(QtGui.QDialog, Ui_SettingsDialog): """ 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 diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index eaeebfba8..35527e1e4 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -360,8 +360,9 @@ class SlideController(DisplayController): # Signals self.preview_list_widget.clicked.connect(self.onSlideSelected) if self.is_live: + # Need to use event as called across threads and UI is updated + QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_toggle_display'), self.toggle_display) Registry().register_function(u'slidecontroller_live_spin_delay', self.receive_spin_delay) - Registry().register_function(u'slidecontroller_toggle_display', self.toggle_display) self.toolbar.set_widget_visible(self.loop_list, False) self.toolbar.set_widget_visible(self.wide_menu, False) else: @@ -373,13 +374,16 @@ class SlideController(DisplayController): 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): """ diff --git a/openlp/plugins/alerts/lib/alertsmanager.py b/openlp/plugins/alerts/lib/alertsmanager.py index 042999a11..830ad05c9 100644 --- a/openlp/plugins/alerts/lib/alertsmanager.py +++ b/openlp/plugins/alerts/lib/alertsmanager.py @@ -49,10 +49,12 @@ class AlertsManager(QtCore.QObject): 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): """ diff --git a/openlp/plugins/bibles/lib/http.py b/openlp/plugins/bibles/lib/http.py index e7056994e..d9fd9c6d4 100644 --- a/openlp/plugins/bibles/lib/http.py +++ b/openlp/plugins/bibles/lib/http.py @@ -55,6 +55,7 @@ UGLY_CHARS = { log = logging.getLogger(__name__) + class BGExtract(object): """ Extract verses from BibleGateway @@ -671,6 +672,7 @@ class HTTPBible(BibleDB): application = property(_get_application) + def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, pre_parse_substitute=None, cleaner=None): """ @@ -715,6 +717,7 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, Registry().get(u'application').process_events() return soup + def send_error_message(error_type): """ Send a standard error message informing the user of an issue. diff --git a/openlp/plugins/media/mediaplugin.py b/openlp/plugins/media/mediaplugin.py index 790dce03c..0bd95a26b 100644 --- a/openlp/plugins/media/mediaplugin.py +++ b/openlp/plugins/media/mediaplugin.py @@ -54,7 +54,7 @@ class MediaPlugin(Plugin): # passed with drag and drop messages self.dnd_id = u'Media' - def create_settings_Tab(self, parent): + def create_settings_tab(self, parent): """ Create the settings Tab """ diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index 7872c25b7..5bc95e388 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -69,7 +69,7 @@ class PresentationPlugin(Plugin): self.icon_path = u':/plugins/plugin_presentations.png' self.icon = build_icon(self.icon_path) - def create_settings_Tab(self, parent): + def create_settings_tab(self, parent): """ Create the settings Tab """ diff --git a/openlp/plugins/remotes/html/openlp.js b/openlp/plugins/remotes/html/openlp.js index 00877e332..3cbe65366 100644 --- a/openlp/plugins/remotes/html/openlp.js +++ b/openlp/plugins/remotes/html/openlp.js @@ -147,7 +147,7 @@ window.OpenLP = { }, pollServer: function () { $.getJSON( - "/api/poll", + "/stage/api/poll", function (data, status) { var prevItem = OpenLP.currentItem; OpenLP.currentSlide = data.results.slide; diff --git a/openlp/plugins/remotes/html/stage.js b/openlp/plugins/remotes/html/stage.js index dcc2e4b70..dff51537c 100644 --- a/openlp/plugins/remotes/html/stage.js +++ b/openlp/plugins/remotes/html/stage.js @@ -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 @@ window.OpenLP = { }, 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 @@ window.OpenLP = { }, pollServer: function () { $.getJSON( - "/api/poll", + "/stage/api/poll", function (data, status) { OpenLP.updateClock(data); if (OpenLP.currentItem != data.results.item || diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index d285baa50..878b197b3 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -43,7 +43,7 @@ the remotes. ``/files/{filename}`` Serve a static file. -``/api/poll`` +``/stage/api/poll`` Poll to see if there are any changes. Returns a JSON-encoded dict of any changes that occurred:: @@ -119,122 +119,198 @@ import os import re import urllib import urlparse +import cherrypy -from PyQt4 import QtCore, QtNetwork from mako.template import Template +from PyQt4 import QtCore from openlp.core.lib import Registry, Settings, PluginStatus, StringContent - from openlp.core.utils import AppLocation, translate +from cherrypy._cpcompat import sha, ntob + log = logging.getLogger(__name__) -class HttpResponse(object): +def make_sha_hash(password): """ - A simple object to encapsulate a pseudo-http response. + Create an encrypted password for the given password. """ - code = '200 OK' - content = '' - headers = { - 'Content-Type': 'text/html; charset="utf-8"\r\n' - } + return sha(ntob(password)).hexdigest() - 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 fetch_password(username): + """ + Fetch the password for a provided user. + """ + if username != Settings().value(u'remotes/user id'): + return None + return make_sha_hash(Settings().value(u'remotes/password')) class HttpServer(object): """ Ability to control OpenLP via a web browser. + This class controls the Cherrypy server and configuration. """ - def __init__(self, plugin): + _cp_config = { + 'tools.sessions.on': True, + 'tools.auth.on': True + } + + def __init__(self): """ - Initialise the httpserver, and start the server. + Initialise the http server, and start the server. """ log.debug(u'Initialise httpserver') - self.plugin = plugin - self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html') - self.connections = [] - self.start_tcp() + self.settings_section = u'remotes' + self.router = HttpRouter() - 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. + Start the http server based on configuration. """ - 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) + log.debug(u'Start CherryPy server') + # Define to security levels and inject the router code + self.root = self.Public() + self.root.files = self.Files() + self.root.stage = self.Stage() + self.root.router = self.router + self.root.files.router = self.router + self.root.stage.router = self.router + cherrypy.tree.mount(self.root, '/', config=self.define_config()) + # Turn off the flood of access messages cause by poll + cherrypy.log.access_log.propagate = False + cherrypy.engine.start() - def new_connection(self): + def define_config(self): """ - A new http connection has been made. Create a client object to handle - communication. + Define the configuration of the server. """ - log.debug(u'new http connection') - socket = self.server.nextPendingConnection() - if socket: - self.connections.append(HttpConnection(self, socket)) + if Settings().value(self.settings_section + u'/https enabled'): + port = Settings().value(self.settings_section + u'/https port') + address = Settings().value(self.settings_section + u'/ip address') + local_data = AppLocation.get_directory(AppLocation.DataDir) + cherrypy.config.update({u'server.socket_host': str(address), + u'server.socket_port': port, + u'server.ssl_certificate': os.path.join(local_data, u'remotes', u'openlp.crt'), + u'server.ssl_private_key': os.path.join(local_data, u'remotes', u'openlp.key')}) + else: + port = Settings().value(self.settings_section + u'/port') + address = Settings().value(self.settings_section + u'/ip address') + cherrypy.config.update({u'server.socket_host': str(address)}) + cherrypy.config.update({u'server.socket_port': port}) + cherrypy.config.update({u'environment': u'embedded'}) + cherrypy.config.update({u'engine.autoreload_on': False}) + directory_config = {u'/': {u'tools.staticdir.on': True, + u'tools.staticdir.dir': self.router.html_dir, + u'tools.basic_auth.on': Settings().value(u'remotes/authentication enabled'), + u'tools.basic_auth.realm': u'OpenLP Remote Login', + u'tools.basic_auth.users': fetch_password, + u'tools.basic_auth.encrypt': make_sha_hash}, + u'/files': {u'tools.staticdir.on': True, + u'tools.staticdir.dir': self.router.html_dir, + u'tools.basic_auth.on': False}, + u'/stage': {u'tools.staticdir.on': True, + u'tools.staticdir.dir': self.router.html_dir, + u'tools.basic_auth.on': False}} + return directory_config - def close_connection(self, connection): + class Public(object): """ - The connection has been closed. Clean up + Main access class with may have security enabled on it. """ - log.debug(u'close http connection') - if connection in self.connections: - self.connections.remove(connection) + @cherrypy.expose + def default(self, *args, **kwargs): + self.router.request_data = None + if isinstance(kwargs, dict): + self.router.request_data = kwargs.get(u'data', None) + url = urlparse.urlparse(cherrypy.url()) + return self.router.process_http_request(url.path, *args) + + class Files(object): + """ + Provides access to files and has no security available. These are read only accesses + """ + @cherrypy.expose + def default(self, *args, **kwargs): + url = urlparse.urlparse(cherrypy.url()) + return self.router.process_http_request(url.path, *args) + + class Stage(object): + """ + Stageview is read only so security is not relevant and would reduce it's usability + """ + @cherrypy.expose + def default(self, *args, **kwargs): + url = urlparse.urlparse(cherrypy.url()) + return self.router.process_http_request(url.path, *args) def close(self): """ Close down the http server. """ log.debug(u'close http server') - self.server.close() + cherrypy.engine.exit() -class HttpConnection(object): +class HttpRouter(object): """ - A single connection, this handles communication between the server - and the client. + This code is called by the HttpServer upon a request and it processes it based on the routing table. """ - def __init__(self, parent, socket): + def __init__(self): """ - Initialise the http connection. Listen out for socket signals. + Initialise the router """ - log.debug(u'Initialise HttpConnection: %s' % socket.peerAddress()) - self.socket = socket - 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() + self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), u'remotes', u'html') + + def process_http_request(self, url_path, *args): + """ + Common function to process HTTP requests + + ``url_path`` + The requested URL. + + ``*args`` + Any passed data. + """ + response = None + for route, func in self.routes: + match = re.match(route, url_path) + if match: + log.debug('Route "%s" matched "%s"', route, url_path) + args = [] + for param in match.groups(): + args.append(param) + response = func(*args) + break + if response: + return response + else: + return self._http_not_found() def _get_service_items(self): + """ + Read the service item in use and return the data as a json object + """ service_items = [] if self.live_controller.service_item: current_unique_identifier = self.live_controller.service_item.unique_identifier @@ -281,40 +357,6 @@ class HttpConnection(object): 'slides': translate('RemotePlugin.Mobile', 'Slides') } - def ready_read(self): - """ - Data has been sent from the client. Respond to it - """ - log.debug(u'ready to read socket') - if self.socket.canReadLine(): - data = str(self.socket.readLine()) - try: - log.debug(u'received: ' + data) - except UnicodeDecodeError: - # Malicious request containing non-ASCII characters. - self.close() - return - words = data.split(' ') - response = None - if words[0] == u'GET': - url = urlparse.urlparse(words[1]) - self.url_params = urlparse.parse_qs(url.query) - # Loop through the routes we set up earlier and execute them - for route, func in self.routes: - match = re.match(route, url.path) - if match: - log.debug('Route "%s" matched "%s"', route, url.path) - args = [] - for param in match.groups(): - args.append(param) - response = func(*args) - break - if response: - self.send_response(response) - else: - self.send_response(HttpResponse(code='404 Not Found')) - self.close() - def serve_file(self, filename=None): """ Send a file to the socket. For now, just a subset of file types @@ -329,9 +371,9 @@ class HttpConnection(object): filename = u'index.html' elif filename == u'stage': filename = u'stage.html' - path = os.path.normpath(os.path.join(self.parent.html_dir, filename)) - if not path.startswith(self.parent.html_dir): - return HttpResponse(code=u'404 Not Found') + path = os.path.normpath(os.path.join(self.html_dir, filename)) + if not path.startswith(self.html_dir): + return self._http_not_found() ext = os.path.splitext(filename)[1] html = None if ext == u'.html': @@ -360,11 +402,12 @@ class HttpConnection(object): content = file_handle.read() except IOError: log.exception(u'Failed to open %s' % path) - return HttpResponse(code=u'404 Not Found') + return self._http_not_found() finally: if file_handle: file_handle.close() - return HttpResponse(content, {u'Content-Type': mimetype}) + cherrypy.response.headers['Content-Type'] = mimetype + return content def poll(self): """ @@ -379,18 +422,20 @@ class HttpConnection(object): u'theme': self.live_controller.theme_screen.isChecked(), u'display': self.live_controller.desktop_screen.isChecked() } - return HttpResponse(json.dumps({u'results': result}), {u'Content-Type': u'application/json'}) + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps({u'results': result}) def display(self, action): """ Hide or show the display screen. + This is a cross Thread call and UI is updated so Events need to be used. ``action`` This is the action, either ``hide`` or ``show``. """ - Registry().execute(u'slidecontroller_toggle_display', action) - return HttpResponse(json.dumps({u'results': {u'success': True}}), - {u'Content-Type': u'application/json'}) + self.live_controller.emit(QtCore.SIGNAL(u'slidecontroller_toggle_display'), action) + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps({u'results': {u'success': True}}) def alert(self): """ @@ -399,16 +444,16 @@ class HttpConnection(object): plugin = self.plugin_manager.get_plugin_by_name("alerts") if plugin.status == PluginStatus.Active: try: - text = json.loads(self.url_params[u'data'][0])[u'request'][u'text'] + text = json.loads(self.request_data)[u'request'][u'text'] except KeyError, ValueError: - return HttpResponse(code=u'400 Bad Request') + return self._http_bad_request() text = urllib.unquote(text) - Registry().execute(u'alerts_text', [text]) + self.alerts_manager.emit(QtCore.SIGNAL(u'alerts_text'), [text]) success = True else: success = False - return HttpResponse(json.dumps({u'results': {u'success': success}}), - {u'Content-Type': u'application/json'}) + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps({u'results': {u'success': success}}) def controller(self, display_type, action): """ @@ -444,44 +489,44 @@ class HttpConnection(object): if current_item: json_data[u'results'][u'item'] = self.live_controller.service_item.unique_identifier else: - if self.url_params and self.url_params.get(u'data'): + if self.request_data: try: - data = json.loads(self.url_params[u'data'][0]) + data = json.loads(self.request_data)[u'request'][u'id'] except KeyError, ValueError: - return HttpResponse(code=u'400 Bad Request') + return self._http_bad_request() log.info(data) # This slot expects an int within a list. - id = data[u'request'][u'id'] - Registry().execute(event, [id]) + self.live_controller.emit(QtCore.SIGNAL(event), [data]) else: - Registry().execute(event) + self.live_controller.emit(QtCore.SIGNAL(event)) json_data = {u'results': {u'success': True}} - return HttpResponse(json.dumps(json_data), {u'Content-Type': u'application/json'}) + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps(json_data) def service(self, action): """ - Handles requests for service items + Handles requests for service items in the service manager ``action`` The action to perform. """ event = u'servicemanager_%s' % action if action == u'list': - return HttpResponse(json.dumps({u'results': {u'items': self._get_service_items()}}), - {u'Content-Type': u'application/json'}) - else: - event += u'_item' - if self.url_params and self.url_params.get(u'data'): + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps({u'results': {u'items': self._get_service_items()}}) + event += u'_item' + if self.request_data: try: - data = json.loads(self.url_params[u'data'][0]) - except KeyError, ValueError: - return HttpResponse(code=u'400 Bad Request') - Registry().execute(event, data[u'request'][u'id']) + data = json.loads(self.request_data)[u'request'][u'id'] + except KeyError: + return self._http_bad_request() + self.service_manager.emit(QtCore.SIGNAL(event), data) else: Registry().execute(event) - return HttpResponse(json.dumps({u'results': {u'success': True}}), {u'Content-Type': u'application/json'}) + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps({u'results': {u'success': True}}) - def pluginInfo(self, action): + def plugin_info(self, action): """ Return plugin related information, based on the action. @@ -493,8 +538,9 @@ class HttpConnection(object): searches = [] for plugin in self.plugin_manager.plugins: if plugin.status == PluginStatus.Active and plugin.media_item and plugin.media_item.has_search: - searches.append([plugin.name, unicode(plugin.textStrings[StringContent.Name][u'plural'])]) - return HttpResponse(json.dumps({u'results': {u'items': searches}}), {u'Content-Type': u'application/json'}) + searches.append([plugin.name, unicode(plugin.text_strings[StringContent.Name][u'plural'])]) + cherrypy.response.headers['Content-Type'] = u'application/json' + return json.dumps({u'results': {u'items': searches}}) def search(self, plugin_name): """ @@ -504,69 +550,63 @@ class HttpConnection(object): 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') + 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 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 _http_success(self): + """ + Set the HTTP success return code. + """ + cherrypy.response.status = 200 - def disconnected(self): + def _http_bad_request(self): """ - The client has disconnected. Tidy up + Set the HTTP bad response return code. """ - log.debug(u'socket disconnected') - self.close() + cherrypy.response.status = 400 - def close(self): + def _http_not_found(self): """ - The server has closed the connection. Tidy up + Set the HTTP not found return code. """ - if not self.socket: - return - log.debug(u'close socket') - self.socket.close() - self.socket = None - self.parent.close_connection(self) + cherrypy.response.status = 404 + cherrypy.response.body = ["Sorry, an error occurred "] def _get_service_manager(self): """ @@ -597,3 +637,13 @@ class HttpConnection(object): 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) diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py index 483b3461b..09934b58c 100644 --- a/openlp/plugins/remotes/lib/remotetab.py +++ b/openlp/plugins/remotes/lib/remotetab.py @@ -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 @@ class RemoteTab(SettingsTab): 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 @@ class RemoteTab(SettingsTab): self.qr_layout.addWidget(self.qr_description_label) self.left_layout.addStretch() self.right_layout.addStretch() - self.twelve_hour_check_box.stateChanged.connect(self.onTwelveHourCheckBoxChanged) + self.twelve_hour_check_box.stateChanged.connect(self.on_twelve_hour_check_box_changed) self.address_edit.textChanged.connect(self.set_urls) self.port_spin_box.valueChanged.connect(self.set_urls) + self.https_port_spin_box.valueChanged.connect(self.set_urls) + self.https_settings_group_box.clicked.connect(self.https_changed) def retranslateUi(self): self.server_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Server Settings')) @@ -112,8 +169,21 @@ class RemoteTab(SettingsTab): 'Scan the QR code or click download to install the ' 'Android app from Google Play.')) + self.https_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'HTTPS Server')) + self.https_error_label.setText(translate('RemotePlugin.RemoteTab', + 'Could not find an SSL certificate. The HTTPS server will not be available unless an SSL certificate ' + 'is found. Please see the manual for more information.')) + self.https_port_label.setText(self.port_label.text()) + self.remote_https_url_label.setText(self.remote_url_label.text()) + self.stage_https_url_label.setText(self.stage_url_label.text()) + self.user_login_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'User Authentication')) + self.user_id_label.setText(translate('RemotePlugin.RemoteTab', 'User id:')) + self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:')) def set_urls(self): + """ + Update the display based on the data input on the screen + """ ip_address = u'localhost' if self.address_edit.text() == ZERO_URL: interfaces = QtNetwork.QNetworkInterface.allInterfaces() @@ -129,31 +199,73 @@ class RemoteTab(SettingsTab): 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'%s' % (url, url)) - url += u'stage' - self.stage_url.setText(u'%s' % (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'%s' % (http_url, http_url)) + self.remote_https_url.setText(u'%s' % (https_url, https_url)) + http_url += u'stage' + https_url += u'stage' + self.stage_url.setText(u'%s' % (http_url, http_url)) + self.stage_https_url.setText(u'%s' % (https_url, https_url)) def load(self): + """ + Load the configuration and update the server configuration if necessary + """ self.port_spin_box.setValue(Settings().value(self.settings_section + u'/port')) + self.https_port_spin_box.setValue(Settings().value(self.settings_section + u'/https port')) self.address_edit.setText(Settings().value(self.settings_section + u'/ip address')) self.twelve_hour = Settings().value(self.settings_section + u'/twelve hour') self.twelve_hour_check_box.setChecked(self.twelve_hour) + local_data = AppLocation.get_directory(AppLocation.DataDir) + if not os.path.exists(os.path.join(local_data, u'remotes', u'openlp.crt')) or \ + not os.path.exists(os.path.join(local_data, u'remotes', u'openlp.key')): + self.https_settings_group_box.setChecked(False) + self.https_settings_group_box.setEnabled(False) + self.https_error_label.setVisible(True) + else: + self.https_settings_group_box.setChecked(Settings().value(self.settings_section + u'/https enabled')) + self.https_settings_group_box.setEnabled(True) + self.https_error_label.setVisible(False) + self.user_login_group_box.setChecked(Settings().value(self.settings_section + u'/authentication enabled')) + self.user_id.setText(Settings().value(self.settings_section + u'/user id')) + self.password.setText(Settings().value(self.settings_section + u'/password')) self.set_urls() + self.https_changed() def save(self): - changed = False + """ + Save the configuration and update the server configuration if necessary + """ if Settings().value(self.settings_section + u'/ip address') != self.address_edit.text() or \ - Settings().value(self.settings_section + u'/port') != self.port_spin_box.value(): - changed = True + Settings().value(self.settings_section + u'/port') != self.port_spin_box.value() or \ + Settings().value(self.settings_section + u'/https port') != self.https_port_spin_box.value() or \ + Settings().value(self.settings_section + u'/https enabled') != \ + self.https_settings_group_box.isChecked() or \ + Settings().value(self.settings_section + u'/authentication enabled') != \ + self.user_login_group_box.isChecked(): + self.settings_form.register_post_process(u'remotes_config_updated') Settings().setValue(self.settings_section + u'/port', self.port_spin_box.value()) + Settings().setValue(self.settings_section + u'/https port', self.https_port_spin_box.value()) + Settings().setValue(self.settings_section + u'/https enabled', self.https_settings_group_box.isChecked()) Settings().setValue(self.settings_section + u'/ip address', self.address_edit.text()) Settings().setValue(self.settings_section + u'/twelve hour', self.twelve_hour) - if changed: - Registry().execute(u'remotes_config_updated') + Settings().setValue(self.settings_section + u'/authentication enabled', self.user_login_group_box.isChecked()) + Settings().setValue(self.settings_section + u'/user id', self.user_id.text()) + Settings().setValue(self.settings_section + u'/password', self.password.text()) - def onTwelveHourCheckBoxChanged(self, check_state): + def on_twelve_hour_check_box_changed(self, check_state): + """ + Toggle the 12 hour check box. + """ self.twelve_hour = False # we have a set value convert to True/False if check_state == QtCore.Qt.Checked: self.twelve_hour = True + + def https_changed(self): + """ + Invert the HTTP group box based on Https group settings + """ + self.http_settings_group_box.setEnabled(not self.https_settings_group_box.isChecked()) + diff --git a/openlp/plugins/remotes/remoteplugin.py b/openlp/plugins/remotes/remoteplugin.py index e990101e3..f443fbda4 100644 --- a/openlp/plugins/remotes/remoteplugin.py +++ b/openlp/plugins/remotes/remoteplugin.py @@ -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 @@ log = logging.getLogger(__name__) __default_settings__ = { u'remotes/twelve hour': True, u'remotes/port': 4316, + u'remotes/https port': 4317, + u'remotes/https enabled': False, + u'remotes/user id': u'openlp', + u'remotes/password': u'password', + u'remotes/authentication enabled': False, u'remotes/ip address': u'0.0.0.0' } @@ -60,7 +67,8 @@ class RemotesPlugin(Plugin): """ log.debug(u'initialise') Plugin.initialise(self) - self.server = HttpServer(self) + self.server = HttpServer() + self.server.start_server() def finalise(self): """ @@ -70,6 +78,7 @@ class RemotesPlugin(Plugin): Plugin.finalise(self) if self.server: self.server.close() + self.server = None def about(self): """ @@ -99,5 +108,6 @@ class RemotesPlugin(Plugin): """ 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.')) diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py index a6e075db4..faa02fada 100755 --- a/scripts/check_dependencies.py +++ b/scripts/check_dependencies.py @@ -81,6 +81,7 @@ MODULES = [ 'enchant', 'BeautifulSoup', 'mako', + 'cherrypy', 'migrate', 'uno', 'icu', diff --git a/tests/functional/openlp_core_lib/test_pluginmanager.py b/tests/functional/openlp_core_lib/test_pluginmanager.py index 9d6c30f8e..8317e78dc 100644 --- a/tests/functional/openlp_core_lib/test_pluginmanager.py +++ b/tests/functional/openlp_core_lib/test_pluginmanager.py @@ -74,7 +74,7 @@ class TestPluginManager(TestCase): # WHEN: We run hook_settings_tabs() plugin_manager.hook_settings_tabs() - # THEN: The create_settings_Tab() method should have been called + # THEN: The hook_settings_tabs() method should have been called assert mocked_plugin.create_media_manager_item.call_count == 0, \ u'The create_media_manager_item() method should not have been called.' @@ -94,8 +94,8 @@ class TestPluginManager(TestCase): # WHEN: We run hook_settings_tabs() plugin_manager.hook_settings_tabs() - # THEN: The create_settings_Tab() method should not have been called, but the plugins lists should be the same - assert mocked_plugin.create_settings_Tab.call_count == 0, \ + # THEN: The create_settings_tab() method should not have been called, but the plugins lists should be the same + assert mocked_plugin.create_settings_tab.call_count == 0, \ u'The create_media_manager_item() method should not have been called.' self.assertEqual(mocked_settings_form.plugin_manager.plugins, plugin_manager.plugins, u'The plugins on the settings form should be the same as the plugins in the plugin manager') @@ -117,7 +117,7 @@ class TestPluginManager(TestCase): plugin_manager.hook_settings_tabs() # THEN: The create_media_manager_item() method should have been called with the mocked settings form - assert mocked_plugin.create_settings_Tab.call_count == 1, \ + assert mocked_plugin.create_settings_tab.call_count == 1, \ u'The create_media_manager_item() method should have been called once.' self.assertEqual(mocked_settings_form.plugin_manager.plugins, plugin_manager.plugins, u'The plugins on the settings form should be the same as the plugins in the plugin manager') @@ -135,8 +135,8 @@ class TestPluginManager(TestCase): # WHEN: We run hook_settings_tabs() plugin_manager.hook_settings_tabs() - # THEN: The create_settings_Tab() method should have been called - mocked_plugin.create_settings_Tab.assert_called_with(self.mocked_settings_form) + # THEN: The create_settings_tab() method should have been called + mocked_plugin.create_settings_tab.assert_called_with(self.mocked_settings_form) def hook_import_menu_with_disabled_plugin_test(self): """ diff --git a/tests/functional/openlp_core_lib/test_settings.py b/tests/functional/openlp_core_lib/test_settings.py index 827bfa156..b06bb4eac 100644 --- a/tests/functional/openlp_core_lib/test_settings.py +++ b/tests/functional/openlp_core_lib/test_settings.py @@ -11,7 +11,9 @@ from PyQt4 import QtGui class TestSettings(TestCase): - + """ + Test the functions in the Settings module + """ def setUp(self): """ Create the UI diff --git a/tests/functional/openlp_core_lib/test_uistrings.py b/tests/functional/openlp_core_lib/test_uistrings.py index 3351657d1..0070533db 100644 --- a/tests/functional/openlp_core_lib/test_uistrings.py +++ b/tests/functional/openlp_core_lib/test_uistrings.py @@ -6,6 +6,7 @@ from unittest import TestCase from openlp.core.lib import UiStrings + class TestUiStrings(TestCase): def check_same_instance_test(self): diff --git a/tests/functional/openlp_core_utils/test_applocation.py b/tests/functional/openlp_core_utils/test_applocation.py index 5473da8c0..b59f41f37 100644 --- a/tests/functional/openlp_core_utils/test_applocation.py +++ b/tests/functional/openlp_core_utils/test_applocation.py @@ -30,8 +30,10 @@ class TestAppLocation(TestCase): mocked_get_directory.return_value = u'test/dir' mocked_check_directory_exists.return_value = True mocked_os.path.normpath.return_value = u'test/dir' + # WHEN: we call AppLocation.get_data_path() data_path = AppLocation.get_data_path() + # THEN: check that all the correct methods were called, and the result is correct mocked_settings.contains.assert_called_with(u'advanced/data path') mocked_get_directory.assert_called_with(AppLocation.DataDir) @@ -49,8 +51,10 @@ class TestAppLocation(TestCase): mocked_settings.contains.return_value = True mocked_settings.value.return_value.toString.return_value = u'custom/dir' mocked_os.path.normpath.return_value = u'custom/dir' + # WHEN: we call AppLocation.get_data_path() data_path = AppLocation.get_data_path() + # THEN: the mocked Settings methods were called and the value returned was our set up value mocked_settings.contains.assert_called_with(u'advanced/data path') mocked_settings.value.assert_called_with(u'advanced/data path') @@ -100,8 +104,10 @@ class TestAppLocation(TestCase): # GIVEN: A mocked out AppLocation.get_data_path() mocked_get_data_path.return_value = u'test/dir' mocked_check_directory_exists.return_value = True + # WHEN: we call AppLocation.get_data_path() data_path = AppLocation.get_section_data_path(u'section') + # THEN: check that all the correct methods were called, and the result is correct mocked_check_directory_exists.assert_called_with(u'test/dir/section') assert data_path == u'test/dir/section', u'Result should be "test/dir/section"' @@ -112,8 +118,10 @@ class TestAppLocation(TestCase): """ with patch(u'openlp.core.utils.applocation._get_frozen_path') as mocked_get_frozen_path: mocked_get_frozen_path.return_value = u'app/dir' + # WHEN: We call AppLocation.get_directory directory = AppLocation.get_directory(AppLocation.AppDir) + # THEN: assert directory == u'app/dir', u'Directory should be "app/dir"' @@ -130,8 +138,10 @@ class TestAppLocation(TestCase): mocked_get_frozen_path.return_value = u'plugins/dir' mocked_sys.frozen = 1 mocked_sys.argv = ['openlp'] + # WHEN: We call AppLocation.get_directory directory = AppLocation.get_directory(AppLocation.PluginsDir) + # THEN: assert directory == u'plugins/dir', u'Directory should be "plugins/dir"' diff --git a/tests/functional/openlp_plugins/remotes/__init__.py b/tests/functional/openlp_plugins/remotes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/functional/openlp_plugins/remotes/test_remotetab.py b/tests/functional/openlp_plugins/remotes/test_remotetab.py new file mode 100644 index 000000000..22bee8139 --- /dev/null +++ b/tests/functional/openlp_plugins/remotes/test_remotetab.py @@ -0,0 +1,108 @@ +""" +This module contains tests for the lib submodule of the Remotes plugin. +""" +import os + +from unittest import TestCase +from tempfile import mkstemp +from mock import patch + +from openlp.core.lib import Settings +from openlp.plugins.remotes.lib.remotetab import RemoteTab + +from PyQt4 import QtGui + +__default_settings__ = { + u'remotes/twelve hour': True, + u'remotes/port': 4316, + u'remotes/https port': 4317, + u'remotes/https enabled': False, + u'remotes/user id': u'openlp', + u'remotes/password': u'password', + u'remotes/authentication enabled': False, + u'remotes/ip address': u'0.0.0.0' +} + +ZERO_URL = u'0.0.0.0' + +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..', u'..', u'resources')) + + +class TestRemoteTab(TestCase): + """ + Test the functions in the :mod:`lib` module. + """ + def setUp(self): + """ + Create the UI + """ + fd, self.ini_file = mkstemp(u'.ini') + Settings().set_filename(self.ini_file) + self.application = QtGui.QApplication.instance() + Settings().extend_default_settings(__default_settings__) + self.parent = QtGui.QMainWindow() + self.form = RemoteTab(self.parent, u'Remotes', None, None) + + def tearDown(self): + """ + Delete all the C++ objects at the end so that we don't have a segfault + """ + del self.application + del self.parent + del self.form + os.unlink(self.ini_file) + + def set_basic_urls_test(self): + """ + Test the set_urls function with standard defaults + """ + # GIVEN: A mocked location + with patch(u'openlp.core.utils.applocation.Settings') as mocked_class, \ + patch(u'openlp.core.utils.AppLocation.get_directory') as mocked_get_directory, \ + patch(u'openlp.core.utils.applocation.check_directory_exists') as mocked_check_directory_exists, \ + patch(u'openlp.core.utils.applocation.os') as mocked_os: + # GIVEN: A mocked out Settings class and a mocked out AppLocation.get_directory() + mocked_settings = mocked_class.return_value + mocked_settings.contains.return_value = False + mocked_get_directory.return_value = u'test/dir' + mocked_check_directory_exists.return_value = True + mocked_os.path.normpath.return_value = u'test/dir' + + # WHEN: when the set_urls is called having reloaded the form. + self.form.load() + self.form.set_urls() + # THEN: the following screen values should be set + self.assertEqual(self.form.address_edit.text(), ZERO_URL, u'The default URL should be set on the screen') + self.assertEqual(self.form.https_settings_group_box.isEnabled(), False, + u'The Https box should not be enabled') + self.assertEqual(self.form.https_settings_group_box.isChecked(), False, + u'The Https checked box should note be Checked') + self.assertEqual(self.form.user_login_group_box.isChecked(), False, + u'The authentication box should not be enabled') + + def set_certificate_urls_test(self): + """ + Test the set_urls function with certificate available + """ + # GIVEN: A mocked location + with patch(u'openlp.core.utils.applocation.Settings') as mocked_class, \ + patch(u'openlp.core.utils.AppLocation.get_directory') as mocked_get_directory, \ + patch(u'openlp.core.utils.applocation.check_directory_exists') as mocked_check_directory_exists, \ + patch(u'openlp.core.utils.applocation.os') as mocked_os: + # GIVEN: A mocked out Settings class and a mocked out AppLocation.get_directory() + mocked_settings = mocked_class.return_value + mocked_settings.contains.return_value = False + mocked_get_directory.return_value = TEST_PATH + mocked_check_directory_exists.return_value = True + mocked_os.path.normpath.return_value = TEST_PATH + + # WHEN: when the set_urls is called having reloaded the form. + self.form.load() + self.form.set_urls() + # THEN: the following screen values should be set + self.assertEqual(self.form.http_settings_group_box.isEnabled(), True, + u'The Http group box should be enabled') + self.assertEqual(self.form.https_settings_group_box.isChecked(), False, + u'The Https checked box should be Checked') + self.assertEqual(self.form.https_settings_group_box.isEnabled(), True, + u'The Https box should be enabled') diff --git a/tests/functional/openlp_plugins/remotes/test_router.py b/tests/functional/openlp_plugins/remotes/test_router.py new file mode 100644 index 000000000..2980a339b --- /dev/null +++ b/tests/functional/openlp_plugins/remotes/test_router.py @@ -0,0 +1,99 @@ +""" +This module contains tests for the lib submodule of the Remotes plugin. +""" +import os + +from unittest import TestCase +from tempfile import mkstemp +from mock import MagicMock + +from openlp.core.lib import Settings +from openlp.plugins.remotes.lib.httpserver import HttpRouter, fetch_password, make_sha_hash +from PyQt4 import QtGui + +__default_settings__ = { + u'remotes/twelve hour': True, + u'remotes/port': 4316, + u'remotes/https port': 4317, + u'remotes/https enabled': False, + u'remotes/user id': u'openlp', + u'remotes/password': u'password', + u'remotes/authentication enabled': False, + u'remotes/ip address': u'0.0.0.0' +} + + +class TestRouter(TestCase): + """ + Test the functions in the :mod:`lib` module. + """ + def setUp(self): + """ + Create the UI + """ + fd, self.ini_file = mkstemp(u'.ini') + Settings().set_filename(self.ini_file) + self.application = QtGui.QApplication.instance() + Settings().extend_default_settings(__default_settings__) + self.router = HttpRouter() + + def tearDown(self): + """ + Delete all the C++ objects at the end so that we don't have a segfault + """ + del self.application + os.unlink(self.ini_file) + + def fetch_password_unknown_test(self): + """ + Test the fetch password code with an unknown userid + """ + # GIVEN: A default configuration + # WHEN: called with the defined userid + password = fetch_password(u'itwinkle') + + # THEN: the function should return None + self.assertEqual(password, None, u'The result for fetch_password should be None') + + def fetch_password_known_test(self): + """ + Test the fetch password code with the defined userid + """ + # GIVEN: A default configuration + # WHEN: called with the defined userid + password = fetch_password(u'openlp') + required_password = make_sha_hash(u'password') + + # THEN: the function should return the correct password + self.assertEqual(password, required_password, u'The result for fetch_password should be the defined password') + + def sha_password_encrypter_test(self): + """ + Test hash password function + """ + # GIVEN: A default configuration + # WHEN: called with the defined userid + required_password = make_sha_hash(u'password') + test_value = u'5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8' + + # THEN: the function should return the correct password + self.assertEqual(required_password, test_value, + u'The result for make_sha_hash should return the correct encrypted password') + + def process_http_request_test(self): + """ + Test the router control functionality + """ + # GIVEN: A testing set of Routes + mocked_function = MagicMock() + test_route = [ + (r'^/stage/api/poll$', mocked_function), + ] + self.router.routes = test_route + + # WHEN: called with a poll route + self.router.process_http_request(u'/stage/api/poll', None) + + # THEN: the function should have been called only once + assert mocked_function.call_count == 1, \ + u'The mocked function should have been matched and called once.' diff --git a/tests/interfaces/openlp_plugins/remotes/__init__.py b/tests/interfaces/openlp_plugins/remotes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/interfaces/openlp_plugins/remotes/test_server.py b/tests/interfaces/openlp_plugins/remotes/test_server.py new file mode 100644 index 000000000..8795eeaf3 --- /dev/null +++ b/tests/interfaces/openlp_plugins/remotes/test_server.py @@ -0,0 +1,138 @@ +""" +This module contains tests for the lib submodule of the Remotes plugin. +""" +import os + +from unittest import TestCase +from tempfile import mkstemp +from mock import MagicMock +import urllib2 +import cherrypy + +from BeautifulSoup import BeautifulSoup + +from openlp.core.lib import Settings +from openlp.plugins.remotes.lib.httpserver import HttpServer +from PyQt4 import QtGui + +__default_settings__ = { + u'remotes/twelve hour': True, + u'remotes/port': 4316, + u'remotes/https port': 4317, + u'remotes/https enabled': False, + u'remotes/user id': u'openlp', + u'remotes/password': u'password', + u'remotes/authentication enabled': False, + u'remotes/ip address': u'0.0.0.0' +} + + +class TestRouter(TestCase): + """ + Test the functions in the :mod:`lib` module. + """ + def setUp(self): + """ + Create the UI + """ + fd, self.ini_file = mkstemp(u'.ini') + Settings().set_filename(self.ini_file) + self.application = QtGui.QApplication.instance() + Settings().extend_default_settings(__default_settings__) + self.server = HttpServer() + + def tearDown(self): + """ + Delete all the C++ objects at the end so that we don't have a segfault + """ + del self.application + os.unlink(self.ini_file) + self.server.close() + + def start_server(self): + """ + Common function to start server then mock out the router. CherryPy crashes if you mock before you start + """ + self.server.start_server() + self.server.router = MagicMock() + self.server.router.process_http_request = process_http_request + + def start_default_server_test(self): + """ + Test the default server serves the correct initial page + """ + # GIVEN: A default configuration + Settings().setValue(u'remotes/authentication enabled', False) + self.start_server() + + # WHEN: called the route location + code, page = call_remote_server(u'http://localhost:4316') + + # THEN: default title will be returned + self.assertEqual(BeautifulSoup(page).title.text, u'OpenLP 2.1 Remote', + u'The default menu should be returned') + + def start_authenticating_server_test(self): + """ + Test the default server serves the correctly with authentication + """ + # GIVEN: A default authorised configuration + Settings().setValue(u'remotes/authentication enabled', True) + self.start_server() + + # WHEN: called the route location with no user details + code, page = call_remote_server(u'http://localhost:4316') + + # THEN: then server will ask for details + self.assertEqual(code, 401, u'The basic authorisation request should be returned') + + # WHEN: called the route location with user details + code, page = call_remote_server(u'http://localhost:4316', u'openlp', u'password') + + # THEN: default title will be returned + self.assertEqual(BeautifulSoup(page).title.text, u'OpenLP 2.1 Remote', + u'The default menu should be returned') + + # WHEN: called the route location with incorrect user details + code, page = call_remote_server(u'http://localhost:4316', u'itwinkle', u'password') + + # THEN: then server will ask for details + self.assertEqual(code, 401, u'The basic authorisation request should be returned') + + +def call_remote_server(url, username=None, password=None): + """ + Helper function + + ``username`` + The username. + + ``password`` + The password. + """ + if username: + passman = urllib2.HTTPPasswordMgrWithDefaultRealm() + passman.add_password(None, url, username, password) + authhandler = urllib2.HTTPBasicAuthHandler(passman) + opener = urllib2.build_opener(authhandler) + urllib2.install_opener(opener) + try: + page = urllib2.urlopen(url) + return 0, page.read() + except urllib2.HTTPError, e: + return e.code, u'' + + +def process_http_request(url_path, *args): + """ + Override function to make the Mock work but does nothing. + + ``Url_path`` + The url_path. + + ``*args`` + Some args. + """ + cherrypy.response.status = 200 + return None + diff --git a/tests/resources/remotes/openlp.crt b/tests/resources/remotes/openlp.crt new file mode 100644 index 000000000..e69de29bb diff --git a/tests/resources/remotes/openlp.key b/tests/resources/remotes/openlp.key new file mode 100644 index 000000000..e69de29bb