Compare commits

...

41 commits

Author SHA1 Message Date
j
0ecba6222d don't fail if zeroconf interface was shut down 2024-08-09 15:41:19 +02:00
j
a85cd8b9bf use wss if site was loaded via https 2024-08-09 15:40:19 +02:00
j
761f973895 ip change is async 2024-06-13 13:31:15 +01:00
j
7b826468d8 remove crop debugging 2024-06-10 16:26:27 +01:00
j
f01807bfa7 remove old pid location fallback 2024-06-10 15:29:12 +01:00
j
50e07bced6 top_dir fixup 2024-06-10 15:17:26 +01:00
j
d5ff48a1c5 unify top directory 2024-06-10 15:01:46 +01:00
j
4df34b28e5 add highlight annotations 2024-06-10 14:05:17 +01:00
j
7e1a282ad6 update pdf.js 2024-06-10 14:03:00 +01:00
j
46eba991e4 debug output 2024-06-10 14:02:41 +01:00
j
5bd561e64f extract detail from pdf 2024-06-09 14:47:36 +01:00
j
fd34ba305c tor fixes 2024-06-09 14:46:28 +01:00
j
8a5d8072ca ignore nodes id is wrong 2024-06-08 17:09:08 +01:00
j
e2ea8fe42f migrate local user to V3 2024-06-08 15:53:02 +01:00
j
da30a40fd6 cache can_connect for 3 seconds 2024-06-08 15:52:13 +01:00
j
647a8b95bc update python-ox repo, egg name 2024-06-08 14:31:06 +01:00
j
2b58800caa fix local peer discovery 2024-06-08 14:31:06 +01:00
j
d8cd9ecd4f run read only backend if enableReadOnlyService is set 2024-06-08 14:31:06 +01:00
j
60e17ab076 work around new sqlalchemy limitations 2024-06-08 14:31:06 +01:00
j
c14d250166 update SQLAlchemy, switch to pycryptodome 2024-06-08 14:31:06 +01:00
j
71634c9ed1 switch to onion v3 ids 2024-06-08 14:31:06 +01:00
j
e175c72a40 switch default linux version to 3.11 2024-06-08 12:26:02 +01:00
j
37410d6089 use list instead of get_children 2024-06-08 12:23:25 +01:00
j
d29309e8b3 remove debug 2024-06-08 12:22:33 +01:00
j
ebfe7898ca scan only counts if it was completed 2021-02-05 17:40:57 +01:00
j
8766c7ef4e checking item can faile with sqlalchemy.orm.exc.ObjectDeletedError 2021-02-03 10:19:17 +01:00
j
91fd3a61f5 handle urlerror timeout 2021-01-31 18:05:34 +01:00
j
ab7863807b queue previews after sync 2021-01-31 16:12:12 +01:00
j
5b6ef3d669 update dependencies 2021-01-31 16:11:50 +01:00
j
6cf39c2ba6 space 2021-01-31 16:10:50 +01:00
j
93708b7625 docs 2021-01-31 15:55:41 +01:00
j
29200fc58b use id from settings, no need for db lookup 2021-01-31 15:53:59 +01:00
j
6c39ae2c7b use local openssl.cnf if it exists 2021-01-28 23:09:17 +01:00
j
808b72316a support application/json request body 2019-10-13 12:16:33 +01:00
j
14c8345740 fix import 2019-10-13 11:16:56 +01:00
j
1e39fc48b2 use ThreadPoolExecutor 2019-10-13 11:16:45 +01:00
j
0e11f04d44 pass id, don't depend on cast 2019-10-13 11:16:33 +01:00
j
45fb954164 overflow count to the left 2019-10-02 11:04:20 -07:00
j
aa968e00fd start check ip change task 2019-06-18 09:19:06 +02:00
j
9d01259d66 space 2019-06-18 09:18:23 +02:00
j
19e790fc3a sort non string keys 2019-03-05 19:48:53 +01:00
42 changed files with 1340 additions and 595 deletions

16
ctl
View file

@ -1,6 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
NAME="openmedialibrary" NAME="openmedialibrary"
PID="/tmp/$NAME.$USER.pid"
cd "`dirname "$0"`" cd "`dirname "$0"`"
if [ -e oml ]; then if [ -e oml ]; then
@ -25,16 +24,7 @@ else
mv "$BASE/config/release.json" "$BASE/data/release.json" mv "$BASE/config/release.json" "$BASE/data/release.json"
fi fi
fi fi
if [ ! -e "$PID" ]; then
if [ -e "$DATA/tor/hostname" ]; then
onion=$(cat "$DATA/tor/hostname")
id=${onion/.onion/}
PID="/tmp/$NAME.$USER.$id.pid"
fi
fi
if [ ! -e "$PID" ]; then
PID="$DATA/$NAME.pid" PID="$DATA/$NAME.pid"
fi
PLATFORM_PYTHON=3.4 PLATFORM_PYTHON=3.4
SHARED_PYTHON=3.7 SHARED_PYTHON=3.7
@ -44,7 +34,7 @@ else
if [ $SYSTEM == "Linux" ]; then if [ $SYSTEM == "Linux" ]; then
if [ $PLATFORM == "x86_64" ]; then if [ $PLATFORM == "x86_64" ]; then
ARCH=64 ARCH=64
PLATFORM_PYTHON=3.7 PLATFORM_PYTHON=3.11
else else
ARCH=32 ARCH=32
fi fi
@ -74,6 +64,10 @@ PATH="$PLATFORM_ENV/bin:$PATH"
SHARED_ENV="$BASE/platform/Shared" SHARED_ENV="$BASE/platform/Shared"
export SHARED_ENV export SHARED_ENV
if [ -e "$SHARED_ENV/etc/openssl/openssl.cnf" ]; then
export OPENSSL_CONF="$SHARED_ENV/etc/openssl/openssl.cnf"
fi
PATH="$SHARED_ENV/bin:$PATH" PATH="$SHARED_ENV/bin:$PATH"
export PATH export PATH

View file

@ -297,7 +297,7 @@ class Changelog(db.Model):
return True return True
def action_addpeer(self, user, timestamp, peerid, username): def action_addpeer(self, user, timestamp, peerid, username):
if len(peerid) == 16: if len(peerid) == settings.ID_LENGTH:
from user.models import User from user.models import User
if not 'users' in user.info: if not 'users' in user.info:
user.info['users'] = {} user.info['users'] = {}
@ -318,7 +318,7 @@ class Changelog(db.Model):
return True return True
def action_editpeer(self, user, timestamp, peerid, data): def action_editpeer(self, user, timestamp, peerid, data):
if len(peerid) == 16: if len(peerid) == settings.ID_LENGTH:
from user.models import User from user.models import User
peer = User.get_or_create(peerid) peer = User.get_or_create(peerid)
update = False update = False
@ -466,7 +466,7 @@ class Changelog(db.Model):
elif op == 'addpeer': elif op == 'addpeer':
peer_id = data[1] peer_id = data[1]
username = data[2] username = data[2]
if len(peer_id) == 16: if len(peer_id) == settings.ID_LENGTH:
peer = User.get(peer_id) peer = User.get(peer_id)
if peer: if peer:
username = peer.json().get('username', 'anonymous') username = peer.json().get('username', 'anonymous')

View file

@ -154,7 +154,7 @@ def command_update_static(*args):
import utils import utils
setup.create_db() setup.create_db()
old_oxjs = os.path.join(settings.static_path, 'oxjs') old_oxjs = os.path.join(settings.static_path, 'oxjs')
oxjs = os.path.join(settings.base_dir, '..', 'oxjs') oxjs = os.path.join(settings.top_dir, 'oxjs')
if os.path.exists(old_oxjs) and not os.path.exists(oxjs): if os.path.exists(old_oxjs) and not os.path.exists(oxjs):
shutil.move(old_oxjs, oxjs) shutil.move(old_oxjs, oxjs)
if not os.path.exists(oxjs): if not os.path.exists(oxjs):
@ -163,7 +163,7 @@ def command_update_static(*args):
os.system('cd "%s" && git pull' % oxjs) os.system('cd "%s" && git pull' % oxjs)
r('python3', os.path.join(oxjs, 'tools', 'build', 'build.py'), '-nogeo') r('python3', os.path.join(oxjs, 'tools', 'build', 'build.py'), '-nogeo')
utils.update_static() utils.update_static()
reader = os.path.join(settings.base_dir, '..', 'reader') reader = os.path.join(settings.top_dir, 'reader')
if not os.path.exists(reader): if not os.path.exists(reader):
r('git', 'clone', '--depth', '1', 'https://code.0x2620.org/0x2620/openmedialibrary_reader.git', reader) r('git', 'clone', '--depth', '1', 'https://code.0x2620.org/0x2620/openmedialibrary_reader.git', reader)
elif os.path.exists(os.path.join(reader, '.git')): elif os.path.exists(os.path.join(reader, '.git')):

View file

@ -5,7 +5,7 @@ import os
import unicodedata import unicodedata
from sqlalchemy.orm import load_only from sqlalchemy.orm import load_only
from sqlalchemy.sql.expression import text from sqlalchemy.sql.expression import text, column
from sqlalchemy import func from sqlalchemy import func
from oxtornado import actions from oxtornado import actions
@ -58,8 +58,13 @@ def find(data):
qs = models.Find.query.filter_by(key=q['group']) qs = models.Find.query.filter_by(key=q['group'])
if items is None or items.first(): if items is None or items.first():
if items is not None: if items is not None:
qs = qs.filter(models.Find.item_id.in_(items)) ids = [i[0] for i in items.with_entities(column('id'))]
values = list(qs.values('value', 'findvalue', 'sortvalue')) qs = qs.filter(models.Find.item_id.in_(ids))
values = list(qs.values(
column('value'),
column('findvalue'),
column('sortvalue'),
))
for f in values: for f in values:
value = f[0] value = f[0]
findvalue = f[1] findvalue = f[1]
@ -167,7 +172,7 @@ actions.register(edit, cache=False)
def remove(data): def remove(data):
''' '''
takes { takes {
id ids
} }
''' '''
if 'ids' in data and data['ids']: if 'ids' in data and data['ids']:

View file

@ -17,7 +17,8 @@ import db
import settings import settings
import tornado.web import tornado.web
import tornado.gen import tornado.gen
import tornado.concurrent from concurrent.futures import ThreadPoolExecutor
from tornado.concurrent import run_on_executor
from oxtornado import json_dumps, json_response from oxtornado import json_dumps, json_response
@ -27,6 +28,8 @@ import state
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
MAX_WORKERS = 4
class OptionalBasicAuthMixin(object): class OptionalBasicAuthMixin(object):
class SendChallenge(Exception): class SendChallenge(Exception):
@ -90,6 +93,23 @@ class EpubHandler(OMLHandler):
self.set_header('Content-Type', content_type) self.set_header('Content-Type', content_type)
self.write(z.read(filename)) self.write(z.read(filename))
class CropHandler(OMLHandler):
def get(self, id, page, left, top, right, bottom):
from media.pdf import crop
with db.session():
item = Item.get(id)
path = item.get_path()
data = crop(path, page, left, top, right, bottom)
if path and data:
self.set_header('Content-Type', 'image/jpeg')
self.set_header('Content-Length', str(len(data)))
self.write(data)
return
self.set_status(404)
return
def serve_static(handler, path, mimetype, include_body=True, disposition=None): def serve_static(handler, path, mimetype, include_body=True, disposition=None):
handler.set_header('Content-Type', mimetype) handler.set_header('Content-Type', mimetype)
size = os.stat(path).st_size size = os.stat(path).st_size
@ -177,6 +197,7 @@ class ReaderHandler(OMLHandler):
return serve_static(self, path, 'text/html') return serve_static(self, path, 'text/html')
class UploadHandler(OMLHandler): class UploadHandler(OMLHandler):
executor = ThreadPoolExecutor(max_workers=MAX_WORKERS)
def initialize(self, context=None): def initialize(self, context=None):
self._context = context self._context = context
@ -184,22 +205,14 @@ class UploadHandler(OMLHandler):
def get(self): def get(self):
self.write('use POST') self.write('use POST')
@tornado.web.asynchronous @run_on_executor
@tornado.gen.coroutine def save_files(self, request):
def post(self):
if 'origin' in self.request.headers and self.request.host not in self.request.headers['origin']:
logger.debug('reject cross site attempt to access api %s', self.request)
self.set_status(403)
self.write('')
return
def save_files(context, request, callback):
listname = request.arguments.get('list', None) listname = request.arguments.get('list', None)
if listname: if listname:
listname = listname[0] listname = listname[0]
if isinstance(listname, bytes): if isinstance(listname, bytes):
listname = listname.decode('utf-8') listname = listname.decode('utf-8')
with context(): with self._context():
prefs = settings.preferences prefs = settings.preferences
ids = [] ids = []
for upload in request.files.get('files', []): for upload in request.files.get('files', []):
@ -240,13 +253,21 @@ class UploadHandler(OMLHandler):
add_record('edititem', item.id, item.meta) add_record('edititem', item.id, item.meta)
item.update() item.update()
if listname and ids: if listname and ids:
l = List.get(settings.USER_ID, listname) list_ = List.get(settings.USER_ID, listname)
if l: if list_:
l.add_items(ids) list_.add_items(ids)
response = json_response({'ids': ids}) response = json_response({'ids': ids})
callback(response) return response
response = yield tornado.gen.Task(save_files, self._context, self.request) @tornado.gen.coroutine
def post(self):
if 'origin' in self.request.headers and self.request.host not in self.request.headers['origin']:
logger.debug('reject cross site attempt to access api %s', self.request)
self.set_status(403)
self.write('')
return
response = yield self.save_files(self.request)
if 'status' not in response: if 'status' not in response:
response = json_response(response) response = json_response(response)
response = json_dumps(response) response = json_dumps(response)

View file

@ -322,7 +322,7 @@ class Item(db.Model):
def remove_annotations(self): def remove_annotations(self):
from annotation.models import Annotation from annotation.models import Annotation
for a in Annotation.query.filter_by(item_id=self.id, user_id=state.user()): for a in Annotation.query.filter_by(item_id=self.id, user_id=settings.USER_ID):
a.add_record('removeannotation') a.add_record('removeannotation')
a.delete() a.delete()
@ -733,7 +733,13 @@ class File(db.Model):
return re.sub(r'^\.|\.$|:|/|\?|<|>|\\|\*|\||"', '_', string) return re.sub(r'^\.|\.$|:|/|\?|<|>|\\|\*|\||"', '_', string)
prefs = settings.preferences prefs = settings.preferences
prefix = os.sep.join(os.path.join(os.path.expanduser(prefs['libraryPath']), 'Books/').split('/')) prefix = os.sep.join(os.path.join(os.path.expanduser(prefs['libraryPath']), 'Books/').split('/'))
if not self.item: not_item = False
try:
not_item = not self.item
except:
logger.debug('trying to move an item that was just deleted', exc_info=True)
not_item = True
if not_item:
return return
j = self.item.json(keys=['title', 'author', 'publisher', 'date', 'extension']) j = self.item.json(keys=['title', 'author', 'publisher', 'date', 'extension'])

View file

@ -23,7 +23,6 @@ def parse(data):
if [r for r in query['range'] if not isinstance(r, int)]: if [r for r in query['range'] if not isinstance(r, int)]:
logger.error('range must be 2 integers! got this: %s', query['range']) logger.error('range must be 2 integers! got this: %s', query['range'])
query['range'] = [0, 0] query['range'] = [0, 0]
#print data
query['qs'] = models.Item.find(data) query['qs'] = models.Item.find(data)
if 'group' not in query: if 'group' not in query:
query['qs'] = order(query['qs'], query['sort']) query['qs'] = order(query['qs'], query['sort'])

View file

@ -225,6 +225,7 @@ def run_scan():
missing = ids - library_items missing = ids - library_items
if missing: if missing:
logger.debug('%s items in library without a record', len(missing)) logger.debug('%s items in library without a record', len(missing))
settings.server['last_scan'] = time.mktime(time.gmtime())
def change_path(old, new): def change_path(old, new):
old_icons = os.path.join(old, 'Metadata', 'icons.db') old_icons = os.path.join(old, 'Metadata', 'icons.db')

View file

@ -153,7 +153,7 @@ class Peer(object):
self.info['lists'][name] = list(set(self.info['lists'][name]) - set(ids)) self.info['lists'][name] = list(set(self.info['lists'][name]) - set(ids))
elif action == 'addpeer': elif action == 'addpeer':
peerid, username = args peerid, username = args
if len(peerid) == 16: if len(peerid) == settings.ID_LENGTH:
self.info['peers'][peerid] = {'username': username} self.info['peers'][peerid] = {'username': username}
# fixme, just trigger peer update here # fixme, just trigger peer update here
from user.models import User from user.models import User
@ -164,7 +164,7 @@ class Peer(object):
peer.save() peer.save()
elif action == 'editpeer': elif action == 'editpeer':
peerid, data = args peerid, data = args
if len(peerid) == 16: if len(peerid) == settings.ID_LENGTH:
if peerid not in self.info['peers']: if peerid not in self.info['peers']:
self.info['peers'][peerid] = {} self.info['peers'][peerid] = {}
for key in ('username', 'contact'): for key in ('username', 'contact'):
@ -377,6 +377,10 @@ def sync_db():
from sqlalchemy.orm import load_only from sqlalchemy.orm import load_only
import item.models import item.models
first = True first = True
missing_previews = []
state.sync_db = True
#FIXME: why is this loop needed
with db.session(): with db.session():
sort_ids = {i.item_id for i in item.models.Sort.query.options(load_only('item_id'))} sort_ids = {i.item_id for i in item.models.Sort.query.options(load_only('item_id'))}
if sort_ids: if sort_ids:
@ -387,9 +391,11 @@ def sync_db():
if first: if first:
first = False first = False
logger.debug('sync items') logger.debug('sync items')
i.update(commit=False) #why?
if i.info.get('mediastate') == 'unavailable' and state.tasks: #i.update(commit=False)
state.tasks.queue('getpreview', i.id) i.update_sort(commit=False)
if i.info.get('mediastate') == 'unavailable':
missing_previews.append(i.id)
commit = True commit = True
#logger.debug('sync:%s', i) #logger.debug('sync:%s', i)
t0 = maybe_commit(t0) t0 = maybe_commit(t0)
@ -397,6 +403,7 @@ def sync_db():
break break
if commit: if commit:
state.db.session.commit() state.db.session.commit()
if not first: if not first:
logger.debug('synced items') logger.debug('synced items')
if not state.shutdown: if not state.shutdown:
@ -408,6 +415,12 @@ def sync_db():
item.models.Sort.query.filter_by(item_id=None).delete() item.models.Sort.query.filter_by(item_id=None).delete()
item.models.Find.query.filter_by(item_id=None).delete() item.models.Find.query.filter_by(item_id=None).delete()
if missing_previews and state.tasks:
logger.debug('queueing download of %s missing previews', len(missing_previews))
for id in missing_previews:
state.tasks.queue('getpreview', id)
state.sync_db = False
def cleanup_lists(): def cleanup_lists():
import item.models import item.models
import user.models import user.models

