diff --git a/config.json b/config.json index 6c410b3..2cbdf65 100644 --- a/config.json +++ b/config.json @@ -318,6 +318,7 @@ "receivedRequests": "notify", "rejectMessage": "", "sendDiagnostics": false, + "enableReadOnlyService": false, "sendRequests": "manually", "uploadRate": null, "username": "" diff --git a/oml/item/api.py b/oml/item/api.py index 857dcf9..c94d7cf 100644 --- a/oml/item/api.py +++ b/oml/item/api.py @@ -113,6 +113,7 @@ def find(data): response['size'] = sum(size) return response actions.register(find) +actions.register(find, version='public') def get(data): @@ -128,6 +129,7 @@ def get(data): response = item.json(data['keys'] if 'keys' in data else None) return response actions.register(get) +actions.register(get, version='public') def edit(data): diff --git a/oml/oxtornado.py b/oml/oxtornado.py index 2ed78ce..7215e9a 100644 --- a/oml/oxtornado.py +++ b/oml/oxtornado.py @@ -71,8 +71,9 @@ def defaultcontext(): class ApiHandler(tornado.web.RequestHandler): executor = ThreadPoolExecutor(max_workers=MAX_WORKERS) - def initialize(self, context=None): + def initialize(self, context=None, public=False): self._context = context + self._public = public def get(self): self.write('use POST') @@ -81,7 +82,7 @@ class ApiHandler(tornado.web.RequestHandler): def api_task(self, request): import settings context = self._context - if context == None: + if context is None: context = defaultcontext action = request.arguments.get('action', [None])[0].decode('utf-8') data = request.arguments.get('data', [b'{}'])[0] @@ -96,7 +97,10 @@ class ApiHandler(tornado.web.RequestHandler): else: if settings.DEBUG_API: logger.debug('API %s %s', action, data) - f = actions.get(action) + if self._public: + f = actions.versions['public'].get(action) + else: + f = actions.get(action) if f: with context(): try: @@ -126,50 +130,60 @@ class ApiHandler(tornado.web.RequestHandler): class ApiActions(dict): properties = {} versions = {} - def __init__(self): - def api(data): - ''' - returns list of all known api actions - takes { - docs: bool - } - if docs is true, action properties contain docstrings - returns { - actions: { - 'api': { - cache: true, - doc: 'recursion' - }, - 'hello': { - cache: true, - .. - } - ... + def _api(self, data, version=None): + ''' + returns list of all known api actions + takes { + docs: bool + } + if docs is true, action properties contain docstrings + returns { + actions: { + 'api': { + cache: true, + doc: 'recursion' + }, + 'hello': { + cache: true, + .. } + ... } - ''' - data = data or {} - docs = data.get('docs', False) - code = data.get('code', False) + } + ''' + data = data or {} + docs = data.get('docs', False) + code = data.get('code', False) + if version: + _actions = list(self.versions[version].keys()) + else: _actions = list(self.keys()) - _actions.sort() - actions = {} - for a in _actions: - actions[a] = self.properties[a] - if docs: - actions[a]['doc'] = self.doc(a) - if code: - actions[a]['code'] = self.code(a) - return {'actions': actions} - self.register(api) + _actions.sort() + actions = {} + for a in _actions: + actions[a] = self.properties[a] + if docs: + actions[a]['doc'] = self.doc(a, version) + if code: + actions[a]['code'] = self.code(a, version) + return {'actions': actions} - def doc(self, name): - f = self[name] + def __init__(self): + self.register(self._api, 'api') + + def doc(self, name, version=None): + if version: + f = self.versions[version][name] + else: + f = self[name] return trim(f.__doc__) def code(self, name, version=None): - f = self[name] + if version: + f = self.versions[version][name] + else: + f = self[name] if name != 'api' and hasattr(f, 'func_closure') and f.__closure__: fc = [c for c in f.__closure__ if hasattr(c.cell_contents, '__call__')] f = fc[len(fc)-1].cell_contents @@ -181,8 +195,9 @@ class ApiActions(dict): if not action: action = method.__name__ if version: - if not version in self.versions: + if version not in self.versions: self.versions[version] = {} + self.register(lambda data: self._api(data, version), action='api', version=version) self.versions[version][action] = method else: self[action] = method @@ -192,4 +207,5 @@ class ApiActions(dict): if action in self: del self[action] + actions = ApiActions() diff --git a/oml/server.py b/oml/server.py index d0e997d..7a3f08d 100644 --- a/oml/server.py +++ b/oml/server.py @@ -95,7 +95,7 @@ def run(): else: debug = False - log_format='%(asctime)s:%(levelname)s:%(name)s:%(message)s' + log_format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s' if debug: logging.basicConfig(level=logging.DEBUG, format=log_format) else: @@ -109,7 +109,7 @@ def run(): 'gzip': True } - handlers = [ + common_handlers = [ (r'/(favicon.ico)', StaticFileHandler, {'path': settings.static_path}), (r'/static/oxjs/(.*)', StaticFileHandler, {'path': os.path.join(settings.base_dir, '..', 'oxjs')}), (r'/static/cbr.js/(.*)', StaticFileHandler, {'path': os.path.join(settings.base_dir, '..', 'reader', 'cbr.js')}), @@ -126,17 +126,33 @@ def run(): 'attachment': True }), (r'/(.*)/(cover|preview)(\d*).jpg', IconHandler), + ] + handlers = common_handlers + [ (r'/api/upload/', UploadHandler, dict(context=db.session)), (r'/api/', oxtornado.ApiHandler, dict(context=db.session)), (r'/ws', websocket.Handler), (r"(.*)", MainHandler), ] + public_handlers = common_handlers + [ + (r'/api/', oxtornado.ApiHandler, dict(context=db.session, public=True)), + (r'/ws', websocket.Handler, dict(public=True)), + (r"(.*)", MainHandler), + ] setup.create_db() http_server = Application(handlers, **options) max_buffer_size = 2*1024*1024*1024 http_server.listen(settings.server['port'], settings.server['address'], max_buffer_size=max_buffer_size) + # public server + ''' + public_port = settings.server.get('public_port') + public_address = settings.server['public_address'] + if public_port: + public_server = Application(public_handlers, **options) + public_server.listen(public_port, public_address) + ''' + if PID: with open(PID, 'w') as pid: pid.write('%s' % os.getpid()) diff --git a/oml/settings.py b/oml/settings.py index 345182a..59a6f77 100644 --- a/oml/settings.py +++ b/oml/settings.py @@ -44,6 +44,8 @@ server_defaults = { 'port': 9842, 'address': '127.0.0.1', 'node_port': 9851, + 'public_address': '127.0.0.1', + 'public_port': 9852, 'node_address': '', 'extract_text': True, 'localnode_discovery': True, diff --git a/oml/tor.py b/oml/tor.py index b51a9f3..d9600e6 100644 --- a/oml/tor.py +++ b/oml/tor.py @@ -200,6 +200,8 @@ class Tor(object): key_content = RSA.importKey(private_key).exportKey().decode() key_content = ''.join(key_content.strip().split('\n')[1:-1]) ports = {9851: settings.server['node_port']} + if settings.preferences.get('enableReadOnlyService'): + ports[80] = settings.server['public_port'] controller.remove_ephemeral_hidden_service(settings.USER_ID) response = controller.create_ephemeral_hidden_service(ports, key_type='RSA1024', key_content=key_content, @@ -207,6 +209,8 @@ class Tor(object): if response.is_ok(): logger.debug('published node as https://%s.onion:%s', settings.USER_ID, settings.server_defaults['node_port']) + if settings.preferences.get('enableReadOnlyService'): + logger.debug('published readonly version as hidden servers: http://%s.onion', settings.USER_ID) else: logger.debug('failed to publish node to tor') else: @@ -217,6 +221,9 @@ class Tor(object): target_port=settings.server['node_port'] ) logger.debug('published node as https://%s:%s', result.hostname, settings.server_defaults['node_port']) + if settings.preferences.get('enableReadOnlyService'): + logger.error('can not publish read-only version, please update TOR') + def depublish(self): if not self.connected: diff --git a/oml/update.py b/oml/update.py index 566d66b..1725b03 100644 --- a/oml/update.py +++ b/oml/update.py @@ -40,8 +40,8 @@ def verify(release): value = '\n'.join(value) value = value.encode() for digest in ('sha512', 'sha256', 'sha1'): - if 'signature_%s'%digest in release: - tls_sig = base64.b64decode(release['signature_%s'%digest].encode()) + if 'signature_%s' % digest in release: + tls_sig = base64.b64decode(release['signature_%s' % digest].encode()) cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, settings.OML_UPDATE_CERT) try: OpenSSL.crypto.verify(cert, tls_sig, value, digest) @@ -301,6 +301,7 @@ def getVersion(data): response['update'] = current < new return response actions.register(getVersion, cache=False) +actions.register(getVersion, cache=False, version='public') def restart(data): ''' diff --git a/oml/user/api.py b/oml/user/api.py index 63216e9..f824282 100644 --- a/oml/user/api.py +++ b/oml/user/api.py @@ -55,6 +55,21 @@ def init(data): return response actions.register(init) +def public_init(data): + response = init(data) + name = response['user']['preferences']['username'] + response['user'] = response['config']['user'] + response['user']['preferences']['username'] = name + response['user']['ui']['page'] = '' + response['user']['ui']['showFolder'] = {'': True} + response['readOnly'] = True + for page in response['config']['pages']: + if page['id'] == 'preferences': + #page['parts'] = [p for p in page['parts'] if p['id'] in ('appearance', 'extensions')] + page['parts'] = [p for p in page['parts'] if p['id'] in ('appearance',)] + return response +actions.register(public_init, action='init', version='public') + def setPreferences(data): ''' @@ -149,6 +164,12 @@ def getUsers(data): } actions.register(getUsers) +def getUsersPublic(data): + return { + 'users': [] + } +actions.register(getUsersPublic, 'getUsers', version='public') + def getLists(data): ''' @@ -172,6 +193,29 @@ def getLists(data): } actions.register(getLists) +def getListsPublic(data): + ''' + returns { + lists: [] + } + ''' + from item.models import Item + from sqlalchemy.sql import operators + user = state.user() + lists = [] + lists.append({ + 'id': '', + 'items': user.items.count(), + 'name': 'Libraries', + 'type': 'libraries', + 'user': None, + }) + lists += user.lists_json() + return { + 'lists': lists + } +actions.register(getListsPublic, 'getLists', version='public') + def validate_query(query): validate_conditions(query['conditions']) diff --git a/oml/websocket.py b/oml/websocket.py index 7e468f2..90821bb 100644 --- a/oml/websocket.py +++ b/oml/websocket.py @@ -17,6 +17,9 @@ logger = logging.getLogger(__name__) class Handler(WebSocketHandler): + def initialize(self, public=False): + self._public = public + def check_origin(self, origin): # allow access to websocket from site, installer and loader (local file) return self.request.host in origin or \ diff --git a/static/html/app.html b/static/html/app.html index 3e9250d..2bde35f 100644 --- a/static/html/app.html +++ b/static/html/app.html @@ -15,7 +15,7 @@
- The latest code is in our git repository. + The latest code is in our git repository.
For everything else, there's IRC, and our development mailing list. Your feedback is welcome. diff --git a/static/js/UI.js b/static/js/UI.js index 430796b..82e3386 100644 --- a/static/js/UI.js +++ b/static/js/UI.js @@ -15,14 +15,21 @@ oml.UI = (function() { that.reset = function() { var ui = oml.user.ui; - oml.api.resetUI({}, function() { + + function reload() { ui = oml.config.user.ui; ui._list = oml.getListState(ui.find); ui._filterState = oml.getFilterState(ui.find); ui._findState = oml.getFindState(ui.find); Ox.Theme(ui.theme); oml.$ui.appPanel.reload(); - }); + } + if (oml.readOnly) { + oml.localStorage('ui', oml.config.user.ui); + reload(); + } else { + oml.api.resetUI({}, reload); + } }; // sets oml.user.ui.key to value @@ -161,7 +168,11 @@ oml.UI = (function() { }); }); if (Ox.len(set)) { - oml.api.setUI(set); + if (oml.readOnly) { + oml.localStorage('ui', oml.user.ui); + } else { + oml.api.setUI(set); + } } if (triggerEvents) { Ox.forEach(trigger, function(value, key) { @@ -182,4 +193,4 @@ oml.UI = (function() { return that; -}()); \ No newline at end of file +}()); diff --git a/static/js/appDialog.js b/static/js/appDialog.js index ac3e409..2576d32 100644 --- a/static/js/appDialog.js +++ b/static/js/appDialog.js @@ -25,12 +25,13 @@ oml.ui.appDialog = function() { title: Ox._('Software Development'), selected: ui.page == 'development' }, + ].concat(oml.readOnly ? [] : [ { id: 'contact', title: Ox._('Send Feedback'), selected: ui.page == 'contact' } - ], + ]), $panel = Ox.TabPanel({ content: function(id) { @@ -95,7 +96,7 @@ oml.ui.appDialog = function() { }), that = Ox.Dialog({ - buttons: [ + buttons: (oml.readOnly ? [] : [ Ox.Button({ id: 'update', style: 'squared', @@ -106,6 +107,7 @@ oml.ui.appDialog = function() { oml.UI.set({page: 'update'}); } }), + ]).concat([ {}, Ox.Button({ id: 'close', @@ -116,7 +118,7 @@ oml.ui.appDialog = function() { that.close(); } }) - ], + ]), closeButton: true, content: $panel, fixedSize: true, diff --git a/static/js/infoView.js b/static/js/infoView.js index 730013b..1a8e1c9 100644 --- a/static/js/infoView.js +++ b/static/js/infoView.js @@ -270,9 +270,10 @@ oml.ui.infoView = function(externalData, isMixed) { items: [ {id: 'read', title: Ox._('Read in Open Media Libary')}, {id: 'open', title: Ox._('Open in External Reader')}, + ].concat(oml.readOnly ? [] : [ {}, {id: 'show', title: Ox._('Show File')} - ], + ]), overlap: 'left', style: 'squared', title: 'select', @@ -284,7 +285,11 @@ oml.ui.infoView = function(externalData, isMixed) { if (data_.id == 'read') { oml.UI.set({itemView: 'book'}); } else if (data_.id == 'open') { - oml.api.openFile({id: oml.user.ui.item}); + if (oml.readOnly) { + document.location.href = '/' + oml.user.ui.item + '/get/' + } else { + oml.api.openFile({id: oml.user.ui.item}); + } } else { oml.api.openFolder({id: oml.user.ui.item}); } @@ -382,9 +387,9 @@ oml.ui.infoView = function(externalData, isMixed) { Ox.print('BOOK DATA', data) var $div, - isEditable = isMultiple || ( + isEditable = !oml.readOnly && (isMultiple || ( data.mediastate == 'available' && !externalData - ), + )), src = !externalData ? '/' + data.id + '/' + ui.icons + '512.jpg?' + data.modified : data.cover, @@ -751,28 +756,29 @@ oml.ui.infoView = function(externalData, isMixed) { ].join(', ') ) .appendTo($data); + if (!oml.readOnly) { + renderIdentifyButton(data).appendTo($data); - renderIdentifyButton(data).appendTo($data); + ['accessed', 'modified', 'added', 'created'].forEach(function(id) { + var title; + if (data[id]) { + title = Ox.getObjectById(oml.config.itemKeys, id).title; + $('