Initial Cherrypy implementation

This commit is contained in:
Tim Bentley 2013-03-06 21:53:29 +00:00
parent 44ebc1fe27
commit f1aadde13c
9 changed files with 534 additions and 242 deletions

View File

@ -69,6 +69,11 @@ try:
MAKO_VERSION = mako.__version__
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')
@ -138,6 +143,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'pyUNO bridge: %s\n' % UNO_VERSION
if platform.system() == u'Linux':
if os.environ.get(u'KDE_FULL_SESSION') == u'true':

View File

@ -362,8 +362,9 @@ class SlideController(DisplayController):
# Signals
QtCore.QObject.connect(self.previewListWidget, QtCore.SIGNAL(u'clicked(QModelIndex)'), self.onSlideSelected)
if self.isLive:
# 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.setWidgetVisible(self.loopList, False)
self.toolbar.setWidgetVisible(self.wideMenu, False)
else:
@ -867,9 +868,9 @@ class SlideController(DisplayController):
"""
Go to the requested slide
"""
index = int(message[0])
if not self.serviceItem:
if not self.serviceItem or not message[0]:
return
index = int(message[0])
if self.serviceItem.is_command():
Registry().execute(u'%s_slide' % self.serviceItem.name.lower(), [self.serviceItem, self.isLive, index])
self.updatePreview()

View File

@ -90,6 +90,7 @@ class AppLocation(object):
VersionDir = 5
CacheDir = 6
LanguageDir = 7
SharedData = 8
# Base path where data/config/cache dir is located
BaseDir = None
@ -150,18 +151,18 @@ def _get_os_dir_path(dir_type):
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')

View File

@ -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 ||

View File

@ -0,0 +1,192 @@
# -*- 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 urlparse
SESSION_KEY = '_cp_openlp'
def check_credentials(user_name, password):
"""
Verifies credentials for username and password.
Returns None on success or a string describing the error on failure
"""
# @todo make from config
if user_name == 'openlp' and password == 'openlp':
return None
else:
return u"Incorrect username or password."
# if u.password != md5.new(password).hexdigest():
# return u"Incorrect password"
def check_auth(*args, **kwargs):
"""
A tool that looks in config for 'auth.require'. If found and it
is not None, a login is required and the entry is evaluated as a list of
conditions that the user must fulfill
"""
print "check"
conditions = cherrypy.request.config.get('auth.require', None)
print conditions
print args, kwargs
print urlparse.urlparse(cherrypy.url())
url = urlparse.urlparse(cherrypy.url())
print urlparse.parse_qs(url.query)
if conditions is not None:
username = cherrypy.session.get(SESSION_KEY)
if username:
cherrypy.request.login = username
for condition in conditions:
# A condition is just a callable that returns true or false
if not condition():
raise cherrypy.HTTPRedirect("/auth/login")
else:
raise cherrypy.HTTPRedirect("/auth/login")
cherrypy.tools.auth = cherrypy.Tool('before_handler', check_auth)
def require_auth(*conditions):
"""
A decorator that appends conditions to the auth.require config variable.
"""
print conditions
def decorate(f):
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)
print "a ", [f]
return f
return decorate
# Conditions are callables that return True
# if the user fulfills the conditions they define, False otherwise
#
# They can access the current username as cherrypy.request.login
#
# Define those at will however suits the application.
#def member_of(groupname):
# def check():
# # replace with actual check if <username> is in <groupname>
# return cherrypy.request.login == 'joe' and groupname == 'admin'
# return check
#def name_is(reqd_username):
# return lambda: reqd_username == cherrypy.request.login
#def any_of(*conditions):
# """
# Returns True if any of the conditions match
# """
# def check():
# for c in conditions:
# if c():
# return True
# return False
# return check
# By default all conditions are required, but this might still be
# needed if you want to use it inside of an any_of(...) condition
#def all_of(*conditions):
# """
# Returns True if all of the conditions match
# """
# def check():
# for c in conditions:
# if not c():
# return False
# return True
# return check
# Controller to provide login and logout actions
class AuthController(object):
def on_login(self, username):
"""
Called on successful login
"""
def on_logout(self, username):
"""
Called on logout
"""
def get_loginform(self, username, msg="Enter login information", from_page="/"):
"""
Provides a login form
"""
return """<html><body>
<form method="post" action="/auth/login">
<input type="hidden" name="from_page" value="%(from_page)s" />
%(msg)s<br />
Username: <input type="text" name="username" value="%(username)s" /><br />
Password: <input type="password" name="password" /><br />
<input type="submit" value="Log in" />
</body></html>""" % locals()
@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_loginform("", from_page=from_page)
error_msg = check_credentials(username, password)
if error_msg:
return self.get_loginform(username, error_msg, from_page)
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="/"):
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 "/")