View file

@ -1,22 +1,26 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import asyncio
import socket import socket
import netifaces import netifaces
from zeroconf import ( from zeroconf import (
ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf ServiceBrowser, ServiceInfo, ServiceStateChange
) )
from zeroconf.asyncio import AsyncZeroconf
from tornado.ioloop import PeriodicCallback from tornado.ioloop import PeriodicCallback
import settings import settings
import state import state
from tor_request import get_opener from tor_request import get_opener
from utils import time_cache
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def can_connect(data): @time_cache(3)
def can_connect(**data):
try: try:
opener = get_opener(data['id']) opener = get_opener(data['id'])
headers = { headers = {
@ -60,90 +64,110 @@ class LocalNodes(dict):
return return
self.setup() self.setup()
self._ip_changed = PeriodicCallback(self._update_if_ip_changed, 60000) self._ip_changed = PeriodicCallback(self._update_if_ip_changed, 60000)
state.main.add_callback(self._ip_changed.start)
def setup(self): def setup(self):
self.local_ips = get_broadcast_interfaces() self.local_ips = get_broadcast_interfaces()
self.zeroconf = {ip: Zeroconf(interfaces=[ip]) for ip in self.local_ips} self.zeroconf = {ip: AsyncZeroconf(interfaces=[ip]) for ip in self.local_ips}
self.register_service() asyncio.create_task(self.register_service())
self.browse() self.browse()
def _update_if_ip_changed(self): async def _update_if_ip_changed(self):
local_ips = get_broadcast_interfaces() local_ips = get_broadcast_interfaces()
username = settings.preferences.get('username', 'anonymous') username = settings.preferences.get('username', 'anonymous')
if local_ips != self.local_ips or self.username != username: if local_ips != self.local_ips or self.username != username:
self.close() await self.close()
self.setup() self.setup()
def browse(self): def browse(self):
self.browser = { self.browser = {
ip: ServiceBrowser(self.zeroconf[ip], self.service_type, handlers=[self.on_service_state_change]) ip: ServiceBrowser(self.zeroconf[ip].zeroconf, self.service_type, handlers=[self.on_service_state_change])
for ip in self.zeroconf for ip in self.zeroconf
} }
def register_service(self): async def register_service(self):
if self.local_info: if self.local_info:
for local_ip, local_info in self.local_info: for local_ip, local_info in self.local_info:
self.zeroconf[local_ip].unregister_service(local_info) self.zeroconf[local_ip].async_unregister_service(local_info)
self.local_info = None self.local_info = None
local_name = socket.gethostname().partition('.')[0] + '.local.' local_name = socket.gethostname().partition('.')[0] + '.local.'
port = settings.server['node_port'] port = settings.server['node_port']
self.local_info = []
self.username = settings.preferences.get('username', 'anonymous') self.username = settings.preferences.get('username', 'anonymous')
desc = { desc = {
'username': self.username 'username': self.username,
'id': settings.USER_ID,
} }
self.local_info = [] tasks = []
for i, local_ip in enumerate(get_broadcast_interfaces()): for i, local_ip in enumerate(get_broadcast_interfaces()):
if i: if i:
name = '%s-%s [%s].%s' % (desc['username'], i+1, settings.USER_ID, self.service_type) name = '%s [%s].%s' % (desc['username'], i, self.service_type)
else: else:
name = '%s [%s].%s' % (desc['username'], settings.USER_ID, self.service_type) name = '%s.%s' % (desc['username'], self.service_type)
local_info = ServiceInfo(self.service_type, name,
socket.inet_aton(local_ip), port, 0, 0, desc, local_name) addresses = [socket.inet_aton(local_ip)]
self.zeroconf[local_ip].register_service(local_info) local_info = ServiceInfo(self.service_type, name, port, 0, 0, desc, local_name, addresses=addresses)
task = self.zeroconf[local_ip].async_register_service(local_info)
tasks.append(task)
self.local_info.append((local_ip, local_info)) self.local_info.append((local_ip, local_info))
await asyncio.gather(*tasks)
def __del__(self): def __del__(self):
self.close() self.close()
def close(self): async def close(self):
if self.local_info: if self.local_info:
tasks = []
for local_ip, local_info in self.local_info: for local_ip, local_info in self.local_info:
try: try:
self.zeroconf[local_ip].unregister_service(local_info) task = self.zeroconf[local_ip].async_unregister_service(local_info)
tasks.append(task)
except: except:
logger.debug('exception closing zeroconf', exc_info=True) logger.debug('exception closing zeroconf', exc_info=True)
self.local_info = None self.local_info = None
if self.zeroconf: if self.zeroconf:
for local_ip in self.zeroconf: for local_ip in self.zeroconf:
try: try:
self.zeroconf[local_ip].close() task = self.zeroconf[local_ip].async_close()
tasks.append(task)
except: except:
logger.debug('exception closing zeroconf', exc_info=True) logger.debug('exception closing zeroconf', exc_info=True)
self.zeroconf = None self.zeroconf = None
for id in list(self): for id in list(self):
self.pop(id, None) self.pop(id, None)
await asyncio.gather(*tasks)
def on_service_state_change(self, zeroconf, service_type, name, state_change): def on_service_state_change(self, zeroconf, service_type, name, state_change):
if '[' not in name: try:
id = name.split('.')[0] info = zeroconf.get_service_info(service_type, name)
else: except zeroconf._exceptions.NotRunningException:
id = name.split('[')[1].split(']')[0] return
if info and b'id' in info.properties:
id = info.properties[b'id'].decode()
if id == settings.USER_ID: if id == settings.USER_ID:
return return
if len(id) != settings.ID_LENGTH:
return
if state_change is ServiceStateChange.Added: if state_change is ServiceStateChange.Added:
info = zeroconf.get_service_info(service_type, name) new = id not in self
if info:
self[id] = { self[id] = {
'id': id, 'id': id,
'host': socket.inet_ntoa(info.address), 'host': socket.inet_ntoa(info.addresses[0]),
'port': info.port 'port': info.port
} }
if info.properties: if info.properties:
for key, value in info.properties.items(): for key, value in info.properties.items():
key = key.decode() key = key.decode()
self[id][key] = value.decode() self[id][key] = value.decode()
logger.debug('add: %s [%s] (%s:%s)', self[id].get('username', 'anon'), id, self[id]['host'], self[id]['port']) logger.debug(
'%s: %s [%s] (%s:%s)',
'add' if new else 'update',
self[id].get('username', 'anon'),
id,
self[id]['host'],
self[id]['port']
)
if state.tasks and id in self: if state.tasks and id in self:
state.tasks.queue('addlocalinfo', self[id]) state.tasks.queue('addlocalinfo', self[id])
elif state_change is ServiceStateChange.Removed: elif state_change is ServiceStateChange.Removed:
@ -154,6 +178,6 @@ class LocalNodes(dict):
def get_data(self, user_id): def get_data(self, user_id):
data = self.get(user_id) data = self.get(user_id)
if data and can_connect(data): if data and can_connect(**data):
return data return data
return None return None

View file

@ -68,17 +68,17 @@ def cover(path):
if manifest: if manifest:
manifest = manifest[0] manifest = manifest[0]
if metadata and manifest: if metadata and manifest:
for e in metadata.getchildren(): for e in list(metadata):
if e.tag == '{http://www.idpf.org/2007/opf}meta' and e.attrib.get('name') == 'cover': if e.tag == '{http://www.idpf.org/2007/opf}meta' and e.attrib.get('name') == 'cover':
cover_id = e.attrib['content'] cover_id = e.attrib['content']
for e in manifest.getchildren(): for e in list(manifest):
if e.attrib['id'] == cover_id: if e.attrib['id'] == cover_id:
filename = unquote(e.attrib['href']) filename = unquote(e.attrib['href'])
filename = normpath(os.path.join(os.path.dirname(opf[0]), filename)) filename = normpath(os.path.join(os.path.dirname(opf[0]), filename))
if filename in files: if filename in files:
return use(filename) return use(filename)
if manifest: if manifest:
images = [e for e in manifest.getchildren() if 'image' in e.attrib['media-type']] images = [e for e in list(manifest) if 'image' in e.attrib['media-type']]
if images: if images:
image_data = [] image_data = []
for e in images: for e in images:
@ -89,7 +89,7 @@ def cover(path):
if image_data: if image_data:
image_data.sort(key=lambda name: z.getinfo(name).file_size) image_data.sort(key=lambda name: z.getinfo(name).file_size)
return use(image_data[-1]) return use(image_data[-1])
for e in manifest.getchildren(): for e in list(manifest):
if 'html' in e.attrib['media-type']: if 'html' in e.attrib['media-type']:
filename = unquote(e.attrib['href']) filename = unquote(e.attrib['href'])
filename = normpath(os.path.join(os.path.dirname(opf[0]), filename)) filename = normpath(os.path.join(os.path.dirname(opf[0]), filename))
@ -118,7 +118,7 @@ def info(epub):
metadata = info.findall('{http://www.idpf.org/2007/opf}metadata') metadata = info.findall('{http://www.idpf.org/2007/opf}metadata')
if metadata: if metadata:
metadata = metadata[0] metadata = metadata[0]
for e in metadata.getchildren(): for e in list(metadata):
if e.text and e.text.strip() and e.text not in ('unknown', 'none'): if e.text and e.text.strip() and e.text not in ('unknown', 'none'):
key = e.tag.split('}')[-1] key = e.tag.split('}')[-1]
key = { key = {
@ -148,7 +148,7 @@ def info(epub):
for point in nav_map.findall('{http://www.daisy.org/z3986/2005/ncx/}navPoint'): for point in nav_map.findall('{http://www.daisy.org/z3986/2005/ncx/}navPoint'):
label = point.find('{http://www.daisy.org/z3986/2005/ncx/}navLabel') label = point.find('{http://www.daisy.org/z3986/2005/ncx/}navLabel')
if label: if label:
txt = label.getchildren()[0].text txt = list(label)[0].text
if txt: if txt:
contents.append(txt) contents.append(txt)
if contents: if contents:

View file

@ -10,6 +10,7 @@ from glob import glob
from datetime import datetime from datetime import datetime
from PyPDF2 import PdfFileReader from PyPDF2 import PdfFileReader
from PIL import Image
import ox import ox
import settings import settings
@ -24,13 +25,13 @@ def cover(pdf):
else: else:
return page(pdf, 1) return page(pdf, 1)
def ql_cover(pdf): def ql_cover(pdf, size=1024):
tmp = tempfile.mkdtemp() tmp = tempfile.mkdtemp()
cmd = [ cmd = [
'qlmanage', 'qlmanage',
'-t', '-t',
'-s', '-s',
'1024', str(size),
'-o', '-o',
tmp, tmp,
pdf pdf
@ -48,7 +49,7 @@ def ql_cover(pdf):
shutil.rmtree(tmp) shutil.rmtree(tmp)
return data return data
def page(pdf, page): def page(pdf, page, size=1024):
tmp = tempfile.mkdtemp() tmp = tempfile.mkdtemp()
if sys.platform == 'win32': if sys.platform == 'win32':
pdf = get_short_path_name(pdf) pdf = get_short_path_name(pdf)
@ -57,7 +58,7 @@ def page(pdf, page):
pdf, pdf,
'-jpeg', '-jpeg',
'-f', str(page), '-l', str(page), '-f', str(page), '-l', str(page),
'-scale-to', '1024', '-cropbox', '-scale-to', str(size), '-cropbox',
os.path.join(tmp, 'page') os.path.join(tmp, 'page')
] ]
if sys.platform == 'win32': if sys.platform == 'win32':
@ -79,6 +80,46 @@ def page(pdf, page):
shutil.rmtree(tmp) shutil.rmtree(tmp)
return data return data
def crop(pdf, page, left, top, right, bottom):
size = 2048
tmp = tempfile.mkdtemp()
if sys.platform == 'win32':
pdf = get_short_path_name(pdf)
cmd = [
'pdftocairo',
pdf,
'-jpeg',
'-f', str(page), '-l', str(page),
'-scale-to', str(size), '-cropbox',
os.path.join(tmp, 'page')
]
if sys.platform == 'win32':
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
p = subprocess.Popen(cmd, close_fds=True, startupinfo=startupinfo)
else:
p = subprocess.Popen(cmd, close_fds=True)
p.wait()
image = glob('%s/*' % tmp)
if image:
image = image[0]
crop = [int(p) for p in (left, top, right, bottom)]
img = Image.open(image).crop(crop)
img.save(image)
with open(image, 'rb') as fd:
data = fd.read()
else:
logger.debug('pdftocairo %s %s', pdf, ' '.join(cmd))
data = None
shutil.rmtree(tmp)
return data
''' '''
def page(pdf, page): def page(pdf, page):
image = tempfile.mkstemp('.jpg')[1] image = tempfile.mkstemp('.jpg')[1]
@ -281,3 +322,4 @@ def extract_isbn(text):
isbns = find_isbns(text) isbns = find_isbns(text)
if isbns: if isbns:
return isbns[0] return isbns[0]

View file

@ -7,8 +7,9 @@ import tempfile
import subprocess import subprocess
def cover(path): def cover(path):
import settings
image = tempfile.mkstemp('.jpg')[1] image = tempfile.mkstemp('.jpg')[1]
cmd = ['python3', '../reader/txt.js/txt.py', '-i', path, '-o', image] cmd = ['python3', os.path.join(settings.top_dir, 'reader/txt.js/txt.py'), '-i', path, '-o', image]
p = subprocess.Popen(cmd, close_fds=True) p = subprocess.Popen(cmd, close_fds=True)
p.wait() p.wait()
with open(image, 'rb') as fd: with open(image, 'rb') as fd:

View file

@ -12,18 +12,20 @@ import socket
import socketserver import socketserver
import time import time
from Crypto.PublicKey import RSA
from Crypto.Util.asn1 import DerSequence
from OpenSSL.crypto import dump_privatekey, FILETYPE_ASN1
from OpenSSL.SSL import ( from OpenSSL.SSL import (
Context, Connection, TLSv1_2_METHOD, Connection,
VERIFY_PEER, VERIFY_FAIL_IF_NO_PEER_CERT, VERIFY_CLIENT_ONCE Context,
TLSv1_2_METHOD,
VERIFY_CLIENT_ONCE,
VERIFY_FAIL_IF_NO_PEER_CERT,
VERIFY_PEER,
) )
import db import db
import settings import settings
import state import state
import user import user
import utils
from changelog import changelog_size, changelog_path from changelog import changelog_size, changelog_path
from websocket import trigger_event from websocket import trigger_event
@ -34,16 +36,15 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_service_id(key): def get_service_id(connection):
''' certs = connection.get_peer_cert_chain()
service_id is the first half of the sha1 of the rsa public key encoded in base32 for cert in certs:
''' if cert.get_signature_algorithm().decode() == "ED25519":
# compute sha1 of public key and encode first half in base32 pubkey = cert.get_pubkey()
pub_der = DerSequence() public_key = pubkey.to_cryptography_key().public_bytes_raw()
pub_der.decode(dump_privatekey(FILETYPE_ASN1, key)) service_id = utils.get_onion(public_key)
public_key = RSA.construct((pub_der._seq[1], pub_der._seq[2])).exportKey('DER')[22:]
service_id = base64.b32encode(hashlib.sha1(public_key).digest()[:10]).lower().decode()
return service_id return service_id
raise Exception("connection with invalid certificate")
class TLSTCPServer(socketserver.TCPServer): class TLSTCPServer(socketserver.TCPServer):
@ -55,7 +56,7 @@ class TLSTCPServer(socketserver.TCPServer):
socketserver.TCPServer.__init__(self, server_address, HandlerClass) socketserver.TCPServer.__init__(self, server_address, HandlerClass)
ctx = Context(TLSv1_2_METHOD) ctx = Context(TLSv1_2_METHOD)
ctx.use_privatekey_file(settings.ssl_key_path) ctx.use_privatekey_file(settings.ssl_key_path)
ctx.use_certificate_file(settings.ssl_cert_path) ctx.use_certificate_chain_file(settings.ssl_cert_path)
# only allow clients with cert: # only allow clients with cert:
ctx.set_verify(VERIFY_PEER | VERIFY_CLIENT_ONCE | VERIFY_FAIL_IF_NO_PEER_CERT, self._accept) ctx.set_verify(VERIFY_PEER | VERIFY_CLIENT_ONCE | VERIFY_FAIL_IF_NO_PEER_CERT, self._accept)
#ctx.set_verify(VERIFY_PEER | VERIFY_CLIENT_ONCE, self._accept) #ctx.set_verify(VERIFY_PEER | VERIFY_CLIENT_ONCE, self._accept)
@ -111,8 +112,7 @@ class Handler(http.server.SimpleHTTPRequestHandler):
return self.do_GET() return self.do_GET()
def do_GET(self): def do_GET(self):
#x509 = self.connection.get_peer_certificate() user_id = get_service_id(self.connection)
#user_id = get_service_id(x509.get_pubkey()) if x509 else None
import item.models import item.models
parts = self.path.split('/') parts = self.path.split('/')
if len(parts) == 3 and parts[1] in ('get', 'preview'): if len(parts) == 3 and parts[1] in ('get', 'preview'):
@ -185,8 +185,7 @@ class Handler(http.server.SimpleHTTPRequestHandler):
self.end_headers() self.end_headers()
def _changelog(self): def _changelog(self):
x509 = self.connection.get_peer_certificate() user_id = get_service_id(self.connection)
user_id = get_service_id(x509.get_pubkey()) if x509 else None
with db.session(): with db.session():
u = user.models.User.get(user_id) u = user.models.User.get(user_id)
if not u: if not u:
@ -257,8 +256,7 @@ class Handler(http.server.SimpleHTTPRequestHandler):
ping responds public ip ping responds public ip
''' '''
x509 = self.connection.get_peer_certificate() user_id = get_service_id(self.connection)
user_id = get_service_id(x509.get_pubkey()) if x509 else None
content = {} content = {}
try: try:

View file

@ -448,6 +448,9 @@ class Node(Thread):
except socket.timeout: except socket.timeout:
logger.debug('timeout %s', url) logger.debug('timeout %s', url)
return False return False
except urllib.error.URLError as e:
logger.debug('urllib.error.URLError %s', e)
return False
except socks.GeneralProxyError: except socks.GeneralProxyError:
logger.debug('download failed %s', url) logger.debug('download failed %s', url)
return False return False
@ -598,6 +601,8 @@ class Nodes(Thread):
def _pull(self): def _pull(self):
if not state.sync_enabled or settings.preferences.get('downloadRate') == 0: if not state.sync_enabled or settings.preferences.get('downloadRate') == 0:
return return
if state.sync_db:
return
if state.activity and state.activity.get('activity') == 'import': if state.activity and state.activity.get('activity') == 'import':
return return
self._pulling = True self._pulling = True
@ -617,12 +622,12 @@ class Nodes(Thread):
node.pullChanges() node.pullChanges()
self._pulling = False self._pulling = False
def join(self): async def join(self):
self._q.put(None) self._q.put(None)
for node in list(self._nodes.values()): for node in list(self._nodes.values()):
node.join() node.join()
if self.local: if self.local:
self.local.close() await self.local.close()
return super().join(1) return super().join(1)
def publish_node(): def publish_node():

View file

@ -83,6 +83,15 @@ class ApiHandler(tornado.web.RequestHandler):
context = self._context context = self._context
if context is None: if context is None:
context = defaultcontext context = defaultcontext
action = None
if request.headers.get('Content-Type') == 'application/json':
try:
r = json.loads(request.body.decode())
action = r['action']
data = r['data']
except:
action = None
if not action:
action = request.arguments.get('action', [None])[0].decode('utf-8') action = request.arguments.get('action', [None])[0].decode('utf-8')
data = request.arguments.get('data', [b'{}'])[0] data = request.arguments.get('data', [b'{}'])[0]
data = json.loads(data.decode('utf-8')) if data else {} data = json.loads(data.decode('utf-8')) if data else {}

View file

@ -2,9 +2,10 @@
from datetime import datetime from datetime import datetime
import unicodedata import unicodedata
import sqlalchemy.orm.exc
from sqlalchemy.sql import operators from sqlalchemy.sql import operators
from sqlalchemy.orm import load_only from sqlalchemy.orm import load_only
from sqlalchemy.sql.expression import text from sqlalchemy.sql.expression import text, column
import utils import utils
import settings import settings
@ -13,6 +14,7 @@ from fulltext import find_fulltext
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_operator(op, type='str'): def get_operator(op, type='str'):
return { return {
'str': { 'str': {
@ -134,7 +136,8 @@ class Parser(object):
q = get_operator(op)(self._find.findvalue, v) q = get_operator(op)(self._find.findvalue, v)
if k != '*': if k != '*':
q &= (self._find.key == k) q &= (self._find.key == k)
ids = self._model.query.join(self._find).filter(q).options(load_only('id')) ids = self._find.query.filter(q).with_entities(column('item_id'))
ids = [i[0] for i in ids]
return self.in_ids(ids, exclude) return self.in_ids(ids, exclude)
elif k == 'list': elif k == 'list':
nickname, name = v.split(':', 1) nickname, name = v.split(':', 1)
@ -268,5 +271,4 @@ class Parser(object):
data.get('query', {}).get('operator', '&')) data.get('query', {}).get('operator', '&'))
for c in conditions: for c in conditions:
qs = qs.filter(c) qs = qs.filter(c)
#print(qs)
return qs return qs

View file

@ -1,16 +1,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import asyncio
import os import os
import sys import sys
import signal import signal
import time import time
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
from tornado.web import StaticFileHandler, Application import tornado.web
from tornado.web import Application
from cache import Cache from cache import Cache
from item.handlers import EpubHandler, ReaderHandler, FileHandler from item.handlers import EpubHandler, ReaderHandler, FileHandler
from item.handlers import OMLHandler, UploadHandler from item.handlers import OMLHandler, UploadHandler
from item.handlers import CropHandler
from item.icons import IconHandler from item.icons import IconHandler
import db import db
import node.server import node.server
@ -28,6 +31,12 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class StaticFileHandler(tornado.web.StaticFileHandler):
def get_content_type(self):
if self.request.path.split('?')[0].endswith('.mjs'):
return 'application/javascript'
return super().get_content_type()
class MainHandler(OMLHandler): class MainHandler(OMLHandler):
def get(self, path): def get(self, path):
@ -59,13 +68,13 @@ def log_request(handler):
log_method("%d %s %.2fms", handler.get_status(), log_method("%d %s %.2fms", handler.get_status(),
handler._request_summary(), request_time) handler._request_summary(), request_time)
def shutdown(): async def shutdown():
state.shutdown = True state.shutdown = True
if state.tor: if state.tor:
state.tor._shutdown = True state.tor._shutdown = True
if state.nodes: if state.nodes:
logger.debug('shutdown nodes') logger.debug('shutdown nodes')
state.nodes.join() await state.nodes.join()
if state.downloads: if state.downloads:
logger.debug('shutdown downloads') logger.debug('shutdown downloads')
state.downloads.join() state.downloads.join()
@ -111,11 +120,11 @@ def run():
common_handlers = [ common_handlers = [
(r'/(favicon.ico)', StaticFileHandler, {'path': settings.static_path}), (r'/(favicon.ico)', StaticFileHandler, {'path': settings.static_path}),
(r'/static/oxjs/(.*)', StaticFileHandler, {'path': os.path.join(settings.base_dir, '..', 'oxjs')}), (r'/static/oxjs/(.*)', StaticFileHandler, {'path': os.path.join(settings.top_dir, 'oxjs')}),
(r'/static/cbr.js/(.*)', StaticFileHandler, {'path': os.path.join(settings.base_dir, '..', 'reader', 'cbr.js')}), (r'/static/cbr.js/(.*)', StaticFileHandler, {'path': os.path.join(settings.top_dir, 'reader', 'cbr.js')}),
(r'/static/epub.js/(.*)', StaticFileHandler, {'path': os.path.join(settings.base_dir, '..', 'reader', 'epub.js')}), (r'/static/epub.js/(.*)', StaticFileHandler, {'path': os.path.join(settings.top_dir, 'reader', 'epub.js')}),
(r'/static/pdf.js/(.*)', StaticFileHandler, {'path': os.path.join(settings.base_dir, '..', 'reader', 'pdf.js')}), (r'/static/pdf.js/(.*)', StaticFileHandler, {'path': os.path.join(settings.top_dir, 'reader', 'pdf.js')}),
(r'/static/txt.js/(.*)', StaticFileHandler, {'path': os.path.join(settings.base_dir, '..', 'reader', 'txt.js')}), (r'/static/txt.js/(.*)', StaticFileHandler, {'path': os.path.join(settings.top_dir, 'reader', 'txt.js')}),
(r'/static/(.*)', StaticFileHandler, {'path': settings.static_path}), (r'/static/(.*)', StaticFileHandler, {'path': settings.static_path}),
(r'/(.*)/epub/(.*)', EpubHandler), (r'/(.*)/epub/(.*)', EpubHandler),
(r'/(.*?)/reader/', ReaderHandler), (r'/(.*?)/reader/', ReaderHandler),
@ -125,6 +134,7 @@ def run():
(r'/(.*?)/get/', FileHandler, { (r'/(.*?)/get/', FileHandler, {
'attachment': True 'attachment': True
}), }),
(r'/(.*)/2048p(\d*),(\d*),(\d*),(\d*),(\d*).jpg', CropHandler),
(r'/(.*)/(cover|preview)(\d*).jpg', IconHandler), (r'/(.*)/(cover|preview)(\d*).jpg', IconHandler),
] ]
handlers = common_handlers + [ handlers = common_handlers + [
@ -146,13 +156,12 @@ def run():
http_server.listen(settings.server['port'], settings.server['address'], max_buffer_size=max_buffer_size) http_server.listen(settings.server['port'], settings.server['address'], max_buffer_size=max_buffer_size)
# public server # public server
''' if settings.preferences.get('enableReadOnlyService'):
public_port = settings.server.get('public_port') public_port = settings.server.get('public_port')
public_address = settings.server['public_address'] public_address = settings.server['public_address']
if public_port: if public_port:
public_server = Application(public_handlers, **options) public_server = Application(public_handlers, **options)
public_server.listen(public_port, public_address) public_server.listen(public_port, public_address)
'''
if PID: if PID:
with open(PID, 'w') as pid: with open(PID, 'w') as pid:
@ -198,10 +207,10 @@ def run():
print('open browser at %s' % url) print('open browser at %s' % url)
logger.debug('Starting OML %s at %s', settings.VERSION, url) logger.debug('Starting OML %s at %s', settings.VERSION, url)
signal.signal(signal.SIGTERM, shutdown) signal.signal(signal.SIGTERM, lambda _, __: sys.exit(0))
try: try:
state.main.start() state.main.start()
except: except:
print('shutting down...') print('shutting down...')
shutdown() asyncio.run(shutdown())

View file

@ -8,15 +8,17 @@ from oml.utils import get_user_id
from oml import fulltext from oml import fulltext
base_dir = os.path.normpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..')) base_dir = os.path.normpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..'))
top_dir = os.path.dirname(base_dir)
static_path = os.path.join(base_dir, 'static') static_path = os.path.join(base_dir, 'static')
updates_path = os.path.normpath(os.path.join(base_dir, '..', 'updates')) updates_path = os.path.normpath(os.path.join(top_dir, 'updates'))
oml_data_path = os.path.join(base_dir, 'config.json') oml_data_path = os.path.join(base_dir, 'config.json')
data_path = os.path.normpath(os.path.join(base_dir, '..', 'data')) data_path = os.path.normpath(os.path.join(top_dir, 'data'))
if not os.path.exists(data_path): if not os.path.exists(data_path):
config_path = os.path.normpath(os.path.join(base_dir, '..', 'config')) config_path = os.path.normpath(os.path.join(top_dir, 'config'))
if os.path.exists(config_path): if os.path.exists(config_path):
data_path = config_path data_path = config_path
else: else:
@ -24,9 +26,11 @@ if not os.path.exists(data_path):
db_path = os.path.join(data_path, 'data.db') db_path = os.path.join(data_path, 'data.db')
log_path = os.path.join(data_path, 'debug.log') log_path = os.path.join(data_path, 'debug.log')
ssl_cert_path = os.path.join(data_path, 'node.ssl.crt')
ssl_key_path = os.path.join(data_path, 'tor', 'private_key')
ca_key_path = os.path.join(data_path, 'node.ca.key')
ca_cert_path = os.path.join(data_path, 'node.ca.crt')
ssl_cert_path = os.path.join(data_path, 'node.tls.crt')
ssl_key_path = os.path.join(data_path, 'node.tls.key')
if os.path.exists(oml_data_path): if os.path.exists(oml_data_path):
with open(oml_data_path) as fd: with open(oml_data_path) as fd:
@ -57,7 +61,7 @@ for key in server_defaults:
release = pdict(os.path.join(data_path, 'release.json')) release = pdict(os.path.join(data_path, 'release.json'))
USER_ID = get_user_id(ssl_key_path, ssl_cert_path) USER_ID = get_user_id(ssl_key_path, ssl_cert_path, ca_key_path, ca_cert_path)
OML_UPDATE_KEY = 'K55EZpPYbP3X+3mA66cztlw1sSaUMqGwfTDKQyP2qOU' OML_UPDATE_KEY = 'K55EZpPYbP3X+3mA66cztlw1sSaUMqGwfTDKQyP2qOU'
OML_UPDATE_CERT = '''-----BEGIN CERTIFICATE----- OML_UPDATE_CERT = '''-----BEGIN CERTIFICATE-----
@ -96,3 +100,5 @@ if not FULLTEXT_SUPPORT:
config['itemKeys'] = [k for k in config['itemKeys'] if k['id'] != 'fulltext'] config['itemKeys'] = [k for k in config['itemKeys'] if k['id'] != 'fulltext']
DB_VERSION = 20 DB_VERSION = 20
ID_LENGTH = 56

View file

@ -423,6 +423,27 @@ def upgrade_db(old, new=None):
)''') )''')
run_sql('CREATE UNIQUE INDEX IF NOT EXISTS user_metadata_index ON user_metadata(id, user_id)') run_sql('CREATE UNIQUE INDEX IF NOT EXISTS user_metadata_index ON user_metadata(id, user_id)')
run_sql('CREATE INDEX ix_user_metadata_data_hash ON user_metadata (data_hash)') run_sql('CREATE INDEX ix_user_metadata_data_hash ON user_metadata (data_hash)')
if old <= '20240608-1469-647a8b9':
old_hostname = os.path.join(settings.data_path, 'tor/hostname')
if os.path.exists(old_hostname):
with open(old_hostname) as fd:
OLD_USER_ID = fd.read().split('.')[0]
statements = [
"UPDATE user SET id = '{nid}' WHERE id = '{oid}'",
"UPDATE list SET user_id = '{nid}' WHERE user_id = '{oid}'",
"UPDATE useritem SET user_id = '{nid}' WHERE user_id = '{oid}'",
"UPDATE changelog SET user_id = '{nid}' WHERE user_id = '{oid}'",
]
run_sql([
sql.format(oid=OLD_USER_ID, nid=settings.USER_ID)
for sql in statements
])
for ext in ('log', 'db', 'json'):
old_log = os.path.join(settings.data_path, 'peers/%s.%s' % (OLD_USER_ID, ext))
new_log = os.path.join(settings.data_path, 'peers/%s.%s' % (USER_ID, ext))
if os.path.exists(old_log) and not os.path.exists(new_log):
os.rename(old_log, new_log)
def create_default_lists(user_id=None): def create_default_lists(user_id=None):
with db.session(): with db.session():

View file

@ -16,6 +16,7 @@ websockets = []
uisockets = [] uisockets = []
peers = {} peers = {}
changelog_size = None changelog_size = None
sync_db = False
activity = {} activity = {}
removepeer = {} removepeer = {}

View file

@ -36,8 +36,7 @@ class Tasks(Thread):
def run(self): def run(self):
self.load_tasks() self.load_tasks()
if time.mktime(time.gmtime()) - settings.server.get('last_scan', 0) > 24*60*60: if (time.mktime(time.gmtime()) - settings.server.get('last_scan', 0)) > 24*60*60:
settings.server['last_scan'] = time.mktime(time.gmtime())
self.queue('scan') self.queue('scan')
import item.scan import item.scan
@ -98,6 +97,7 @@ class Tasks(Thread):
def load_tasks(self): def load_tasks(self):
if os.path.exists(self._taskspath): if os.path.exists(self._taskspath):
logger.debug('loading tasks')
try: try:
with open(self._taskspath) as f: with open(self._taskspath) as f:
tasks = json.load(f) tasks = json.load(f)

View file

@ -22,9 +22,11 @@ import logging
logging.getLogger('stem').setLevel(logging.ERROR) logging.getLogger('stem').setLevel(logging.ERROR)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class TorDaemon(Thread): class TorDaemon(Thread):
installing = False installing = False
running = True running = True
ended = False
p = None p = None
def __init__(self): def __init__(self):
@ -105,6 +107,8 @@ DirReqStatistics 0
logger.debug(line) logger.debug(line)
self.p.communicate() self.p.communicate()
time.sleep(0.5) time.sleep(0.5)
self.ended = True
self.running = False
self.p = None self.p = None
def kill(self): def kill(self):
@ -142,6 +146,10 @@ class Tor(object):
logger.debug("Start tor") logger.debug("Start tor")
self.daemon = TorDaemon() self.daemon = TorDaemon()
return self.connect() return self.connect()
elif self.daemon.ended:
logger.debug("Try starting tor again")
self.daemon = TorDaemon()
return self.connect()
if not self.daemon.installing: if not self.daemon.installing:
logger.debug("Failed to connect to tor") logger.debug("Failed to connect to tor")
return False return False
@ -201,18 +209,20 @@ class Tor(object):
return False return False
controller = self.controller controller = self.controller
if controller.get_version() >= stem.version.Requirement.ADD_ONION: if controller.get_version() >= stem.version.Requirement.ADD_ONION:
with open(settings.ssl_key_path, 'rb') as fd: private_key, public_key = utils.load_pem_key(settings.ca_key_path)
private_key = fd.read() key_type, key_content = utils.get_onion_key(private_key)
key_content = RSA.importKey(private_key).exportKey().decode()
key_content = ''.join(key_content.strip().split('\n')[1:-1])
ports = {9851: settings.server['node_port']} ports = {9851: settings.server['node_port']}
if settings.preferences.get('enableReadOnlyService'): if settings.preferences.get('enableReadOnlyService'):
ports[80] = settings.server['public_port'] ports[80] = settings.server['public_port']
controller.remove_ephemeral_hidden_service(settings.USER_ID) controller.remove_ephemeral_hidden_service(settings.USER_ID)
response = controller.create_ephemeral_hidden_service(ports, response = controller.create_ephemeral_hidden_service(
key_type='RSA1024', key_content=key_content, ports,
detached=True) key_type=key_type, key_content=key_content,
detached=True
)
if response.is_ok(): if response.is_ok():
if response.service_id != settings.USER_ID:
logger.error("Something is wrong with tor id %s vs %s", response.service_id, settings.USER_ID)
logger.debug('published node as https://%s.onion:%s', logger.debug('published node as https://%s.onion:%s',
settings.USER_ID, settings.server_defaults['node_port']) settings.USER_ID, settings.server_defaults['node_port'])
if settings.preferences.get('enableReadOnlyService'): if settings.preferences.get('enableReadOnlyService'):
@ -259,48 +269,54 @@ def torbrowser_url(sys_platform=None):
data = read_url(base_url, timeout=3*24*60*60).decode() data = read_url(base_url, timeout=3*24*60*60).decode()
versions = [] versions = []
for r in ( for r in (
re.compile('href="(\d\.\d\.\d/)"'), re.compile('href="(\d+\.\d+\.\d+/)"'),
re.compile('href="(\d\.\d/)"'), re.compile('href="(\d+\.\d+/)"'),
): ):
versions += r.findall(data) versions += r.findall(data)
if not versions:
return None
current = sorted(versions)[-1] current = sorted(versions)[-1]
url = base_url + current url = base_url + current
language = '.*?en'
if sys_platform.startswith('linux'): if sys_platform.startswith('linux'):
if platform.architecture()[0] == '64bit': if platform.architecture()[0] == '64bit':
osname = 'linux64' osname = 'linux-x86_64'
else: else:
osname = 'linux32' osname = 'linux-x86_32'
ext = 'xz' ext = 'xz'
elif sys_platform == 'darwin': elif sys_platform == 'darwin':
osname = 'osx64' osname = 'macos'
ext = 'dmg' ext = 'dmg'
elif sys_platform == 'win32': elif sys_platform == 'win32':
language = '' osname = 'windows-x86_64-portable'
osname = '' ext = 'exe'
ext = 'zip'
else: else:
logger.debug('no way to get torbrowser url for %s', sys.platform) logger.debug('no way to get torbrowser url for %s', sys.platform)
return None return None
r = re.compile('href="(.*?{osname}{language}.*?{ext})"'.format(osname=osname,language=language,ext=ext)) data = read_url(url).decode()
torbrowser = sorted(r.findall(read_url(url).decode()))[-1] r = re.compile('href="(.*?{osname}.*?{ext})"'.format(osname=osname, ext=ext)).findall(data)
if not r:
r = re.compile('href="(.*?{ext})"'.format(ext=ext)).findall(data)
torbrowser = sorted(r)[-1]
url += torbrowser url += torbrowser
return url return url
def get_tor(): def get_tor():
if sys.platform == 'darwin': if sys.platform == 'darwin':
for path in ( for path in (
os.path.join(settings.base_dir, '..', 'platform_darwin64', 'tor', 'tor'), os.path.join(settings.top_dir, 'platform_darwin64', 'tor', 'tor'),
'/Applications/TorBrowser.app/TorBrowser/Tor/tor', '/Applications/TorBrowser.app/TorBrowser/Tor/tor',
os.path.join(settings.base_dir, '..', 'tor', 'TorBrowser.app', 'TorBrowser', 'Tor', 'tor') os.path.join(settings.top_dir, 'tor', 'TorBrowser.app', 'TorBrowser', 'Tor', 'tor')
): ):
if os.path.isfile(path) and os.access(path, os.X_OK): if os.path.isfile(path) and os.access(path, os.X_OK):
return path return path
elif sys.platform == 'win32': elif sys.platform == 'win32':
paths = [ paths = [
os.path.join(settings.base_dir, '..', 'platform_win32', 'tor', 'tor.exe') os.path.join(settings.top_dir, 'platform_win32', 'tor', 'tor.exe')
] ]
exe = os.path.join('Tor Browser', 'Browser', 'TorBrowser', 'Tor', 'tor.exe') for exe in (
os.path.join('Tor Browser', 'Browser', 'TorBrowser', 'Tor', 'tor.exe'),
os.path.join('Tor Browser', 'Browser', 'TorBrowser', 'Tor', 'Tor', 'tor.exe'),
):
for prefix in ( for prefix in (
os.path.join(os.path.expanduser('~'), 'Desktop'), os.path.join(os.path.expanduser('~'), 'Desktop'),
os.path.join('C:', 'Program Files'), os.path.join('C:', 'Program Files'),
@ -308,16 +324,16 @@ def get_tor():
): ):
path = os.path.join(prefix, exe) path = os.path.join(prefix, exe)
paths.append(path) paths.append(path)
paths.append(os.path.join(settings.base_dir, '..', 'tor', 'Tor', 'tor.exe')) paths.append(os.path.join(settings.top_dir, 'tor', 'Tor', 'tor.exe'))
for path in paths: for path in paths:
if os.path.isfile(path) and os.access(path, os.X_OK): if os.path.isfile(path) and os.access(path, os.X_OK):
return os.path.normpath(path) return os.path.normpath(path)
elif sys.platform.startswith('linux'): elif sys.platform.startswith('linux'):
for path in ( for path in (
os.path.join(settings.base_dir, '..', 'platform_linux64', 'tor', 'tor'), os.path.join(settings.top_dir, 'platform_linux64', 'tor', 'tor'),
os.path.join(settings.base_dir, '..', 'platform_linux32', 'tor', 'tor'), os.path.join(settings.top_dir, 'platform_linux32', 'tor', 'tor'),
os.path.join(settings.base_dir, '..', 'platform_linux_armv7l', 'tor', 'tor'), os.path.join(settings.top_dir, 'platform_linux_armv7l', 'tor', 'tor'),
os.path.join(settings.base_dir, '..', 'platform_linux_aarch64', 'tor', 'tor'), os.path.join(settings.top_dir, 'platform_linux_aarch64', 'tor', 'tor'),
): ):
if os.path.isfile(path) and os.access(path, os.X_OK): if os.path.isfile(path) and os.access(path, os.X_OK):
return os.path.normpath(path) return os.path.normpath(path)
@ -331,9 +347,12 @@ def get_tor():
path = os.path.join(base, 'TorBrowser', 'Tor', 'tor') path = os.path.join(base, 'TorBrowser', 'Tor', 'tor')
if os.path.isfile(path) and os.access(path, os.X_OK): if os.path.isfile(path) and os.access(path, os.X_OK):
return path return path
path = os.path.join(base, 'TorBrowser', 'Tor', 'Tor', 'tor')
if os.path.isfile(path) and os.access(path, os.X_OK):
return path
except: except:
pass pass
local_tor = os.path.normpath(os.path.join(settings.base_dir, '..', local_tor = os.path.normpath(os.path.join(settings.top_dir,
'tor', 'tor-browser_en-US', 'Browser', 'TorBrowser', 'Tor', 'tor')) 'tor', 'tor-browser_en-US', 'Browser', 'TorBrowser', 'Tor', 'tor'))
if os.path.exists(local_tor): if os.path.exists(local_tor):
return local_tor return local_tor
@ -342,7 +361,7 @@ def get_tor():
def get_geoip(tor): def get_geoip(tor):
geo = [] geo = []
for tordir in ( for tordir in (
os.path.normpath(os.path.join(settings.base_dir, '..', 'platform', 'tor')), os.path.normpath(os.path.join(settings.top_dir, 'platform', 'tor')),
os.path.join(os.path.dirname(os.path.dirname(tor)), 'Data', 'Tor') os.path.join(os.path.dirname(os.path.dirname(tor)), 'Data', 'Tor')
): ):
gepipfile = os.path.join(tordir, 'geoip') gepipfile = os.path.join(tordir, 'geoip')
@ -364,7 +383,7 @@ def install_tor():
logger.debug('found existing tor installation') logger.debug('found existing tor installation')
return return
url = torbrowser_url() url = torbrowser_url()
target = os.path.normpath(os.path.join(settings.base_dir, '..', 'tor')) target = os.path.normpath(os.path.join(settings.top_dir, 'tor'))
if url: if url:
logger.debug('downloading and installing tor') logger.debug('downloading and installing tor')
if sys.platform.startswith('linux'): if sys.platform.startswith('linux'):

View file

@ -40,6 +40,9 @@ def create_tor_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
proto = 6 proto = 6
sa = address sa = address
sock = None sock = None
logger.debug('make tor connection to: %s', address)
try: try:
sock = socks.socksocket(af, socktype, proto) sock = socks.socksocket(af, socktype, proto)
if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT: if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:
@ -66,27 +69,30 @@ class TorHTTPSConnection(http.client.HTTPSConnection):
def __init__(self, host, port=None, service_id=None, check_hostname=None, context=None, **kwargs): def __init__(self, host, port=None, service_id=None, check_hostname=None, context=None, **kwargs):
self._service_id = service_id self._service_id = service_id
if self._service_id: if self._service_id:
if hasattr(ssl, '_create_default_https_context'): context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
context = ssl._create_default_https_context()
elif hasattr(ssl, '_create_stdlib_context'):
context = ssl._create_stdlib_context()
if context: if context:
context.check_hostname = False context.check_hostname = False
context.verify_mode = ssl.CERT_NONE context.verify_mode = ssl.CERT_NONE
# tor keys are still 1024 bit, debian started to require 2048 by default,
# try to lower requirements to 1024 if needed
try:
context.load_cert_chain(settings.ssl_cert_path, settings.ssl_key_path)
except ssl.SSLError:
context.set_ciphers('DEFAULT@SECLEVEL=1')
context.load_cert_chain(settings.ssl_cert_path, settings.ssl_key_path) context.load_cert_chain(settings.ssl_cert_path, settings.ssl_key_path)
context.load_default_certs() context.load_default_certs()
context.set_alpn_protocols(['http/1.1'])
context.post_handshake_auth = True
http.client.HTTPSConnection.__init__(self, host, port, http.client.HTTPSConnection.__init__(self, host, port,
check_hostname=check_hostname, context=context, **kwargs) check_hostname=check_hostname, context=context, **kwargs)
if not is_local(host): if not is_local(host):
self._create_connection = create_tor_connection self._create_connection = create_tor_connection
def get_service_id_cert(self):
for cert in self.sock._sslobj.get_verified_chain():
info = cert.get_info()
subject = info.get("subject")
if subject:
CN = subject[0][0][1]
if CN == self._service_id:
cert = cert.public_bytes()
return cert
def _check_service_id(self, cert): def _check_service_id(self, cert):
service_id = get_service_id(cert=cert) service_id = get_service_id(cert=cert)
if service_id != self._service_id: if service_id != self._service_id:
@ -96,11 +102,9 @@ class TorHTTPSConnection(http.client.HTTPSConnection):
def connect(self): def connect(self):
http.client.HTTPSConnection.connect(self) http.client.HTTPSConnection.connect(self)
if self._service_id: if self._service_id:
cert = self.sock.getpeercert(binary_form=True) cert = self.get_service_id_cert()
if not self._check_service_id(cert): if not self._check_service_id(cert):
raise InvalidCertificateException(self._service_id, cert, raise InvalidCertificateException(self._service_id, cert, 'service_id mismatch')
'service_id mismatch')
#logger.debug('CIPHER %s VERSION %s', self.sock.cipher(), self.sock.ssl_version)
class TorHTTPSHandler(urllib.request.HTTPSHandler): class TorHTTPSHandler(urllib.request.HTTPSHandler):
def __init__(self, debuglevel=0, context=None, check_hostname=None, service_id=None): def __init__(self, debuglevel=0, context=None, check_hostname=None, service_id=None):

View file

@ -411,7 +411,7 @@ def requestPeering(data):
nickname (optional) nickname (optional)
} }
''' '''
if len(data.get('id', '')) != 16: if len(data.get('id', '')) != settings.ID_LENGTH:
logger.debug('invalid user id') logger.debug('invalid user id')
return {} return {}
u = models.User.get_or_create(data['id']) u = models.User.get_or_create(data['id'])
@ -434,7 +434,7 @@ def acceptPeering(data):
message message
} }
''' '''
if len(data.get('id', '')) != 16: if len(data.get('id', '')) != settings.ID_LENGTH:
logger.debug('invalid user id') logger.debug('invalid user id')
return {} return {}
logger.debug('acceptPeering... %s', data) logger.debug('acceptPeering... %s', data)
@ -453,8 +453,8 @@ def rejectPeering(data):
message message
} }
''' '''
if len(data.get('id', '')) not in (16, 43): if len(data.get('id', '')) not in (16, 43, 56):
logger.debug('invalid user id') logger.debug('invalid user id: %s', data)
return {} return {}
u = models.User.get_or_create(data['id']) u = models.User.get_or_create(data['id'])
u.info['message'] = data.get('message', '') u.info['message'] = data.get('message', '')
@ -471,8 +471,8 @@ def removePeering(data):
message message
} }
''' '''
if len(data.get('id', '')) not in (16, 43): if len(data.get('id', '')) not in (16, 43, 56):
logger.debug('invalid user id') logger.debug('invalid user id: %s', data)
return {} return {}
u = models.User.get(data['id'], for_update=True) u = models.User.get(data['id'], for_update=True)
if u: if u:
@ -488,8 +488,8 @@ def cancelPeering(data):
takes { takes {
} }
''' '''
if len(data.get('id', '')) != 16: if len(data.get('id', '')) != settings.ID_LENGTH:
logger.debug('invalid user id') logger.debug('invalid user id: %s', data)
return {} return {}
u = models.User.get_or_create(data['id']) u = models.User.get_or_create(data['id'])
u.info['message'] = data.get('message', '') u.info['message'] = data.get('message', '')

View file

@ -27,7 +27,7 @@ class User(db.Model):
created = sa.Column(sa.DateTime()) created = sa.Column(sa.DateTime())
modified = sa.Column(sa.DateTime()) modified = sa.Column(sa.DateTime())
id = sa.Column(sa.String(43), primary_key=True) id = sa.Column(sa.String(128), primary_key=True)
info = sa.Column(MutableDict.as_mutable(sa.PickleType(pickler=json_pickler))) info = sa.Column(MutableDict.as_mutable(sa.PickleType(pickler=json_pickler)))
nickname = sa.Column(sa.String(256), index=True) nickname = sa.Column(sa.String(256), index=True)
@ -256,7 +256,7 @@ class List(db.Model):
type = sa.Column(sa.String(64)) type = sa.Column(sa.String(64))
_query = sa.Column('query', MutableDict.as_mutable(sa.PickleType(pickler=json_pickler))) _query = sa.Column('query', MutableDict.as_mutable(sa.PickleType(pickler=json_pickler)))
user_id = sa.Column(sa.String(43), sa.ForeignKey('user.id')) user_id = sa.Column(sa.String(128), sa.ForeignKey('user.id'))
user = sa.orm.relationship('User', backref=sa.orm.backref('lists', lazy='dynamic')) user = sa.orm.relationship('User', backref=sa.orm.backref('lists', lazy='dynamic'))
items = sa.orm.relationship('Item', secondary=list_items, items = sa.orm.relationship('Item', secondary=list_items,
@ -456,7 +456,7 @@ class Metadata(db.Model):
id = sa.Column(sa.Integer(), primary_key=True) id = sa.Column(sa.Integer(), primary_key=True)
item_id = sa.Column(sa.String(32)) item_id = sa.Column(sa.String(32))
user_id = sa.Column(sa.String(43), sa.ForeignKey('user.id')) user_id = sa.Column(sa.String(128), sa.ForeignKey('user.id'))
data_hash = sa.Column(sa.String(40), index=True) data_hash = sa.Column(sa.String(40), index=True)
data = sa.Column(MutableDict.as_mutable(sa.PickleType(pickler=json_pickler))) data = sa.Column(MutableDict.as_mutable(sa.PickleType(pickler=json_pickler)))

View file

@ -5,6 +5,7 @@ from datetime import datetime
from io import StringIO, BytesIO from io import StringIO, BytesIO
from PIL import Image, ImageFile from PIL import Image, ImageFile
import base64 import base64
import functools
import hashlib import hashlib
import json import json
import os import os
@ -17,19 +18,26 @@ import time
import unicodedata import unicodedata
import ox import ox
import OpenSSL.crypto
from OpenSSL.crypto import ( from OpenSSL.crypto import (
load_privatekey, load_certificate, dump_certificate,
dump_privatekey, dump_certificate, dump_privatekey,
FILETYPE_ASN1, FILETYPE_PEM, PKey, TYPE_RSA, FILETYPE_PEM,
X509, X509Extension load_certificate,
load_privatekey,
PKey,
TYPE_RSA,
X509,
X509Extension
) )
from Crypto.PublicKey import RSA from cryptography.hazmat.primitives import serialization
from Crypto.Util.asn1 import DerSequence from cryptography.hazmat.primitives.asymmetric import ed25519
from meta.utils import normalize_isbn, find_isbns, get_language, to_isbn13 from meta.utils import normalize_isbn, find_isbns, get_language, to_isbn13
from win32utils import get_short_path_name from win32utils import get_short_path_name
import logging import logging
logging.getLogger('PIL').setLevel(logging.ERROR) logging.getLogger('PIL').setLevel(logging.ERROR)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -92,7 +100,7 @@ def resize_image(data, width=None, size=None):
height = max(height, 1) height = max(height, 1)
if width < source_width: if width < source_width:
resize_method = Image.ANTIALIAS resize_method = Image.LANCZOS
else: else:
resize_method = Image.BICUBIC resize_method = Image.BICUBIC
output = source.resize((width, height), resize_method) output = source.resize((width, height), resize_method)
@ -119,78 +127,157 @@ def get_position_by_id(list, key):
return i return i
return -1 return -1
def get_user_id(private_key, cert_path): def sign_cert(cert, key):
if os.path.exists(private_key): # pyOpenSSL sgin api does not allow NULL hash
with open(private_key) as fd: # return cert.sign(key, None)
key = load_privatekey(FILETYPE_PEM, fd.read()) return OpenSSL.crypto._lib.X509_sign(cert._x509, key._pkey, OpenSSL.crypto._ffi.NULL)
if key.bits() != 1024:
os.unlink(private_key) def load_pem_key(pem):
with open(pem) as fd:
ca_key_pem = fd.read()
key = load_privatekey(FILETYPE_PEM, ca_key_pem)
if key.bits() != 256:
raise Exception("Invalid key %s" % pem)
key = key.to_cryptography_key()
private_key = key.private_bytes_raw()
public_key = key.public_key().public_bytes_raw()
return private_key, public_key
def expand_private_key(secret_key) -> bytes:
hash = hashlib.sha512(secret_key).digest()
hash = bytearray(hash)
hash[0] &= 248
hash[31] &= 127
hash[31] |= 64
return bytes(hash)
def get_onion(pubkey):
version_byte = b"\x03"
checksum_str = ".onion checksum".encode()
checksum = hashlib.sha3_256(checksum_str + pubkey + version_byte).digest()[:2]
return base64.b32encode(pubkey + checksum + version_byte).decode().lower()
def get_onion_key(private_key):
onion_key = expand_private_key(private_key)
key_type = 'ED25519-V3'
key_content = base64.encodebytes(onion_key).decode().strip().replace('\n', '')
return key_type, key_content
def get_user_id(key_path, cert_path, ca_key_path, ca_cert_path):
if os.path.exists(ca_key_path):
try:
private_key, public_key = load_pem_key(ca_key_path)
except:
os.unlink(ca_key_path)
else: else:
user_id = get_service_id(private_key) user_id = get_onion(public_key)
if not os.path.exists(private_key):
if os.path.exists(cert_path): if not os.path.exists(ca_key_path):
os.unlink(cert_path) private_key = ed25519.Ed25519PrivateKey.generate()
folder = os.path.dirname(private_key) private_bytes = private_key.private_bytes(
if not os.path.exists(folder): encoding=serialization.Encoding.PEM,
os.makedirs(folder) format=serialization.PrivateFormat.PKCS8,
os.chmod(folder, 0o700) encryption_algorithm=serialization.NoEncryption()
key = PKey() )
key.generate_key(TYPE_RSA, 1024) with open(ca_key_path, 'wb') as fd:
with open(private_key, 'wb') as fd: fd.write(private_bytes)
os.chmod(private_key, 0o600)
fd.write(dump_privatekey(FILETYPE_PEM, key)) public_key = private_key.public_key().public_bytes_raw()
os.chmod(private_key, 0o400) user_id = get_onion(public_key)
user_id = get_service_id(private_key)
if not os.path.exists(cert_path) or \ if not os.path.exists(ca_cert_path) or \
(datetime.now() - datetime.fromtimestamp(os.path.getmtime(cert_path))).days > 60: (datetime.now() - datetime.fromtimestamp(os.path.getmtime(ca_cert_path))).days > 5*365:
with open(ca_key_path, 'rb') as key_file:
key_data = key_file.read()
cakey = load_privatekey(FILETYPE_PEM, key_data)
ca = X509() ca = X509()
ca.set_version(2) ca.set_version(2)
ca.set_serial_number(1) ca.set_serial_number(1)
ca.get_subject().CN = user_id ca.get_subject().CN = user_id
ca.gmtime_adj_notBefore(0) ca.gmtime_adj_notBefore(0)
ca.gmtime_adj_notAfter(90 * 24 * 60 * 60) ca.gmtime_adj_notAfter(10 * 356 * 24 * 60 * 60)
ca.set_issuer(ca.get_subject()) ca.set_issuer(ca.get_subject())
ca.set_pubkey(key) ca.set_pubkey(cakey)
ca.add_extensions([ ca.add_extensions([
X509Extension(b"basicConstraints", True, b"CA:TRUE, pathlen:0"), X509Extension(b"basicConstraints", False, b"CA:TRUE"),
X509Extension(b"nsCertType", True, b"sslCA"), X509Extension(b"keyUsage", False, b"keyCertSign, cRLSign"),
X509Extension(
b"subjectKeyIdentifier", False, b"hash", subject=ca
),
])
ca.add_extensions([
X509Extension(
b"authorityKeyIdentifier", False, b"keyid:always", issuer=ca
)
])
sign_cert(ca, cakey)
with open(ca_cert_path, 'wb') as fd:
fd.write(dump_certificate(FILETYPE_PEM, ca))
if os.path.exists(cert_path):
os.unlink(cert_path)
if os.path.exists(key_path):
os.unlink(key_path)
else:
with open(ca_cert_path) as fd:
ca = load_certificate(FILETYPE_PEM, fd.read())
with open(ca_key_path) as fd:
cakey = load_privatekey(FILETYPE_PEM, fd.read())
# create RSA intermediate certificate since clients don't quite like Ed25519 yet
if not os.path.exists(cert_path) or \
(datetime.now() - datetime.fromtimestamp(os.path.getmtime(cert_path))).days > 60:
key = PKey()
key.generate_key(TYPE_RSA, 2048)
cert = X509()
cert.set_version(2)
cert.set_serial_number(2)
cert.get_subject().CN = user_id + ".onion"
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(90 * 24 * 60 * 60)
cert.set_issuer(ca.get_subject())
cert.set_pubkey(key)
subject_alt_names = b"DNS: %s.onion" % user_id.encode()
cert.add_extensions([
X509Extension(b"basicConstraints", True, b"CA:FALSE"),
X509Extension(b"extendedKeyUsage", True, X509Extension(b"extendedKeyUsage", True,
b"serverAuth,clientAuth,emailProtection,timeStamping,msCodeInd,msCodeCom,msCTLSign,msSGC,msEFS,nsSGC"), b"serverAuth,clientAuth,emailProtection,timeStamping,msCodeInd,msCodeCom,msCTLSign,msSGC,msEFS,nsSGC"),
X509Extension(b"keyUsage", False, b"keyCertSign, cRLSign"), X509Extension(b"keyUsage", False, b"keyCertSign, cRLSign"),
X509Extension(b"subjectKeyIdentifier", False, b"hash", subject=ca), X509Extension(b"subjectKeyIdentifier", False, b"hash", subject=ca),
X509Extension(b"subjectAltName", critical=True, value=subject_alt_names),
]) ])
ca.sign(key, "sha256") sign_cert(cert, cakey)
with open(cert_path, 'wb') as fd: with open(cert_path, 'wb') as fd:
fd.write(dump_certificate(FILETYPE_PEM, cert))
fd.write(dump_certificate(FILETYPE_PEM, ca)) fd.write(dump_certificate(FILETYPE_PEM, ca))
with open(key_path, 'wb') as fd:
fd.write(dump_privatekey(FILETYPE_PEM, key))
return user_id return user_id
def get_service_id(private_key_file=None, cert=None): def get_service_id(private_key_file=None, cert=None):
''' '''
service_id is the first half of the sha1 of the rsa public key encoded in base32 service_id is the first half of the sha1 of the rsa public key encoded in base32
''' '''
if private_key_file: if private_key_file:
with open(private_key_file, 'rb') as fd: with open(private_key_file, 'rb') as key_file:
private_key = fd.read() key_type, key_content = key_file.read().split(b':', 1)
public_key = RSA.importKey(private_key).publickey().exportKey('DER')[22:] private_key = base64.decodebytes(key_content)
# compute sha1 of public key and encode first half in base32 public_key = Ed25519().public_key_from_hash(private_key)
service_id = base64.b32encode(hashlib.sha1(public_key).digest()[:10]).lower().decode() service_id = get_onion(public_key)
'''
# compute public key from priate key and export in DER format
# ignoring the SPKI header(22 bytes)
key = load_privatekey(FILETYPE_PEM, private_key)
cert = X509()
cert.set_pubkey(key)
public_key = dump_privatekey(FILETYPE_ASN1, cert.get_pubkey())[22:]
# compute sha1 of public key and encode first half in base32
service_id = base64.b32encode(hashlib.sha1(public_key).digest()[:10]).lower().decode()
'''
elif cert: elif cert:
# compute sha1 of public key and encode first half in base32 cert_ = load_certificate(FILETYPE_PEM, cert)
key = load_certificate(FILETYPE_ASN1, cert).get_pubkey() key = cert_.get_pubkey()
pub_der = DerSequence() public_key = key.to_cryptography_key().public_bytes_raw()
pub_der.decode(dump_privatekey(FILETYPE_ASN1, key)) service_id = get_onion(public_key)
public_key = RSA.construct((pub_der._seq[1], pub_der._seq[2])).exportKey('DER')[22:] else:
service_id = base64.b32encode(hashlib.sha1(public_key).digest()[:10]).lower().decode() service_id = None
return service_id return service_id
def update_dict(root, data): def update_dict(root, data):
@ -398,7 +485,7 @@ def check_pidfile(pid):
def ctl(*args): def ctl(*args):
import settings import settings
if sys.platform == 'win32': if sys.platform == 'win32':
platform_win32 = os.path.normpath(os.path.join(settings.base_dir, '..', 'platform_win32')) platform_win32 = os.path.normpath(os.path.join(settings.top_dir, 'platform_win32'))
python = os.path.join(platform_win32, 'pythonw.exe') python = os.path.join(platform_win32, 'pythonw.exe')
cmd = [python, 'oml'] + list(args) cmd = [python, 'oml'] + list(args)
startupinfo = subprocess.STARTUPINFO() startupinfo = subprocess.STARTUPINFO()
@ -495,3 +582,36 @@ def iexists(path):
def same_path(f1, f2): def same_path(f1, f2):
return unicodedata.normalize('NFC', f1) == unicodedata.normalize('NFC', f2) return unicodedata.normalize('NFC', f1) == unicodedata.normalize('NFC', f2)
def time_cache(max_age, maxsize=128, typed=False):
def _decorator(fn):
@functools.lru_cache(maxsize=maxsize, typed=typed)
def _new(*args, __time_salt, **kwargs):
return fn(*args, **kwargs)
@functools.wraps(fn)
def _wrapped(*args, **kwargs):
return _new(*args, **kwargs, __time_salt=int(time.time() / max_age))
return _wrapped
return _decorator
def migrate_userid(old_id, new_id):
from db import run_sql
import settings
statements = [
"UPDATE user SET id = '{nid}' WHERE id = '{oid}'",
"UPDATE list SET user_id = '{nid}' WHERE user_id = '{oid}'",
"UPDATE useritem SET user_id = '{nid}' WHERE user_id = '{oid}'",
"UPDATE changelog SET user_id = '{nid}' WHERE user_id = '{oid}'",
]
run_sql([
sql.format(oid=old_id, nid=new_id)
for sql in statements
])
for ext in ('log', 'db', 'json'):
old_file = os.path.join(settings.data_path, 'peers/%s.%s' % (old_id, ext))
new_file = os.path.join(settings.data_path, 'peers/%s.%s' % (new_id, ext))
if os.path.exists(old_file) and not os.path.exists(new_file):
os.rename(old_file, new_file)

View file

@ -2,7 +2,7 @@ requests==2.21.0
chardet chardet
html5lib html5lib
#ox>=2.0.666 #ox>=2.0.666
git+http://git.0x2620.org/python-ox.git#egg=python-ox git+https://code.0x2620.org/0x2620/python-ox.git#egg=ox
python-stdnum==1.2 python-stdnum==1.2
PyPDF2==1.25.1 PyPDF2==1.25.1
pysocks pysocks

View file

@ -1,9 +1,9 @@
lxml lxml
simplejson simplejson
ed25519>=1.4 ed25519>=1.4
SQLAlchemy==1.0.12 SQLAlchemy==1.4.46
pyopenssl>=0.15 pyopenssl>=0.15
pyCrypto>=2.6.1 pycryptodome
pillow pillow
netifaces netifaces
tornado==5.1.1 tornado==6.0.3

View file

@ -10,6 +10,10 @@
font-size: 14px; font-size: 14px;
line-height: 21px; line-height: 21px;
} }
.OMLQuote img {
max-width: 100%;
margin: auto;
}
.OMLAnnotation .OMLQuoteBackground { .OMLAnnotation .OMLQuoteBackground {
position: absolute; position: absolute;

View file

@ -1,4 +1,4 @@
<!DOCTYPE html> <!DOCTYPE html>
<!-- <!--
Copyright 2012 Mozilla Foundation Copyright 2012 Mozilla Foundation
@ -25,47 +25,53 @@ See https://github.com/adobe-type-tools/cmap-resources
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="google" content="notranslate"> <meta name="google" content="notranslate">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>PDF.js viewer</title>
<title></title>
<link rel="stylesheet" href="/static/pdf.js/viewer.css?3"/>
<script>
var DEFAULT_URL=document.location.pathname.replace(/\/reader\//, '/pdf/');
</script>
<style> <style>
#download, #openFile, #print, #viewBookmark { #download, #openFile, #print, #viewBookmark {
display:none; display:none;
} }
</style> </style>
<link rel="resource" type="application/l10n" href="/static/pdf.js/locale/locale.json">
<script src="/static/pdf.js/pdf.mjs" type="module"></script>
<script src="/static/oxjs/min/Ox.js"></script>
<script src="/static/oxjs/min/Ox.js?3"></script> <link rel="stylesheet" href="/static/pdf.js/viewer.css">
<script src="/static/pdf.js/compatibility.js?3"></script> <link rel="stylesheet" href="/static/reader/pdf.css">
<!-- This snippet is used in production (included from viewer.html) --> <script src="/static/pdf.js/viewer.mjs" type="module"></script>
<link rel="resource" type="application/l10n" href="/static/pdf.js/locale/en-US/viewer.properties"/>
<script src="/static/pdf.js/l10n.js?3"></script>
<script src="/static/pdf.js/pdf.js?3"></script>
<script src="/static/pdf.js/viewer.js?3"></script>
<script src="/static/reader/pdf.js?3"></script>
</head> </head>
<body tabindex="1" class="loadingInProgress"> <body tabindex="1">
<div id="outerContainer"> <div id="outerContainer">
<div id="sidebarContainer"> <div id="sidebarContainer">
<div id="toolbarSidebar"> <div id="toolbarSidebar">
<div class="splitToolbarButton toggled"> <div id="toolbarSidebarLeft">
<button id="viewThumbnail" class="toolbarButton toggled" title="Show Thumbnails" tabindex="2" data-l10n-id="thumbs"> <div id="sidebarViewButtons" class="splitToolbarButton toggled" role="radiogroup">
<span data-l10n-id="thumbs_label">Thumbnails</span> <button id="viewThumbnail" class="toolbarButton toggled" title="Show Thumbnails" tabindex="2" data-l10n-id="pdfjs-thumbs-button" role="radio" aria-checked="true" aria-controls="thumbnailView">
<span data-l10n-id="pdfjs-thumbs-button-label">Thumbnails</span>
</button> </button>
<button id="viewOutline" class="toolbarButton" title="Show Document Outline (double-click to expand/collapse all items)" tabindex="3" data-l10n-id="document_outline"> <button id="viewOutline" class="toolbarButton" title="Show Document Outline (double-click to expand/collapse all items)" tabindex="3" data-l10n-id="pdfjs-document-outline-button" role="radio" aria-checked="false" aria-controls="outlineView">
<span data-l10n-id="document_outline_label">Document Outline</span> <span data-l10n-id="pdfjs-document-outline-button-label">Document Outline</span>
</button> </button>
<button id="viewAttachments" class="toolbarButton" title="Show Attachments" tabindex="4" data-l10n-id="attachments"> <button id="viewAttachments" class="toolbarButton" title="Show Attachments" tabindex="4" data-l10n-id="pdfjs-attachments-button" role="radio" aria-checked="false" aria-controls="attachmentsView">
<span data-l10n-id="attachments_label">Attachments</span> <span data-l10n-id="pdfjs-attachments-button-label">Attachments</span>
</button> </button>
<button id="viewLayers" class="toolbarButton" title="Show Layers (double-click to reset all layers to the default state)" tabindex="5" data-l10n-id="pdfjs-layers-button" role="radio" aria-checked="false" aria-controls="layersView">
<span data-l10n-id="pdfjs-layers-button-label">Layers</span>
</button>
</div>
</div>
<div id="toolbarSidebarRight">
<div id="outlineOptionsContainer">
<div class="verticalToolbarSeparator"></div>
<button id="currentOutlineItem" class="toolbarButton" disabled="disabled" title="Find Current Outline Item" tabindex="6" data-l10n-id="pdfjs-current-outline-item-button">
<span data-l10n-id="pdfjs-current-outline-item-button-label">Current Outline Item</span>
</button>
</div>
</div> </div>
</div> </div>
<div id="sidebarContent"> <div id="sidebarContent">
@ -75,119 +81,195 @@ See https://github.com/adobe-type-tools/cmap-resources
</div> </div>
<div id="attachmentsView" class="hidden"> <div id="attachmentsView" class="hidden">
</div> </div>
<div id="layersView" class="hidden">
</div> </div>
<div id="sidebarResizer" class="hidden"></div> </div>
<div id="sidebarResizer"></div>
</div> <!-- sidebarContainer --> </div> <!-- sidebarContainer -->
<div id="mainContainer"> <div id="mainContainer">
<div class="findbar hidden doorHanger" id="findbar"> <div class="findbar hidden doorHanger" id="findbar">
<div id="findbarInputContainer"> <div id="findbarInputContainer">
<input id="findInput" class="toolbarField" title="Find" placeholder="Find in document…" tabindex="91" data-l10n-id="find_input"> <span class="loadingInput end">
<input id="findInput" class="toolbarField" title="Find" placeholder="Find in document…" tabindex="91" data-l10n-id="pdfjs-find-input" aria-invalid="false">
</span>
<div class="splitToolbarButton"> <div class="splitToolbarButton">
<button id="findPrevious" class="toolbarButton findPrevious" title="Find the previous occurrence of the phrase" tabindex="92" data-l10n-id="find_previous"> <button id="findPrevious" class="toolbarButton" title="Find the previous occurrence of the phrase" tabindex="92" data-l10n-id="pdfjs-find-previous-button">
<span data-l10n-id="find_previous_label">Previous</span> <span data-l10n-id="pdfjs-find-previous-button-label">Previous</span>
</button> </button>
<div class="splitToolbarButtonSeparator"></div> <div class="splitToolbarButtonSeparator"></div>
<button id="findNext" class="toolbarButton findNext" title="Find the next occurrence of the phrase" tabindex="93" data-l10n-id="find_next"> <button id="findNext" class="toolbarButton" title="Find the next occurrence of the phrase" tabindex="93" data-l10n-id="pdfjs-find-next-button">
<span data-l10n-id="find_next_label">Next</span> <span data-l10n-id="pdfjs-find-next-button-label">Next</span>
</button> </button>
</div> </div>
</div> </div>
<div id="findbarOptionsOneContainer"> <div id="findbarOptionsOneContainer">
<input type="checkbox" id="findHighlightAll" class="toolbarField" tabindex="94"> <input type="checkbox" id="findHighlightAll" class="toolbarField" tabindex="94">
<label for="findHighlightAll" class="toolbarLabel" data-l10n-id="find_highlight">Highlight all</label> <label for="findHighlightAll" class="toolbarLabel" data-l10n-id="pdfjs-find-highlight-checkbox">Highlight All</label>
<input type="checkbox" id="findMatchCase" class="toolbarField" tabindex="95"> <input type="checkbox" id="findMatchCase" class="toolbarField" tabindex="95">
<label for="findMatchCase" class="toolbarLabel" data-l10n-id="find_match_case_label">Match case</label> <label for="findMatchCase" class="toolbarLabel" data-l10n-id="pdfjs-find-match-case-checkbox-label">Match Case</label>
</div> </div>
<div id="findbarOptionsTwoContainer"> <div id="findbarOptionsTwoContainer">
<input type="checkbox" id="findEntireWord" class="toolbarField" tabindex="96"> <input type="checkbox" id="findMatchDiacritics" class="toolbarField" tabindex="96">
<label for="findEntireWord" class="toolbarLabel" data-l10n-id="find_entire_word_label">Whole words</label> <label for="findMatchDiacritics" class="toolbarLabel" data-l10n-id="pdfjs-find-match-diacritics-checkbox-label">Match Diacritics</label>
<span id="findResultsCount" class="toolbarLabel hidden"></span> <input type="checkbox" id="findEntireWord" class="toolbarField" tabindex="97">
<label for="findEntireWord" class="toolbarLabel" data-l10n-id="pdfjs-find-entire-word-checkbox-label">Whole Words</label>
</div> </div>
<div id="findbarMessageContainer"> <div id="findbarMessageContainer" aria-live="polite">
<span id="findResultsCount" class="toolbarLabel"></span>
<span id="findMsg" class="toolbarLabel"></span> <span id="findMsg" class="toolbarLabel"></span>
</div> </div>
</div> <!-- findbar --> </div> <!-- findbar -->
<div class="editorParamsToolbar hidden doorHangerRight" id="editorHighlightParamsToolbar">
<div id="highlightParamsToolbarContainer" class="editorParamsToolbarContainer">
<div id="editorHighlightColorPicker" class="colorPicker">
<span id="highlightColorPickerLabel" class="editorParamsLabel" data-l10n-id="pdfjs-editor-highlight-colorpicker-label">Highlight color</span>
</div>
<div id="editorHighlightThickness">
<label for="editorFreeHighlightThickness" class="editorParamsLabel" data-l10n-id="pdfjs-editor-free-highlight-thickness-input">Thickness</label>
<div class="thicknessPicker">
<input type="range" id="editorFreeHighlightThickness" class="editorParamsSlider" data-l10n-id="pdfjs-editor-free-highlight-thickness-title" value="12" min="8" max="24" step="1" tabindex="101">
</div>
</div>
<div id="editorHighlightVisibility">
<div class="divider"></div>
<div class="toggler">
<label for="editorHighlightShowAll" class="editorParamsLabel" data-l10n-id="pdfjs-editor-highlight-show-all-button-label">Show all</label>
<button id="editorHighlightShowAll" class="toggle-button" data-l10n-id="pdfjs-editor-highlight-show-all-button" aria-pressed="true" tabindex="102"></button>
</div>
</div>
</div>
</div>
<div class="editorParamsToolbar hidden doorHangerRight" id="editorFreeTextParamsToolbar">
<div class="editorParamsToolbarContainer">
<div class="editorParamsSetter">
<label for="editorFreeTextColor" class="editorParamsLabel" data-l10n-id="pdfjs-editor-free-text-color-input">Color</label>
<input type="color" id="editorFreeTextColor" class="editorParamsColor" tabindex="103">
</div>
<div class="editorParamsSetter">
<label for="editorFreeTextFontSize" class="editorParamsLabel" data-l10n-id="pdfjs-editor-free-text-size-input">Size</label>
<input type="range" id="editorFreeTextFontSize" class="editorParamsSlider" value="10" min="5" max="100" step="1" tabindex="104">
</div>
</div>
</div>
<div class="editorParamsToolbar hidden doorHangerRight" id="editorInkParamsToolbar">
<div class="editorParamsToolbarContainer">
<div class="editorParamsSetter">
<label for="editorInkColor" class="editorParamsLabel" data-l10n-id="pdfjs-editor-ink-color-input">Color</label>
<input type="color" id="editorInkColor" class="editorParamsColor" tabindex="105">
</div>
<div class="editorParamsSetter">
<label for="editorInkThickness" class="editorParamsLabel" data-l10n-id="pdfjs-editor-ink-thickness-input">Thickness</label>
<input type="range" id="editorInkThickness" class="editorParamsSlider" value="1" min="1" max="20" step="1" tabindex="106">
</div>
<div class="editorParamsSetter">
<label for="editorInkOpacity" class="editorParamsLabel" data-l10n-id="pdfjs-editor-ink-opacity-input">Opacity</label>
<input type="range" id="editorInkOpacity" class="editorParamsSlider" value="100" min="1" max="100" step="1" tabindex="107">
</div>
</div>
</div>
<div class="editorParamsToolbar hidden doorHangerRight" id="editorStampParamsToolbar">
<div class="editorParamsToolbarContainer">
<button id="editorStampAddImage" class="secondaryToolbarButton" title="Add image" tabindex="108" data-l10n-id="pdfjs-editor-stamp-add-image-button">
<span class="editorParamsLabel" data-l10n-id="pdfjs-editor-stamp-add-image-button-label">Add image</span>
</button>
</div>
</div>
<div id="secondaryToolbar" class="secondaryToolbar hidden doorHangerRight"> <div id="secondaryToolbar" class="secondaryToolbar hidden doorHangerRight">
<div id="secondaryToolbarButtonContainer"> <div id="secondaryToolbarButtonContainer">
<button id="secondaryPresentationMode" class="secondaryToolbarButton presentationMode visibleLargeView" title="Switch to Presentation Mode" tabindex="51" data-l10n-id="presentation_mode"> <button id="secondaryOpenFile" class="secondaryToolbarButton" title="Open File" tabindex="51" data-l10n-id="pdfjs-open-file-button">
<span data-l10n-id="presentation_mode_label">Presentation Mode</span> <span data-l10n-id="pdfjs-open-file-button-label">Open</span>
</button> </button>
<button id="secondaryOpenFile" class="secondaryToolbarButton openFile visibleLargeView" title="Open File" tabindex="52" data-l10n-id="open_file"> <button id="secondaryPrint" class="secondaryToolbarButton visibleMediumView" title="Print" tabindex="52" data-l10n-id="pdfjs-print-button">
<span data-l10n-id="open_file_label">Open</span> <span data-l10n-id="pdfjs-print-button-label">Print</span>
</button> </button>
<button id="secondaryPrint" class="secondaryToolbarButton print visibleMediumView" title="Print" tabindex="53" data-l10n-id="print"> <button id="secondaryDownload" class="secondaryToolbarButton visibleMediumView" title="Save" tabindex="53" data-l10n-id="pdfjs-save-button">
<span data-l10n-id="print_label">Print</span> <span data-l10n-id="pdfjs-save-button-label">Save</span>
</button> </button>
<button id="secondaryDownload" class="secondaryToolbarButton download visibleMediumView" title="Download" tabindex="54" data-l10n-id="download"> <div class="horizontalToolbarSeparator"></div>
<span data-l10n-id="download_label">Download</span>
<button id="presentationMode" class="secondaryToolbarButton" title="Switch to Presentation Mode" tabindex="54" data-l10n-id="pdfjs-presentation-mode-button">
<span data-l10n-id="pdfjs-presentation-mode-button-label">Presentation Mode</span>
</button> </button>
<a href="#" id="secondaryViewBookmark" class="secondaryToolbarButton bookmark visibleSmallView" title="Current view (copy or open in new window)" tabindex="55" data-l10n-id="bookmark"> <a href="#" id="viewBookmark" class="secondaryToolbarButton" title="Current Page (View URL from Current Page)" tabindex="55" data-l10n-id="pdfjs-bookmark-button">
<span data-l10n-id="bookmark_label">Current View</span> <span data-l10n-id="pdfjs-bookmark-button-label">Current Page</span>
</a> </a>
<div class="horizontalToolbarSeparator visibleLargeView"></div> <div id="viewBookmarkSeparator" class="horizontalToolbarSeparator"></div>
<button id="firstPage" class="secondaryToolbarButton firstPage" title="Go to First Page" tabindex="56" data-l10n-id="first_page"> <button id="firstPage" class="secondaryToolbarButton" title="Go to First Page" tabindex="56" data-l10n-id="pdfjs-first-page-button">
<span data-l10n-id="first_page_label">Go to First Page</span> <span data-l10n-id="pdfjs-first-page-button-label">Go to First Page</span>
</button> </button>
<button id="lastPage" class="secondaryToolbarButton lastPage" title="Go to Last Page" tabindex="57" data-l10n-id="last_page"> <button id="lastPage" class="secondaryToolbarButton" title="Go to Last Page" tabindex="57" data-l10n-id="pdfjs-last-page-button">
<span data-l10n-id="last_page_label">Go to Last Page</span> <span data-l10n-id="pdfjs-last-page-button-label">Go to Last Page</span>
</button> </button>
<div class="horizontalToolbarSeparator"></div> <div class="horizontalToolbarSeparator"></div>
<button id="pageRotateCw" class="secondaryToolbarButton rotateCw" title="Rotate Clockwise" tabindex="58" data-l10n-id="page_rotate_cw"> <button id="pageRotateCw" class="secondaryToolbarButton" title="Rotate Clockwise" tabindex="58" data-l10n-id="pdfjs-page-rotate-cw-button">
<span data-l10n-id="page_rotate_cw_label">Rotate Clockwise</span> <span data-l10n-id="pdfjs-page-rotate-cw-button-label">Rotate Clockwise</span>
</button> </button>
<button id="pageRotateCcw" class="secondaryToolbarButton rotateCcw" title="Rotate Counterclockwise" tabindex="59" data-l10n-id="page_rotate_ccw"> <button id="pageRotateCcw" class="secondaryToolbarButton" title="Rotate Counterclockwise" tabindex="59" data-l10n-id="pdfjs-page-rotate-ccw-button">
<span data-l10n-id="page_rotate_ccw_label">Rotate Counterclockwise</span> <span data-l10n-id="pdfjs-page-rotate-ccw-button-label">Rotate Counterclockwise</span>
</button> </button>
<div class="horizontalToolbarSeparator"></div> <div class="horizontalToolbarSeparator"></div>
<button id="cursorSelectTool" class="secondaryToolbarButton selectTool toggled" title="Enable Text Selection Tool" tabindex="60" data-l10n-id="cursor_text_select_tool"> <div id="cursorToolButtons" role="radiogroup">
<span data-l10n-id="cursor_text_select_tool_label">Text Selection Tool</span> <button id="cursorSelectTool" class="secondaryToolbarButton toggled" title="Enable Text Selection Tool" tabindex="60" data-l10n-id="pdfjs-cursor-text-select-tool-button" role="radio" aria-checked="true">
<span data-l10n-id="pdfjs-cursor-text-select-tool-button-label">Text Selection Tool</span>
</button> </button>
<button id="cursorHandTool" class="secondaryToolbarButton handTool" title="Enable Hand Tool" tabindex="61" data-l10n-id="cursor_hand_tool"> <button id="cursorHandTool" class="secondaryToolbarButton" title="Enable Hand Tool" tabindex="61" data-l10n-id="pdfjs-cursor-hand-tool-button" role="radio" aria-checked="false">
<span data-l10n-id="cursor_hand_tool_label">Hand Tool</span> <span data-l10n-id="pdfjs-cursor-hand-tool-button-label">Hand Tool</span>
</button> </button>
</div>
<div class="horizontalToolbarSeparator"></div> <div class="horizontalToolbarSeparator"></div>
<button id="scrollVertical" class="secondaryToolbarButton scrollModeButtons scrollVertical toggled" title="Use Vertical Scrolling" tabindex="62" data-l10n-id="scroll_vertical"> <div id="scrollModeButtons" role="radiogroup">
<span data-l10n-id="scroll_vertical_label">Vertical Scrolling</span> <button id="scrollPage" class="secondaryToolbarButton" title="Use Page Scrolling" tabindex="62" data-l10n-id="pdfjs-scroll-page-button" role="radio" aria-checked="false">
<span data-l10n-id="pdfjs-scroll-page-button-label">Page Scrolling</span>
</button> </button>
<button id="scrollHorizontal" class="secondaryToolbarButton scrollModeButtons scrollHorizontal" title="Use Horizontal Scrolling" tabindex="63" data-l10n-id="scroll_horizontal"> <button id="scrollVertical" class="secondaryToolbarButton toggled" title="Use Vertical Scrolling" tabindex="63" data-l10n-id="pdfjs-scroll-vertical-button" role="radio" aria-checked="true">
<span data-l10n-id="scroll_horizontal_label">Horizontal Scrolling</span> <span data-l10n-id="pdfjs-scroll-vertical-button-label" >Vertical Scrolling</span>
</button> </button>
<button id="scrollWrapped" class="secondaryToolbarButton scrollModeButtons scrollWrapped" title="Use Wrapped Scrolling" tabindex="64" data-l10n-id="scroll_wrapped"> <button id="scrollHorizontal" class="secondaryToolbarButton" title="Use Horizontal Scrolling" tabindex="64" data-l10n-id="pdfjs-scroll-horizontal-button" role="radio" aria-checked="false">
<span data-l10n-id="scroll_wrapped_label">Wrapped Scrolling</span> <span data-l10n-id="pdfjs-scroll-horizontal-button-label">Horizontal Scrolling</span>
</button> </button>
<button id="scrollWrapped" class="secondaryToolbarButton" title="Use Wrapped Scrolling" tabindex="65" data-l10n-id="pdfjs-scroll-wrapped-button" role="radio" aria-checked="false">
<span data-l10n-id="pdfjs-scroll-wrapped-button-label">Wrapped Scrolling</span>
</button>
</div>
<div class="horizontalToolbarSeparator scrollModeButtons"></div> <div class="horizontalToolbarSeparator"></div>
<button id="spreadNone" class="secondaryToolbarButton spreadModeButtons spreadNone toggled" title="Do not join page spreads" tabindex="65" data-l10n-id="spread_none"> <div id="spreadModeButtons" role="radiogroup">
<span data-l10n-id="spread_none_label">No Spreads</span> <button id="spreadNone" class="secondaryToolbarButton toggled" title="Do not join page spreads" tabindex="66" data-l10n-id="pdfjs-spread-none-button" role="radio" aria-checked="true">
<span data-l10n-id="pdfjs-spread-none-button-label">No Spreads</span>
</button> </button>
<button id="spreadOdd" class="secondaryToolbarButton spreadModeButtons spreadOdd" title="Join page spreads starting with odd-numbered pages" tabindex="66" data-l10n-id="spread_odd"> <button id="spreadOdd" class="secondaryToolbarButton" title="Join page spreads starting with odd-numbered pages" tabindex="67" data-l10n-id="pdfjs-spread-odd-button" role="radio" aria-checked="false">
<span data-l10n-id="spread_odd_label">Odd Spreads</span> <span data-l10n-id="pdfjs-spread-odd-button-label">Odd Spreads</span>
</button> </button>
<button id="spreadEven" class="secondaryToolbarButton spreadModeButtons spreadEven" title="Join page spreads starting with even-numbered pages" tabindex="67" data-l10n-id="spread_even"> <button id="spreadEven" class="secondaryToolbarButton" title="Join page spreads starting with even-numbered pages" tabindex="68" data-l10n-id="pdfjs-spread-even-button" role="radio" aria-checked="false">
<span data-l10n-id="spread_even_label">Even Spreads</span> <span data-l10n-id="pdfjs-spread-even-button-label">Even Spreads</span>
</button> </button>
</div>
<div class="horizontalToolbarSeparator spreadModeButtons"></div> <div class="horizontalToolbarSeparator"></div>
<button id="documentProperties" class="secondaryToolbarButton documentProperties" title="Document Properties…" tabindex="68" data-l10n-id="document_properties"> <button id="documentProperties" class="secondaryToolbarButton" title="Document Properties…" tabindex="69" data-l10n-id="pdfjs-document-properties-button" aria-controls="documentPropertiesDialog">
<span data-l10n-id="document_properties_label">Document Properties…</span> <span data-l10n-id="pdfjs-document-properties-button-label">Document Properties…</span>
</button> </button>
</div> </div>
</div> <!-- secondaryToolbar --> </div> <!-- secondaryToolbar -->
@ -196,76 +278,84 @@ See https://github.com/adobe-type-tools/cmap-resources
<div id="toolbarContainer"> <div id="toolbarContainer">
<div id="toolbarViewer"> <div id="toolbarViewer">
<div id="toolbarViewerLeft"> <div id="toolbarViewerLeft">
<button id="sidebarToggle" class="toolbarButton" title="Toggle Sidebar" tabindex="11" data-l10n-id="toggle_sidebar"> <button id="sidebarToggle" class="toolbarButton" title="Toggle Sidebar" tabindex="11" data-l10n-id="pdfjs-toggle-sidebar-button" aria-expanded="false" aria-controls="sidebarContainer">
<span data-l10n-id="toggle_sidebar_label">Toggle Sidebar</span> <span data-l10n-id="pdfjs-toggle-sidebar-button-label">Toggle Sidebar</span>
</button> </button>
<div class="toolbarButtonSpacer"></div> <div class="toolbarButtonSpacer"></div>
<button id="viewFind" class="toolbarButton" title="Find in Document" tabindex="12" data-l10n-id="findbar"> <button id="viewFind" class="toolbarButton" title="Find in Document" tabindex="12" data-l10n-id="pdfjs-findbar-button" aria-expanded="false" aria-controls="findbar">
<span data-l10n-id="findbar_label">Find</span> <span data-l10n-id="pdfjs-findbar-button-label">Find</span>
</button> </button>
<div class="splitToolbarButton hiddenSmallView"> <div class="splitToolbarButton hiddenSmallView">
<button class="toolbarButton pageUp" title="Previous Page" id="previous" tabindex="13" data-l10n-id="previous"> <button class="toolbarButton" title="Previous Page" id="previous" tabindex="13" data-l10n-id="pdfjs-previous-button">
<span data-l10n-id="previous_label">Previous</span> <span data-l10n-id="pdfjs-previous-button-label">Previous</span>
</button> </button>
<div class="splitToolbarButtonSeparator"></div> <div class="splitToolbarButtonSeparator"></div>
<button class="toolbarButton pageDown" title="Next Page" id="next" tabindex="14" data-l10n-id="next"> <button class="toolbarButton" title="Next Page" id="next" tabindex="14" data-l10n-id="pdfjs-next-button">
<span data-l10n-id="next_label">Next</span> <span data-l10n-id="pdfjs-next-button-label">Next</span>
</button> </button>
</div> </div>
<input type="number" id="pageNumber" class="toolbarField pageNumber" title="Page" value="1" size="4" min="1" tabindex="15" data-l10n-id="page"> <span class="loadingInput start">
<input type="number" id="pageNumber" class="toolbarField" title="Page" value="1" min="1" tabindex="15" data-l10n-id="pdfjs-page-input" autocomplete="off">
</span>
<span id="numPages" class="toolbarLabel"></span> <span id="numPages" class="toolbarLabel"></span>
</div> </div>
<div id="toolbarViewerRight"> <div id="toolbarViewerRight">
<button id="presentationMode" class="toolbarButton presentationMode hiddenLargeView" title="Switch to Presentation Mode" tabindex="31" data-l10n-id="presentation_mode"> <div id="editorModeButtons" class="splitToolbarButton toggled" role="radiogroup">
<span data-l10n-id="presentation_mode_label">Presentation Mode</span> <button id="editorHighlight" class="toolbarButton" hidden="true" disabled="disabled" title="Highlight" role="radio" aria-checked="false" aria-controls="editorHighlightParamsToolbar" tabindex="31" data-l10n-id="pdfjs-editor-highlight-button">
<span data-l10n-id="pdfjs-editor-highlight-button-label">Highlight</span>
</button>
<button id="editorFreeText" class="toolbarButton" disabled="disabled" title="Text" role="radio" aria-checked="false" aria-controls="editorFreeTextParamsToolbar" tabindex="32" data-l10n-id="pdfjs-editor-free-text-button">
<span data-l10n-id="pdfjs-editor-free-text-button-label">Text</span>
</button>
<button id="editorInk" class="toolbarButton" disabled="disabled" title="Draw" role="radio" aria-checked="false" aria-controls="editorInkParamsToolbar" tabindex="33" data-l10n-id="pdfjs-editor-ink-button">
<span data-l10n-id="pdfjs-editor-ink-button-label">Draw</span>
</button>
<button id="editorStamp" class="toolbarButton hidden" disabled="disabled" title="Add or edit images" role="radio" aria-checked="false" aria-controls="editorStampParamsToolbar" tabindex="34" data-l10n-id="pdfjs-editor-stamp-button">
<span data-l10n-id="pdfjs-editor-stamp-button-label">Add or edit images</span>
</button>
</div>
<div id="editorModeSeparator" class="verticalToolbarSeparator"></div>
<button id="print" class="toolbarButton hiddenMediumView" title="Print" tabindex="41" data-l10n-id="pdfjs-print-button">
<span data-l10n-id="pdfjs-print-button-label">Print</span>
</button> </button>
<button id="openFile" class="toolbarButton openFile hiddenLargeView" title="Open File" tabindex="32" data-l10n-id="open_file"> <button id="download" class="toolbarButton hiddenMediumView" title="Save" tabindex="42" data-l10n-id="pdfjs-save-button">
<span data-l10n-id="open_file_label">Open</span> <span data-l10n-id="pdfjs-save-button-label">Save</span>
</button> </button>
<button id="print" class="toolbarButton print hiddenMediumView" title="Print" tabindex="33" data-l10n-id="print"> <div class="verticalToolbarSeparator hiddenMediumView"></div>
<span data-l10n-id="print_label">Print</span>
</button>
<button id="download" class="toolbarButton download hiddenMediumView" title="Download" tabindex="34" data-l10n-id="download"> <button id="secondaryToolbarToggle" class="toolbarButton" title="Tools" tabindex="43" data-l10n-id="pdfjs-tools-button" aria-expanded="false" aria-controls="secondaryToolbar">
<span data-l10n-id="download_label">Download</span> <span data-l10n-id="pdfjs-tools-button-label">Tools</span>
</button>
<a href="#" id="viewBookmark" class="toolbarButton bookmark hiddenSmallView" title="Current view (copy or open in new window)" tabindex="35" data-l10n-id="bookmark">
<span data-l10n-id="bookmark_label">Current View</span>
</a>
<div class="verticalToolbarSeparator hiddenSmallView"></div>
<button id="secondaryToolbarToggle" class="toolbarButton" title="Tools" tabindex="36" data-l10n-id="tools">
<span data-l10n-id="tools_label">Tools</span>
</button> </button>
</div> </div>
<div id="toolbarViewerMiddle"> <div id="toolbarViewerMiddle">
<div class="splitToolbarButton"> <div class="splitToolbarButton">
<button id="zoomOut" class="toolbarButton zoomOut" title="Zoom Out" tabindex="21" data-l10n-id="zoom_out"> <button id="zoomOut" class="toolbarButton" title="Zoom Out" tabindex="21" data-l10n-id="pdfjs-zoom-out-button">
<span data-l10n-id="zoom_out_label">Zoom Out</span> <span data-l10n-id="pdfjs-zoom-out-button-label">Zoom Out</span>
</button> </button>
<div class="splitToolbarButtonSeparator"></div> <div class="splitToolbarButtonSeparator"></div>
<button id="zoomIn" class="toolbarButton zoomIn" title="Zoom In" tabindex="22" data-l10n-id="zoom_in"> <button id="zoomIn" class="toolbarButton" title="Zoom In" tabindex="22" data-l10n-id="pdfjs-zoom-in-button">
<span data-l10n-id="zoom_in_label">Zoom In</span> <span data-l10n-id="pdfjs-zoom-in-button-label">Zoom In</span>
</button> </button>
</div> </div>
<span id="scaleSelectContainer" class="dropdownToolbarButton"> <span id="scaleSelectContainer" class="dropdownToolbarButton">
<select id="scaleSelect" title="Zoom" tabindex="23" data-l10n-id="zoom"> <select id="scaleSelect" title="Zoom" tabindex="23" data-l10n-id="pdfjs-zoom-select">
<option id="pageAutoOption" title="" value="auto" selected="selected" data-l10n-id="page_scale_auto">Automatic Zoom</option> <option id="pageAutoOption" title="" value="auto" selected="selected" data-l10n-id="pdfjs-page-scale-auto">Automatic Zoom</option>
<option id="pageActualOption" title="" value="page-actual" data-l10n-id="page_scale_actual">Actual Size</option> <option id="pageActualOption" title="" value="page-actual" data-l10n-id="pdfjs-page-scale-actual">Actual Size</option>
<option id="pageFitOption" title="" value="page-fit" data-l10n-id="page_scale_fit">Page Fit</option> <option id="pageFitOption" title="" value="page-fit" data-l10n-id="pdfjs-page-scale-fit">Page Fit</option>
<option id="pageWidthOption" title="" value="page-width" data-l10n-id="page_scale_width">Page Width</option> <option id="pageWidthOption" title="" value="page-width" data-l10n-id="pdfjs-page-scale-width">Page Width</option>
<option id="customScaleOption" title="" value="custom" disabled="disabled" hidden="true"></option> <option id="customScaleOption" title="" value="custom" disabled="disabled" hidden="true" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 0 }'>0%</option>
<option title="" value="0.5" data-l10n-id="page_scale_percent" data-l10n-args='{ "scale": 50 }'>50%</option> <option title="" value="0.5" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 50 }'>50%</option>
<option title="" value="0.75" data-l10n-id="page_scale_percent" data-l10n-args='{ "scale": 75 }'>75%</option> <option title="" value="0.75" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 75 }'>75%</option>
<option title="" value="1" data-l10n-id="page_scale_percent" data-l10n-args='{ "scale": 100 }'>100%</option> <option title="" value="1" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 100 }'>100%</option>
<option title="" value="1.25" data-l10n-id="page_scale_percent" data-l10n-args='{ "scale": 125 }'>125%</option> <option title="" value="1.25" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 125 }'>125%</option>
<option title="" value="1.5" data-l10n-id="page_scale_percent" data-l10n-args='{ "scale": 150 }'>150%</option> <option title="" value="1.5" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 150 }'>150%</option>
<option title="" value="2" data-l10n-id="page_scale_percent" data-l10n-args='{ "scale": 200 }'>200%</option> <option title="" value="2" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 200 }'>200%</option>
<option title="" value="3" data-l10n-id="page_scale_percent" data-l10n-args='{ "scale": 300 }'>300%</option> <option title="" value="3" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 300 }'>300%</option>
<option title="" value="4" data-l10n-id="page_scale_percent" data-l10n-args='{ "scale": 400 }'>400%</option> <option title="" value="4" data-l10n-id="pdfjs-page-scale-percent" data-l10n-args='{ "scale": 400 }'>400%</option>
</select> </select>
</span> </span>
</div> </div>
@ -279,125 +369,147 @@ See https://github.com/adobe-type-tools/cmap-resources
</div> </div>
</div> </div>
<menu type="context" id="viewerContextMenu">
<menuitem id="contextFirstPage" label="First Page"
data-l10n-id="first_page"></menuitem>
<menuitem id="contextLastPage" label="Last Page"
data-l10n-id="last_page"></menuitem>
<menuitem id="contextPageRotateCw" label="Rotate Clockwise"
data-l10n-id="page_rotate_cw"></menuitem>
<menuitem id="contextPageRotateCcw" label="Rotate Counter-Clockwise"
data-l10n-id="page_rotate_ccw"></menuitem>
</menu>
<div id="viewerContainer" tabindex="0"> <div id="viewerContainer" tabindex="0">
<div id="viewer" class="pdfViewer"></div> <div id="viewer" class="pdfViewer"></div>
</div> </div>
<div id="errorWrapper" hidden='true'>
<div id="errorMessageLeft">
<span id="errorMessage"></span>
<button id="errorShowMore" data-l10n-id="error_more_info">
More Information
</button>
<button id="errorShowLess" data-l10n-id="error_less_info" hidden='true'>
Less Information
</button>
</div>
<div id="errorMessageRight">
<button id="errorClose" data-l10n-id="error_close">
Close
</button>
</div>
<div class="clearBoth"></div>
<textarea id="errorMoreInfo" hidden='true' readonly="readonly"></textarea>
</div>
</div> <!-- mainContainer --> </div> <!-- mainContainer -->
<div id="overlayContainer" class="hidden"> <div id="dialogContainer">
<div id="passwordOverlay" class="container hidden"> <dialog id="passwordDialog">
<div class="dialog">
<div class="row"> <div class="row">
<p id="passwordText" data-l10n-id="password_label">Enter the password to open this PDF file:</p> <label for="password" id="passwordText" data-l10n-id="pdfjs-password-label">Enter the password to open this PDF file:</label>
</div> </div>
<div class="row"> <div class="row">
<input type="password" id="password" class="toolbarField"> <input type="password" id="password" class="toolbarField">
</div> </div>
<div class="buttonRow"> <div class="buttonRow">
<button id="passwordCancel" class="overlayButton"><span data-l10n-id="password_cancel">Cancel</span></button> <button id="passwordCancel" class="dialogButton"><span data-l10n-id="pdfjs-password-cancel-button">Cancel</span></button>
<button id="passwordSubmit" class="overlayButton"><span data-l10n-id="password_ok">OK</span></button> <button id="passwordSubmit" class="dialogButton"><span data-l10n-id="pdfjs-password-ok-button">OK</span></button>
</div> </div>
</div> </dialog>
</div> <dialog id="documentPropertiesDialog">
<div id="documentPropertiesOverlay" class="container hidden">
<div class="dialog">
<div class="row"> <div class="row">
<span data-l10n-id="document_properties_file_name">File name:</span> <p id="fileNameField">-</p> <span id="fileNameLabel" data-l10n-id="pdfjs-document-properties-file-name">File name:</span>
<p id="fileNameField" aria-labelledby="fileNameLabel">-</p>
</div> </div>
<div class="row"> <div class="row">
<span data-l10n-id="document_properties_file_size">File size:</span> <p id="fileSizeField">-</p> <span id="fileSizeLabel" data-l10n-id="pdfjs-document-properties-file-size">File size:</span>
<p id="fileSizeField" aria-labelledby="fileSizeLabel">-</p>
</div> </div>
<div class="separator"></div> <div class="separator"></div>
<div class="row"> <div class="row">
<span data-l10n-id="document_properties_title">Title:</span> <p id="titleField">-</p> <span id="titleLabel" data-l10n-id="pdfjs-document-properties-title">Title:</span>
<p id="titleField" aria-labelledby="titleLabel">-</p>
</div> </div>
<div class="row"> <div class="row">
<span data-l10n-id="document_properties_author">Author:</span> <p id="authorField">-</p> <span id="authorLabel" data-l10n-id="pdfjs-document-properties-author">Author:</span>
<p id="authorField" aria-labelledby="authorLabel">-</p>
</div> </div>
<div class="row"> <div class="row">
<span data-l10n-id="document_properties_subject">Subject:</span> <p id="subjectField">-</p> <span id="subjectLabel" data-l10n-id="pdfjs-document-properties-subject">Subject:</span>
<p id="subjectField" aria-labelledby="subjectLabel">-</p>
</div> </div>
<div class="row"> <div class="row">
<span data-l10n-id="document_properties_keywords">Keywords:</span> <p id="keywordsField">-</p> <span id="keywordsLabel" data-l10n-id="pdfjs-document-properties-keywords">Keywords:</span>
<p id="keywordsField" aria-labelledby="keywordsLabel">-</p>
</div> </div>
<div class="row"> <div class="row">
<span data-l10n-id="document_properties_creation_date">Creation Date:</span> <p id="creationDateField">-</p> <span id="creationDateLabel" data-l10n-id="pdfjs-document-properties-creation-date">Creation Date:</span>
<p id="creationDateField" aria-labelledby="creationDateLabel">-</p>
</div> </div>
<div class="row"> <div class="row">
<span data-l10n-id="document_properties_modification_date">Modification Date:</span> <p id="modificationDateField">-</p> <span id="modificationDateLabel" data-l10n-id="pdfjs-document-properties-modification-date">Modification Date:</span>
<p id="modificationDateField" aria-labelledby="modificationDateLabel">-</p>
</div> </div>
<div class="row"> <div class="row">
<span data-l10n-id="document_properties_creator">Creator:</span> <p id="creatorField">-</p> <span id="creatorLabel" data-l10n-id="pdfjs-document-properties-creator">Creator:</span>
<p id="creatorField" aria-labelledby="creatorLabel">-</p>
</div> </div>
<div class="separator"></div> <div class="separator"></div>
<div class="row"> <div class="row">
<span data-l10n-id="document_properties_producer">PDF Producer:</span> <p id="producerField">-</p> <span id="producerLabel" data-l10n-id="pdfjs-document-properties-producer">PDF Producer:</span>
<p id="producerField" aria-labelledby="producerLabel">-</p>
</div> </div>
<div class="row"> <div class="row">
<span data-l10n-id="document_properties_version">PDF Version:</span> <p id="versionField">-</p> <span id="versionLabel" data-l10n-id="pdfjs-document-properties-version">PDF Version:</span>
<p id="versionField" aria-labelledby="versionLabel">-</p>
</div> </div>
<div class="row"> <div class="row">
<span data-l10n-id="document_properties_page_count">Page Count:</span> <p id="pageCountField">-</p> <span id="pageCountLabel" data-l10n-id="pdfjs-document-properties-page-count">Page Count:</span>
<p id="pageCountField" aria-labelledby="pageCountLabel">-</p>
</div> </div>
<div class="row"> <div class="row">
<span data-l10n-id="document_properties_page_size">Page Size:</span> <p id="pageSizeField">-</p> <span id="pageSizeLabel" data-l10n-id="pdfjs-document-properties-page-size">Page Size:</span>
<p id="pageSizeField" aria-labelledby="pageSizeLabel">-</p>
</div> </div>
<div class="separator"></div> <div class="separator"></div>
<div class="row"> <div class="row">
<span data-l10n-id="document_properties_linearized">Fast Web View:</span> <p id="linearizedField">-</p> <span id="linearizedLabel" data-l10n-id="pdfjs-document-properties-linearized">Fast Web View:</span>
<p id="linearizedField" aria-labelledby="linearizedLabel">-</p>
</div> </div>
<div class="buttonRow"> <div class="buttonRow">
<button id="documentPropertiesClose" class="overlayButton"><span data-l10n-id="document_properties_close">Close</span></button> <button id="documentPropertiesClose" class="dialogButton"><span data-l10n-id="pdfjs-document-properties-close-button">Close</span></button>
</div>
</dialog>
<dialog class="dialog altText" id="altTextDialog" aria-labelledby="dialogLabel" aria-describedby="dialogDescription">
<div id="altTextContainer" class="mainContainer">
<div id="overallDescription">
<span id="dialogLabel" data-l10n-id="pdfjs-editor-alt-text-dialog-label" class="title">Choose an option</span>
<span id="dialogDescription" data-l10n-id="pdfjs-editor-alt-text-dialog-description">
Alt text (alternative text) helps when people can’t see the image or when it doesn’t load.
</span>
</div>
<div id="addDescription">
<div class="radio">
<div class="radioButton">
<input type="radio" id="descriptionButton" name="altTextOption" tabindex="0" aria-describedby="descriptionAreaLabel" checked>
<label for="descriptionButton" data-l10n-id="pdfjs-editor-alt-text-add-description-label">Add a description</label>
</div>
<div class="radioLabel">
<span id="descriptionAreaLabel" data-l10n-id="pdfjs-editor-alt-text-add-description-description">
Aim for 1-2 sentences that describe the subject, setting, or actions.
</span>
</div>
</div>
<div class="descriptionArea">
<textarea id="descriptionTextarea" placeholder="For example, “A young man sits down at a table to eat a meal”" aria-labelledby="descriptionAreaLabel" data-l10n-id="pdfjs-editor-alt-text-textarea" tabindex="0"></textarea>
</div>
</div>
<div id="markAsDecorative">
<div class="radio">
<div class="radioButton">
<input type="radio" id="decorativeButton" name="altTextOption" aria-describedby="decorativeLabel">
<label for="decorativeButton" data-l10n-id="pdfjs-editor-alt-text-mark-decorative-label">Mark as decorative</label>
</div>
<div class="radioLabel">
<span id="decorativeLabel" data-l10n-id="pdfjs-editor-alt-text-mark-decorative-description">
This is used for ornamental images, like borders or watermarks.
</span>
</div> </div>
</div> </div>
</div> </div>
<div id="printServiceOverlay" class="container hidden"> <div id="buttons">
<div class="dialog"> <button id="altTextCancel" class="secondaryButton" tabindex="0"><span data-l10n-id="pdfjs-editor-alt-text-cancel-button">Cancel</span></button>
<button id="altTextSave" class="primaryButton" tabindex="0"><span data-l10n-id="pdfjs-editor-alt-text-save-button">Save</span></button>
</div>
</div>
</dialog>
<dialog id="printServiceDialog" style="min-width: 200px;">
<div class="row"> <div class="row">
<span data-l10n-id="print_progress_message">Preparing document for printing…</span> <span data-l10n-id="pdfjs-print-progress-message">Preparing document for printing…</span>
</div> </div>
<div class="row"> <div class="row">
<progress value="0" max="100"></progress> <progress value="0" max="100"></progress>
<span data-l10n-id="print_progress_percent" data-l10n-args='{ "progress": 0 }' class="relative-progress">0%</span> <span data-l10n-id="pdfjs-print-progress-percent" data-l10n-args='{ "progress": 0 }' class="relative-progress">0%</span>
</div> </div>
<div class="buttonRow"> <div class="buttonRow">
<button id="printCancel" class="overlayButton"><span data-l10n-id="print_progress_close">Cancel</span></button> <button id="printCancel" class="dialogButton"><span data-l10n-id="pdfjs-print-progress-close-button">Cancel</span></button>
</div> </div>
</div> </dialog>
</div> </div> <!-- dialogContainer -->
</div> <!-- overlayContainer -->
</div> <!-- outerContainer --> </div> <!-- outerContainer -->
<div id="printContainer"></div> <div id="printContainer"></div>
<script src="/static/reader/pdf.js"></script>
</body> </body>
</html> </html>