View File

@ -119,40 +119,23 @@ 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 openlp.plugins.remotes.lib.httpauth import AuthController, require_auth
log = logging.getLogger(__name__)
class HttpResponse(object):
"""
A simple object to encapsulate a pseudo-http response.
"""
code = '200 OK'
content = ''
headers = {
'Content-Type': 'text/html; charset="utf-8"\r\n'
}
def __init__(self, content='', headers=None, code=None):
if headers is None:
headers = {}
self.content = content
for key, value in headers.iteritems():
self.headers[key] = value
if code:
self.code = code
class HttpServer(object):
"""
Ability to control OpenLP via a web browser.
"""
def __init__(self, plugin):
"""
Initialise the httpserver, and start the server.
@ -163,22 +146,28 @@ class HttpServer(object):
self.connections = []
self.current_item = None
self.current_slide = None
self.start_tcp()
self.conf = {'/files': {u'tools.staticdir.on': True,
u'tools.staticdir.dir': self.html_dir}}
self.start_server()
def start_tcp(self):
def start_server(self):
"""
Start the http server, use the port in the settings default to 4316.
Listen out for slide and song changes so they can be broadcast to
clients. Listen out for socket connections.
"""
log.debug(u'Start TCP server')
log.debug(u'Start CherryPy server')
port = Settings().value(self.plugin.settingsSection + u'/port')
address = Settings().value(self.plugin.settingsSection + u'/ip address')
self.server = QtNetwork.QTcpServer()
self.server.listen(QtNetwork.QHostAddress(address), port)
server_config = {u'server.socket_host': str(address),
u'server.socket_port': port}
cherrypy.config.update(server_config)
cherrypy.config.update({'environment': 'embedded'})
cherrypy.config.update({'engine.autoreload_on': False})
cherrypy.tree.mount(HttpConnection(self), '/', config=self.conf)
cherrypy.engine.start()
Registry().register_function(u'slidecontroller_live_changed', self.slide_change)
Registry().register_function(u'slidecontroller_live_started', self.item_change)
QtCore.QObject.connect(self.server, QtCore.SIGNAL(u'newConnection()'), self.new_connection)
log.debug(u'TCP listening on port %d' % port)
def slide_change(self, row):
@ -193,50 +182,41 @@ class HttpServer(object):
"""
self.current_item = items[0]
def new_connection(self):
"""
A new http connection has been made. Create a client object to handle
communication.
"""
log.debug(u'new http connection')
socket = self.server.nextPendingConnection()
if socket:
self.connections.append(HttpConnection(self, socket))
def close_connection(self, connection):
"""
The connection has been closed. Clean up
"""
log.debug(u'close http connection')
if connection in self.connections:
self.connections.remove(connection)
def close(self):
"""
Close down the http server.
"""
log.debug(u'close http server')
self.server.close()
cherrypy.engine.exit()
cherrypy.engine.stop()
class HttpConnection(object):
"""
A single connection, this handles communication between the server
and the client.
A single connection, this handles communication between the server and the client.
"""
def __init__(self, parent, socket):
_cp_config = {
'tools.sessions.on': True,
'tools.auth.on': True
}
auth = AuthController()
def __init__(self, parent):
"""
Initialise the http connection. Listen out for socket signals.
"""
log.debug(u'Initialise HttpConnection: %s' % socket.peerAddress())
self.socket = socket
#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/(.*)$', self.controller),
(r'^/api/service/(.*)$', self.service),
(r'^/api/display/(hide|show|blank|theme|desktop)$', self.display),
(r'^/api/alert$', self.alert),
@ -245,17 +225,79 @@ class HttpConnection(object):
(r'^/api/(.*)/live$', self.go_live),
(r'^/api/(.*)/add$', self.add_to_service)
]
QtCore.QObject.connect(self.socket, QtCore.SIGNAL(u'readyRead()'), self.ready_read)
QtCore.QObject.connect(self.socket, QtCore.SIGNAL(u'disconnected()'), self.disconnected)
self.translate()
@cherrypy.expose
#@require_auth(auth)
def default(self, *args, **kwargs):
"""
Handles the requests for the main url. This is secure depending on settings.
"""
# Loop through the routes we set up earlier and execute them
return self._process_http_request(args, kwargs)
@cherrypy.expose
def stage(self, *args, **kwargs):
"""
Handles the requests for the stage url. This is not secure.
"""
print "Stage"
url = urlparse.urlparse(cherrypy.url())
self.url_params = urlparse.parse_qs(url.query)
print url
print [self.url_params]
#return self.serve_file(u'stage')
return self._process_http_request(args, kwargs)
@cherrypy.expose
def files(self, *args, **kwargs):
"""
Handles the requests for the stage url. This is not secure.
"""
print "files"
url = urlparse.urlparse(cherrypy.url())
self.url_params = urlparse.parse_qs(url.query)
print url
print [self.url_params]
print args
#return self.serve_file(args)
return self._process_http_request(args, kwargs)
def _process_http_request(self, args, kwargs):
"""
Common function to process HTTP requests where secure or insecure
"""
print "common handler"
url = urlparse.urlparse(cherrypy.url())
self.url_params = urlparse.parse_qs(url.query)
print url
print [self.url_params]
response = None
for route, func in self.routes:
match = re.match(route, url.path)
if match:
print 'Route "%s" matched "%s"', route, url.path
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.parent.current_item:
current_unique_identifier = self.parent.current_item.unique_identifier
else:
current_unique_identifier = None
for item in self.service_manager.serviceItems:
for item in self.service_manager.service_items:
service_item = item[u'service_item']
service_items.append({
u'id': unicode(service_item.unique_identifier),
@ -296,40 +338,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
@ -339,6 +347,7 @@ class HttpConnection(object):
Ultimately for i18n, this could first look for xx/file.html before
falling back to file.html... where xx is the language, e.g. 'en'
"""
print "serve_file", filename
log.debug(u'serve file request %s' % filename)
if not filename:
filename = u'index.html'
@ -346,7 +355,7 @@ class HttpConnection(object):
filename = u'stage.html'
path = os.path.normpath(os.path.join(self.parent.html_dir, filename))
if not path.startswith(self.parent.html_dir):
return HttpResponse(code=u'404 Not Found')
return self._http_not_found()
ext = os.path.splitext(filename)[1]
html = None
if ext == u'.html':
@ -375,11 +384,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):
"""
@ -389,24 +399,25 @@ class HttpConnection(object):
u'service': self.service_manager.service_id,
u'slide': self.parent.current_slide or 0,
u'item': self.parent.current_item.unique_identifier if self.parent.current_item else u'',
u'twelve':Settings().value(u'remotes/twelve hour'),
u'twelve': Settings().value(u'remotes/twelve hour'),
u'blank': self.live_controller.blankScreen.isChecked(),
u'theme': self.live_controller.themeScreen.isChecked(),
u'display': self.live_controller.desktopScreen.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):
"""
@ -417,14 +428,14 @@ class HttpConnection(object):
try:
text = json.loads(self.url_params[u'data'][0])[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])
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, type, action):
"""
@ -465,34 +476,37 @@ class HttpConnection(object):
try:
data = json.loads(self.url_params[u'data'][0])
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])
else:
Registry().execute(event)
Registry().execute(event, [0])
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):
"""
List details of the Service and update the UI
"""
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'})
cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': {u'items': self._get_service_items()}})
else:
event += u'_item'
if self.url_params and self.url_params.get(u'data'):
try:
data = json.loads(self.url_params[u'data'][0])
except KeyError, ValueError:
return HttpResponse(code=u'400 Bad Request')
return self._http_bad_request()
Registry().execute(event, data[u'request'][u'id'])
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):
"""
@ -507,9 +521,8 @@ class HttpConnection(object):
for plugin in self.plugin_manager.plugins:
if plugin.status == PluginStatus.Active and plugin.mediaItem and plugin.mediaItem.hasSearch:
searches.append([plugin.name, unicode(plugin.textStrings[StringContent.Name][u'plural'])])
return HttpResponse(
json.dumps({u'results': {u'items': searches}}),
{u'Content-Type': u'application/json'})
cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': {u'items': searches}})
def search(self, type):
"""
@ -521,15 +534,15 @@ class HttpConnection(object):
try:
text = json.loads(self.url_params[u'data'][0])[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(type)
if plugin.status == PluginStatus.Active and plugin.mediaItem and plugin.mediaItem.hasSearch:
results = plugin.mediaItem.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, type):
"""
@ -538,11 +551,11 @@ class HttpConnection(object):
try:
id = json.loads(self.url_params[u'data'][0])[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(type)
if plugin.status == PluginStatus.Active and plugin.mediaItem:
plugin.mediaItem.goLive(id, remote=True)
return HttpResponse(code=u'200 OK')
return self._http_success()
def add_to_service(self, type):
"""
@ -551,38 +564,22 @@ class HttpConnection(object):
try:
id = json.loads(self.url_params[u'data'][0])[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(type)
if plugin.status == PluginStatus.Active and plugin.mediaItem:
item_id = plugin.mediaItem.createItemFromId(id)
plugin.mediaItem.addToService(item_id, remote=True)
return HttpResponse(code=u'200 OK')
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):
cherrypy.response.status = 200
def disconnected(self):
"""
The client has disconnected. Tidy up
"""
log.debug(u'socket disconnected')
self.close()
def _http_bad_request(self):
cherrypy.response.status = 400
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)
def _http_not_found(self):
cherrypy.response.status = 404
cherrypy.response.body = ["<html><body>Sorry, an error occured</body></html>"]
def _get_service_manager(self):
"""

View File

@ -27,9 +27,12 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
import os.path
from PyQt4 import QtCore, QtGui, QtNetwork
from openlp.core.lib import Registry, Settings, SettingsTab, translate
from openlp.core.utils import AppLocation
ZERO_URL = u'0.0.0.0'
@ -45,117 +48,203 @@ class RemoteTab(SettingsTab):
def setupUi(self):
self.setObjectName(u'RemoteTab')
SettingsTab.setupUi(self)
self.serverSettingsGroupBox = QtGui.QGroupBox(self.leftColumn)
self.serverSettingsGroupBox.setObjectName(u'serverSettingsGroupBox')
self.serverSettingsLayout = QtGui.QFormLayout(self.serverSettingsGroupBox)
self.serverSettingsLayout.setObjectName(u'serverSettingsLayout')
self.addressLabel = QtGui.QLabel(self.serverSettingsGroupBox)
self.addressLabel.setObjectName(u'addressLabel')
self.addressEdit = QtGui.QLineEdit(self.serverSettingsGroupBox)
self.addressEdit.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed)
self.addressEdit.setValidator(QtGui.QRegExpValidator(QtCore.QRegExp(
u'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'), self))
self.addressEdit.setObjectName(u'addressEdit')
QtCore.QObject.connect(self.addressEdit, QtCore.SIGNAL(u'textChanged(const QString&)'), self.setUrls)
self.serverSettingsLayout.addRow(self.addressLabel, self.addressEdit)
self.twelveHourCheckBox = QtGui.QCheckBox(self.serverSettingsGroupBox)
self.twelveHourCheckBox.setObjectName(u'twelveHourCheckBox')
self.serverSettingsLayout.addRow(self.twelveHourCheckBox)
self.portLabel = QtGui.QLabel(self.serverSettingsGroupBox)
self.portLabel.setObjectName(u'portLabel')
self.portSpinBox = QtGui.QSpinBox(self.serverSettingsGroupBox)
self.portSpinBox.setMaximum(32767)
self.portSpinBox.setObjectName(u'portSpinBox')
QtCore.QObject.connect(self.portSpinBox, QtCore.SIGNAL(u'valueChanged(int)'), self.setUrls)
self.serverSettingsLayout.addRow(self.portLabel, self.portSpinBox)
self.remoteUrlLabel = QtGui.QLabel(self.serverSettingsGroupBox)
self.remoteUrlLabel.setObjectName(u'remoteUrlLabel')
self.remoteUrl = QtGui.QLabel(self.serverSettingsGroupBox)
self.remoteUrl.setObjectName(u'remoteUrl')
self.remoteUrl.setOpenExternalLinks(True)
self.serverSettingsLayout.addRow(self.remoteUrlLabel, self.remoteUrl)
self.stageUrlLabel = QtGui.QLabel(self.serverSettingsGroupBox)
self.stageUrlLabel.setObjectName(u'stageUrlLabel')
self.stageUrl = QtGui.QLabel(self.serverSettingsGroupBox)
self.stageUrl.setObjectName(u'stageUrl')
self.stageUrl.setOpenExternalLinks(True)
self.serverSettingsLayout.addRow(self.stageUrlLabel, self.stageUrl)
self.leftLayout.addWidget(self.serverSettingsGroupBox)
self.androidAppGroupBox = QtGui.QGroupBox(self.rightColumn)
self.androidAppGroupBox.setObjectName(u'androidAppGroupBox')
self.rightLayout.addWidget(self.androidAppGroupBox)
self.qrLayout = QtGui.QVBoxLayout(self.androidAppGroupBox)
self.qrLayout.setObjectName(u'qrLayout')
self.qrCodeLabel = QtGui.QLabel(self.androidAppGroupBox)
self.qrCodeLabel.setPixmap(QtGui.QPixmap(u':/remotes/android_app_qr.png'))
self.qrCodeLabel.setAlignment(QtCore.Qt.AlignCenter)
self.qrCodeLabel.setObjectName(u'qrCodeLabel')
self.qrLayout.addWidget(self.qrCodeLabel)
self.qrDescriptionLabel = QtGui.QLabel(self.androidAppGroupBox)
self.qrDescriptionLabel.setObjectName(u'qrDescriptionLabel')
self.qrDescriptionLabel.setOpenExternalLinks(True)
self.qrDescriptionLabel.setWordWrap(True)
self.qrLayout.addWidget(self.qrDescriptionLabel)
self.server_settings_group_box = QtGui.QGroupBox(self.leftColumn)
self.server_settings_group_box.setObjectName(u'server_settings_group_box')
self.server_settings_layout = QtGui.QFormLayout(self.server_settings_group_box)
self.server_settings_layout.setObjectName(u'server_settings_layout')
self.address_label = QtGui.QLabel(self.server_settings_group_box)
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.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.leftLayout.addWidget(self.server_settings_group_box)
self.http_settings_group_box = QtGui.QGroupBox(self.leftColumn)
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.http_settings_group_box)
self.port_spin_box.setMaximum(32767)
self.port_spin_box.setObjectName(u'port_spin_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.http_settings_group_box)
self.remote_url.setObjectName(u'remote_url')
self.remote_url.setOpenExternalLinks(True)
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.http_settings_group_box)
self.stage_url.setObjectName(u'stage_url')
self.stage_url.setOpenExternalLinks(True)
self.http_setting_layout.addRow(self.stage_url_label, self.stage_url)
self.leftLayout.addWidget(self.http_settings_group_box)
self.https_settings_group_box = QtGui.QGroupBox(self.leftColumn)
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.leftLayout.addWidget(self.https_settings_group_box)
self.user_login_group_box = QtGui.QGroupBox(self.leftColumn)
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.leftLayout.addWidget(self.user_login_group_box)
self.android_app_group_box = QtGui.QGroupBox(self.rightColumn)
self.android_app_group_box.setObjectName(u'android_app_group_box')
self.rightLayout.addWidget(self.android_app_group_box)
self.qr_layout = QtGui.QVBoxLayout(self.android_app_group_box)
self.qr_layout.setObjectName(u'qr_layout')
self.qr_code_label = QtGui.QLabel(self.android_app_group_box)
self.qr_code_label.setPixmap(QtGui.QPixmap(u':/remotes/android_app_qr.png'))
self.qr_code_label.setAlignment(QtCore.Qt.AlignCenter)
self.qr_code_label.setObjectName(u'qr_code_label')
self.qr_layout.addWidget(self.qr_code_label)
self.qr_description_label = QtGui.QLabel(self.android_app_group_box)
self.qr_description_label.setObjectName(u'qr_description_label')
self.qr_description_label.setOpenExternalLinks(True)
self.qr_description_label.setWordWrap(True)
self.qr_layout.addWidget(self.qr_description_label)
self.leftLayout.addStretch()
self.rightLayout.addStretch()
QtCore.QObject.connect(self.twelveHourCheckBox, QtCore.SIGNAL(u'stateChanged(int)'),
self.onTwelveHourCheckBoxChanged)
self.twelve_hour_check_box.stateChanged.connect(self.on_twelve_hour_check_box_changed)
self.address_edit.textChanged.connect(self.set_urls)
self.port_spin_box.valueChanged.connect(self.set_urls)
self.https_port_spin_box.valueChanged.connect(self.set_urls)
def retranslateUi(self):
self.serverSettingsGroupBox.setTitle(
translate('RemotePlugin.RemoteTab', 'Server Settings'))
self.addressLabel.setText(translate('RemotePlugin.RemoteTab', 'Serve on IP address:'))
self.portLabel.setText(translate('RemotePlugin.RemoteTab', 'Port number:'))
self.remoteUrlLabel.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:'))
self.stageUrlLabel.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
self.twelveHourCheckBox.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
self.androidAppGroupBox.setTitle(translate('RemotePlugin.RemoteTab', 'Android App'))
self.qrDescriptionLabel.setText(translate('RemotePlugin.RemoteTab',
'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.server_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Server Settings'))
self.address_label.setText(translate('RemotePlugin.RemoteTab', 'Serve on IP address:'))
self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:'))
self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:'))
self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App'))
self.qr_description_label.setText(translate('RemotePlugin.RemoteTab',
'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 setUrls(self):
def set_urls(self):
ipAddress = u'localhost'
if self.addressEdit.text() == ZERO_URL:
ifaces = QtNetwork.QNetworkInterface.allInterfaces()
for iface in ifaces:
if not iface.isValid():
if self.address_edit.text() == ZERO_URL:
interfaces = QtNetwork.QNetworkInterface.allInterfaces()
for interface in interfaces:
if not interface.isValid():
continue
if not (iface.flags() & (QtNetwork.QNetworkInterface.IsUp | QtNetwork.QNetworkInterface.IsRunning)):
if not (interface.flags() & (QtNetwork.QNetworkInterface.IsUp | QtNetwork.QNetworkInterface.IsRunning)):
continue
for addr in iface.addressEntries():
ip = addr.ip()
for address in interface.addressEntries():
ip = address.ip()
if ip.protocol() == 0 and ip != QtNetwork.QHostAddress.LocalHost:
ipAddress = ip
break
else:
ipAddress = self.addressEdit.text()
url = u'http://%s:%s/' % (ipAddress, self.portSpinBox.value())
self.remoteUrl.setText(u'<a href="%s">%s</a>' % (url, url))
url += u'stage'
self.stageUrl.setText(u'<a href="%s">%s</a>' % (url, url))
ipAddress = self.address_edit.text()
http_url = u'http://%s:%s/' % (ipAddress, self.port_spin_box.value())
https_url = u'https://%s:%s/' % (ipAddress, 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.portSpinBox.setValue(Settings().value(self.settingsSection + u'/port'))
self.addressEdit.setText(Settings().value(self.settingsSection + u'/ip address'))
self.twelveHour = Settings().value(self.settingsSection + u'/twelve hour')
self.twelveHourCheckBox.setChecked(self.twelveHour)
self.setUrls()
self.port_spin_box.setValue(Settings().value(self.settingsSection + u'/port'))
self.https_port_spin_box.setValue(Settings().value(self.settingsSection + u'/https port'))
self.address_edit.setText(Settings().value(self.settingsSection + u'/ip address'))
self.twelve_hour = Settings().value(self.settingsSection + 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.settingsSection + 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.settingsSection + u'/authentication enabled'))
self.user_id.setText(Settings().value(self.settingsSection + u'/user id'))
self.password.setText(Settings().value(self.settingsSection + u'/password'))
self.set_urls()
def save(self):
changed = False
if Settings().value(self.settingsSection + u'/ip address') != self.addressEdit.text() or \
Settings().value(self.settingsSection + u'/port') != self.portSpinBox.value():
if Settings().value(self.settingsSection + u'/ip address') != self.address_edit.text() or \
Settings().value(self.settingsSection + u'/port') != self.port_spin_box.value() or \
Settings().value(self.settingsSection + u'/https port') != self.https_port_spin_box.value() or \
Settings().value(self.settingsSection + u'/https enabled') != self.https_settings_group_box.isChecked():
changed = True
Settings().setValue(self.settingsSection + u'/port', self.portSpinBox.value())
Settings().setValue(self.settingsSection + u'/ip address', self.addressEdit.text())
Settings().setValue(self.settingsSection + u'/twelve hour', self.twelveHour)
Settings().setValue(self.settingsSection + u'/port', self.port_spin_box.value())
Settings().setValue(self.settingsSection + u'/https port', self.https_port_spin_box.value())
Settings().setValue(self.settingsSection + u'/https enabled', self.https_settings_group_box.isChecked())
Settings().setValue(self.settingsSection + u'/ip address', self.address_edit.text())
Settings().setValue(self.settingsSection + u'/twelve hour', self.twelve_hour)
Settings().setValue(self.settingsSection + u'/authentication enabled', self.user_login_group_box.isChecked())
Settings().setValue(self.settingsSection + u'/user id', self.user_id.text())
Settings().setValue(self.settingsSection + u'/password', self.password.text())
if changed:
Registry().register_function(u'remotes_config_updated')
def onTwelveHourCheckBoxChanged(self, check_state):
self.twelveHour = False
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.twelveHour = True
self.twelve_hour = True

View File

@ -37,6 +37,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'
}

View File

@ -81,6 +81,7 @@ MODULES = [
'enchant',
'BeautifulSoup',
'mako',
'cherrypy',
'migrate',
'uno',
]