View file

@ -1,9 +1,18 @@
'use strict'; 'use strict';
oml.SELECTION = 0
oml.HIGHLIGHT = 1
oml.ui.annotation = function(annotation, $iframe) { oml.ui.annotation = function(annotation, $iframe) {
var value = Ox.encodeHTMLEntities(annotation.text).replace(/\n/g, '<br/>')
if (annotation.type == oml.HIGHLIGHT) {
let coord = annotation.coords[0].map(p => parseInt(p)).join(',')
let image = `/${oml.user.ui.item}/2048p${parseInt(annotation.page)},${coord}.jpg`
value = `<img src="${image}">`
}
var $quoteText = Ox.Element() var $quoteText = Ox.Element()
.addClass('OxSelectable OMLQuote') .addClass('OxSelectable OMLQuote')
.html(Ox.encodeHTMLEntities(annotation.text).replace(/\n/g, '<br/>')) .html(value)
.on({ .on({
click: function(event) { click: function(event) {
var id var id

View file

@ -128,6 +128,10 @@ oml.ui.folders = function() {
.appendTo(that) .appendTo(that)
); );
oml.$ui.librariesList.$body.css({height: '16px'}); // FIXME! oml.$ui.librariesList.$body.css({height: '16px'}); // FIXME!
oml.$ui.librariesList.find('.OxColumnItems').css({
'text-overflow': 'reset',
'direction': 'rtl'
})
users.forEach(function(user, index) { users.forEach(function(user, index) {

View file

@ -219,7 +219,7 @@
function loadOML(browserSupported) { function loadOML(browserSupported) {
window.oml = Ox.App({ window.oml = Ox.App({
name: 'oml', name: 'oml',
socket: 'ws://' + document.location.host + '/ws', socket: document.location.protocol.replace('http', 'ws') + '//' + document.location.host + '/ws',
url: '/api/' url: '/api/'
}).bindEvent({ }).bindEvent({
load: function(data) { load: function(data) {

View file

@ -1028,7 +1028,7 @@ oml.updateFilterMenus = function() {
}; };
oml.validatePublicKey = function(value) { oml.validatePublicKey = function(value) {
return /^[a-z0-9+\/]{16}$/.test(value); return /^[a-z0-9+\/]{56}$/.test(value);
}; };
oml.updateDebugMenu = function() { oml.updateDebugMenu = function() {

View file

@ -151,7 +151,7 @@ oml.ui.viewer = function() {
height: '100%', height: '100%',
border: 0 border: 0
}).onMessage(function(data, event) { }).onMessage(function(data, event) {
console.log('got', event, data) // console.log('got', event, data, data.page)
if (event == 'addAnnotation') { if (event == 'addAnnotation') {
addAnnotation(data); addAnnotation(data);
var $annotation = oml.ui.annotation(data, $iframe).bindEvent(annotationEvents) var $annotation = oml.ui.annotation(data, $iframe).bindEvent(annotationEvents)
@ -215,7 +215,11 @@ oml.ui.viewer = function() {
}) })
return return
} }
annotations = Ox.sortBy(annotations, sortKey) var map = {}
map[sortKey] = function(value) {
return value ? value.toString() : '';
}
annotations = Ox.sortBy(annotations, sortKey, map)
oml.$ui.annotationFolder.empty(); oml.$ui.annotationFolder.empty();
var visibleAnnotations = []; var visibleAnnotations = [];
var hasAnnotations = false; var hasAnnotations = false;

62
static/reader/pdf.css Normal file
View file

@ -0,0 +1,62 @@
.toolbarButton.cropFile::before,
.secondaryToolbarButton.cropFile::before {
mask-image: url(pdf/toolbarButton-crop.png);
}
.toolbarButton.embedPage::before,
.secondaryToolbarButton.embedPage::before {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNTYiIGhlaWdodD0iMjU2IiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGxpbmUgeDE9Ijg4IiB5MT0iNTYiIHgyPSIyNCIgeTI9IjEyOCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iNDgiLz48bGluZSB4MT0iMjQiIHkxPSIxMjgiIHgyPSI4OCIgeTI9IjIwMCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iNDgiLz48bGluZSB4MT0iMTY4IiB5MT0iNTYiIHgyPSIyMzIiIHkyPSIxMjgiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2Utd2lkdGg9IjQ4Ii8+PGxpbmUgeDE9IjIzMiIgeTE9IjEyOCIgeDI9IjE2OCIgeTI9IjIwMCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iNDgiLz48L3N2Zz48IS0teyJjb2xvciI6ImRlZmF1bHQiLCJuYW1lIjoic3ltYm9sRW1iZWQiLCJ0aGVtZSI6Im94bWVkaXVtIn0tLT4=);
}
@media screen and (min-resolution: 2dppx) {
.toolbarButton.cropFile::before,
.secondaryToolbarButton.cropFile::before {
mask-image: url(pdf/toolbarButton-crop@2x.png);
}
.toolbarButton.embedPage::before,
.secondaryToolbarButton.embedPage::before {
mask-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNTYiIGhlaWdodD0iMjU2IiB2aWV3Qm94PSIwIDAgMjU2IDI1NiI+PGxpbmUgeDE9Ijg4IiB5MT0iNTYiIHgyPSIyNCIgeTI9IjEyOCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iNDgiLz48bGluZSB4MT0iMjQiIHkxPSIxMjgiIHgyPSI4OCIgeTI9IjIwMCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iNDgiLz48bGluZSB4MT0iMTY4IiB5MT0iNTYiIHgyPSIyMzIiIHkyPSIxMjgiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2Utd2lkdGg9IjQ4Ii8+PGxpbmUgeDE9IjIzMiIgeTE9IjEyOCIgeDI9IjE2OCIgeTI9IjIwMCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iNDgiLz48L3N2Zz48IS0teyJjb2xvciI6ImRlZmF1bHQiLCJuYW1lIjoic3ltYm9sRW1iZWQiLCJ0aGVtZSI6Im94bWVkaXVtIn0tLT4=);
}
}
.verticalToolbarSeparator.hiddenMediumView,
#print,
#secondaryPrint,
#openFile,
#secondaryOpenFile,
#editorModeButtons {
display: none !important;
}
.page .crop-overlay {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
//background: rgba(0,0,0,0.5);
cursor: crosshair;
z-index: 100;
}
.page .crop-overlay.inactive {
pointer-events: none;
cursor: default;
}
.page .crop-overlay canvas {
width: 100%;
height: 100%;
}
.page .highlights {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
//background: rgba(0,0,0,0.5);
z-index: 101;
pointer-events: none;
}
.page .highlights canvas {
width: 100%;
height: 100%;
}

View file

@ -1,6 +1,97 @@
var id = document.location.pathname.split('/')[1]; var id = document.location.pathname.split('/')[1];
var annotations = []; var annotations = [];
var currentPage = 1, rendered = false var currentPage = 1, rendered = false
var highlightInactive = true
var selectedAnnotation
const SELECTION = 0
const HIGHLIGHT = 1
var div = document.createElement("div")
div.innerHTML = `
<button id="cropFile" class="toolbarButton cropFile hiddenLargeView" title="Highlight" tabindex="30" data-l10n-id="crop_file">
<span data-l10n-id="crop_file_label">Highlight</span>
</button>
`
var cropFile = div.querySelector("#cropFile")
document.querySelector('#toolbarViewerRight').insertBefore(cropFile, document.querySelector('#toolbarViewerRight').firstChild)
// secondary menu
div.innerHTML = `
<button id="secondaryCropFile" class="secondaryToolbarButton visibleMediumView cropFile" title="Highlight" tabindex="50" data-l10n-id="crop">
<span data-l10n-id="crop_label">Highlight</span>
</button>
`
var secondaryCropFile = div.querySelector("#secondaryCropFile")
document.querySelector('#secondaryToolbarButtonContainer').insertBefore(
secondaryCropFile,
document.querySelector('#secondaryToolbarButtonContainer').firstChild
)
function initOverlay() {
document.querySelectorAll('#cropFile,.secondaryToolbarButton.cropFile').forEach(btn => {
btn.addEventListener('click', event=> {
if (highlightInactive) {
event.target.style.background = 'red'
highlightInactive = false
document.querySelectorAll('.crop-overlay.inactive').forEach(element => {
element.classList.remove('inactive')
})
} else {
event.target.style.background = ''
highlightInactive = true
document.querySelectorAll('.crop-overlay').forEach(element => {
element.classList.add('inactive')
})
}
})
})
PDFViewerApplication.initializedPromise.then(function() {
PDFViewerApplication.pdfViewer.eventBus.on("pagesinit", function(event) {
/*
document.querySelector('#viewerContainer').addEventListener('scroll', event => {
if (window.parent && window.parent.postMessage) {
if (first) {
first = false
} else {
window.parent.postMessage({event: 'scrolled', top: event.target.scrollTop})
}
}
})
*/
})
PDFViewerApplication.pdfViewer.eventBus.on("pagerender", function(event) {
var page = event.pageNumber.toString()
var div = event.source.div
var overlay = document.createElement('div')
overlay.classList.add('crop-overlay')
overlay.id = 'overlay' + page
if (highlightInactive) {
overlay.classList.add('inactive')
}
div.appendChild(overlay)
renderHighlightSelectionOverlay(overlay, id, page, event.source)
var highlights = document.createElement('div')
highlights.classList.add('highlights')
highlights.id = 'highlights' + page
var canvas = document.createElement('canvas')
highlights.appendChild(canvas)
div.appendChild(highlights)
renderHighlights(page)
})
PDFViewerApplication.eventBus.on('pagerendered', function(event) {
loadAnnotations(event.pageNumber)
})
})
}
document.addEventListener('DOMContentLoaded', function() {
window.PDFViewerApplication ? initOverlay() : document.addEventListener("webviewerloaded", initOverlay)
})
Ox.load({ Ox.load({
'UI': { 'UI': {
@ -25,7 +116,15 @@ Ox.load({
document.querySelector('#viewerContainer').scrollTop = el.offsetTop + el.parentElement.offsetTop - 64; document.querySelector('#viewerContainer').scrollTop = el.offsetTop + el.parentElement.offsetTop - 64;
} }
}, delay) }, delay)
var oldSelection = selectedAnnotation
selectedAnnotation = data.id
selectAnnotation(data.id) selectAnnotation(data.id)
if (oldSelection) {
var old = annotations.filter(function(a) { return a.id == oldSelection })[0]
if (old && old.type == HIGHLIGHT) {
renderHighlights(old.page)
}
}
} else if (event == 'addAnnotation') { } else if (event == 'addAnnotation') {
createAnnotation() createAnnotation()
} else if (event == 'addAnnotations') { } else if (event == 'addAnnotations') {
@ -37,7 +136,11 @@ Ox.load({
} }
data.annotations.forEach(function(annotation) { data.annotations.forEach(function(annotation) {
annotations.push(annotation) annotations.push(annotation)
if (annotation.type == HIGHLIGHT) {
renderHighlights(annotation.page)
} else {
renderAnnotation(annotation) renderAnnotation(annotation)
}
}) })
} else if (event == 'removeAnnotation') { } else if (event == 'removeAnnotation') {
removeAnnotation(data.id) removeAnnotation(data.id)
@ -75,18 +178,6 @@ window.addEventListener('mouseup', function(event) {
} }
}) })
function bindEvents() {
if (!window.PDFViewerApplication || !window.PDFViewerApplication.eventBus) {
setTimeout(bindEvents, 10)
return
}
PDFViewerApplication.eventBus.on('pagerendered', function(event) {
loadAnnotations(event.pageNumber)
})
}
bindEvents()
function getHighlight() { function getHighlight() {
var pageNumber = PDFViewerApplication.pdfViewer.currentPageNumber; var pageNumber = PDFViewerApplication.pdfViewer.currentPageNumber;
var pageIndex = pageNumber - 1; var pageIndex = pageNumber - 1;
@ -102,6 +193,7 @@ function getHighlight() {
var text = selection.toString(); var text = selection.toString();
var position = [pageNumber].concat(Ox.sort(selected.map(function(c) { return [c[1], c[0]]}))[0]); var position = [pageNumber].concat(Ox.sort(selected.map(function(c) { return [c[1], c[0]]}))[0]);
return { return {
type: SELECTION,
page: pageNumber, page: pageNumber,
pageLabel: PDFViewerApplication.pdfViewer.currentPageLabel, pageLabel: PDFViewerApplication.pdfViewer.currentPageLabel,
position: position, position: position,
@ -134,6 +226,10 @@ function renderAnnotation(annotation) {
} }
var pageElement = page.canvas.parentElement.parentElement; var pageElement = page.canvas.parentElement.parentElement;
var viewport = page.viewport; var viewport = page.viewport;
if (annotation.type == HIGHLIGHT) {
renderHighlights(annotation.page)
} else if (annotation.coords) {
pageElement.querySelectorAll('.oml-annotation').forEach(el => el.remove())
annotation.coords.forEach(function (rect) { annotation.coords.forEach(function (rect) {
var bounds = viewport.convertToViewportRectangle(rect); var bounds = viewport.convertToViewportRectangle(rect);
var el = document.createElement('div'); var el = document.createElement('div');
@ -153,6 +249,9 @@ function renderAnnotation(annotation) {
}); });
pageElement.appendChild(el); pageElement.appendChild(el);
}); });
} else {
// console.log("annotation without position", annotation)
}
} }
function addAnnotation(annotation) { function addAnnotation(annotation) {
@ -169,6 +268,11 @@ function selectAnnotation(id) {
g.classList.add('selected') g.classList.add('selected')
g.style.backgroundColor = 'blue' g.style.backgroundColor = 'blue'
}) })
annotations.forEach(a => {
if (a.id == id && a.type == HIGHLIGHT) {
renderHighlights(a.page)
}
})
} }
function deselectAnnotation(id) { function deselectAnnotation(id) {
@ -207,7 +311,7 @@ function loadAnnotations(page) {
e.remove() e.remove()
}) })
annotations.filter(function(a) { annotations.filter(function(a) {
return a.page == page return a.page == page && !a.type == HIGHLIGHT
}).forEach(function(annot) { }).forEach(function(annot) {
renderAnnotation(annot) renderAnnotation(annot)
}) })
@ -220,3 +324,145 @@ function isInView(element) {
var elementBottom = elementTop + $(element).height(); var elementBottom = elementTop + $(element).height();
return elementTop < docViewBottom && elementBottom > docViewTop; return elementTop < docViewBottom && elementBottom > docViewTop;
} }
function renderHighlightSelectionOverlay(root, documentId, page, source) {
var canvas = document.createElement('canvas')
root.appendChild(canvas)
canvas.width = canvas.clientWidth
canvas.height = canvas.clientHeight
var ctx = canvas.getContext('2d');
var viewerContainer = document.querySelector('#viewerContainer')
var bounds = root.getBoundingClientRect();
var base = 2048
var scale = Math.max(bounds.height, bounds.width) / base
var last_mousex = last_mousey = 0;
var mousex = mousey = 0;
var mousedown = false;
var p = {
top: 0,
left: 0,
bottom: 0,
right: 0
}
var inside = false
canvas.addEventListener('mousedown', function(e) {
if (inside) {
const coords = [
[p.left, p.top, p.right, p.bottom]
]
addAnnotation({
type: HIGHLIGHT,
id: Ox.SHA1(pageNumber.toString() + JSON.stringify(p)),
text: "",
page: parseInt(page),
pageLabel: source.pageLabel,
coords: coords,
})
return
}
let bounds = root.getBoundingClientRect();
last_mousex = e.clientX - bounds.left;
last_mousey = e.clientY - bounds.top;
p.top = parseInt(last_mousey / scale)
p.left = parseInt(last_mousex / scale)
mousedown = true;
});
document.addEventListener('mouseup', function(e) {
if (mousedown) {
mousedown = false;
p.bottom = parseInt(mousey / scale)
p.right = parseInt(mousex / scale)
if (p.top > p.bottom) {
var t = p.top
p.top = p.bottom
p.bottom = t
}
if (p.left > p.right) {
var t = p.left
p.left = p.right
p.right = t
}
/*
var url = `${baseUrl}/documents/${documentId}/2048p${page},${p.left},${p.top},${p.right},${p.bottom}.jpg`
info.url = `${baseUrl}/document/${documentId}/${page}`
info.page = page
if (p.left != p.right && p.top != p.bottom) {
var context = formatOutput(info, url)
copyToClipboard(context)
addToRecent({
document: documentId,
page: parseInt(page),
title: info.title,
type: 'fragment',
link: `${baseUrl}/documents/${documentId}/${page}`,
src: url
})
}
*/
}
});
canvas.addEventListener('mousemove', function(e) {
let bounds = root.getBoundingClientRect();
mousex = e.clientX - bounds.left;
mousey = e.clientY - bounds.top;
if(mousedown) {
ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight)
ctx.beginPath()
var width = mousex - last_mousex
var height = mousey - last_mousey
ctx.rect(last_mousex, last_mousey, width, height)
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;
ctx.stroke();
} else {
let py = parseInt(mousey / scale)
let px = parseInt(mousex / scale)
if (py > p.top && py < p.bottom && px > p.left && px < p.right) {
inside = true
canvas.style.cursor = 'pointer'
canvas.title = 'Click to add highlight'
} else {
inside = false
canvas.style.cursor = ''
canvas.title = ''
}
}
});
}
function renderHighlights(page) {
var pageAnnotations = annotations.filter(annotation => {
return annotation.type == HIGHLIGHT && (!page || (annotation.page == page))
})
pageAnnotations.forEach(annotation => {
let page = annotation.page
var canvas = document.querySelector(`#highlights${page} canvas`)
if (canvas) {
canvas.width = canvas.clientWidth
canvas.height = canvas.clientHeight
var ctx = canvas.getContext('2d');
var viewerContainer = document.querySelector('#viewerContainer')
var bounds = canvas.parentElement.getBoundingClientRect();
var base = 2048
var scale = Math.max(bounds.height, bounds.width) / base
ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight)
pageAnnotations.forEach(annotation => {
ctx.beginPath()
ctx.strokeStyle = annotation.id == selectedAnnotation ? 'blue' : 'yellow';
ctx.lineWidth = 2;
annotation.coords.forEach(coord => {
const width = coord[2] - coord[0],
height = coord[3] - coord[1];
ctx.rect(coord[0] * scale, coord[1] * scale, width * scale, height * scale)
})
ctx.stroke();
})
}
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -0,0 +1,4 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" height="54" width="54">
<path stroke-linejoin="round" stroke="#333" stroke-linecap="round" stroke-width="5" fill="none" d="m13.2 39 35-34.6m-45 8.1h36.6v38m-27-47v36.6h38"/>
</svg>

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB