Skip to content

Commit

Permalink
made authentication cookies persistent, added beginnings of offline m…
Browse files Browse the repository at this point in the history
…ode, use WAL mode for SQLite to reduce lockout when feeds are updating
  • Loading branch information
fazalmajid committed Aug 1, 2018
1 parent 0f82c6b commit 6fdf7fd
Show file tree
Hide file tree
Showing 7 changed files with 929 additions and 72 deletions.
15 changes: 9 additions & 6 deletions Makefile
@@ -1,4 +1,4 @@
VERSION= 2.0 VERSION= 2.2.0
TAR_VERSION= $(VERSION) TAR_VERSION= $(VERSION)
PAGES= view error opml feeds temboz_css rules catch_up PAGES= view error opml feeds temboz_css rules catch_up
DATE:sh= date +'%Y-%m-%d' DATE:sh= date +'%Y-%m-%d'
Expand All @@ -23,12 +23,15 @@ sync-js:
../src/scripts/vcheck --verbose -d --file etc/vcheck ../src/scripts/vcheck --verbose -d --file etc/vcheck
(cd spool; wget -N http://malsup.github.io/jquery.form.js) (cd spool; wget -N http://malsup.github.io/jquery.form.js)
(cd spool; wget -N https://raw.githubusercontent.com/jeresig/jquery.hotkeys/master/jquery.hotkeys.js) (cd spool; wget -N https://raw.githubusercontent.com/jeresig/jquery.hotkeys/master/jquery.hotkeys.js)
js: (cd spool; wget -N https://unpkg.com/dexie@latest/dist/dexie.js)
cat $(JUI)/external/jquery/jquery*.js spool/jquery.form.js \ (cd spool; wget -N https://raw.githubusercontent.com/janl/mustache.js/master/mustache.js)
$(JUI)/jquery-ui.js \
spool/jquery.hotkeys.meta.js tembozapp/static/specific.js \ js: $(JUI)/external/jquery/jquery*.js spool/jquery.form.js $(JUI)/jquery-ui.js spool/jquery.hotkeys.meta.js tembozapp/static/specific.js #spool/dexie.js
cat $^ \
| $(JSMIN) > tembozapp/static/temboz.js | $(JSMIN) > tembozapp/static/temboz.js
./temboz --kill cp spool/mustache.js tembozapp/static
#./temboz --kill
(svcadm restart temboz:fazal;svcadm restart nginx)
changelog: changelog:
cvs2cl.pl --tags -g -q cvs2cl.pl --tags -g -q


Expand Down
50 changes: 49 additions & 1 deletion tembozapp/dbop.py
Expand Up @@ -370,6 +370,7 @@ def fts(d, c):
fts_enabled = False fts_enabled = False


def sync_col(d, c): def sync_col(d, c):
c.execute("""pragma journal_mode=WAL""")
sql = c.execute("""select * from sqlite_master sql = c.execute("""select * from sqlite_master
where tbl_name='fm_items' and sql like '%updated%'""") where tbl_name='fm_items' and sql like '%updated%'""")
status = c.fetchone() status = c.fetchone()
Expand All @@ -392,6 +393,53 @@ def sync_col(d, c):
except: except:
d.rollback() d.rollback()


def sessions(d, c):
sql = c.execute("""select * from sqlite_master
where tbl_name='fm_sessions'""")
status = c.fetchone()
if not status:
try:
# create an updated column on fm_items with corresponding triggers
# to maintain it. This will be used to sync offline mode
c.execute("""create table fm_sessions (
uuid text primary key,
user_agent text,
created timestamp default (julianday('now')),
expires timestamp default (julianday('now') + 14)
)""")
d.commit()
except:
d.rollback()

def save_session(uuid, user_agent):
with db() as d:
c = d.cursor()
try:
c.execute("""delete from fm_sessions where expires < julianday('now')""")
c.execute("""insert into fm_sessions (uuid, user_agent) values (?, ?)""",
[uuid, user_agent])
d.commit()
auth_cache[uuid, user_agent] = time.time() + 14 * 86400
except sqlite3.IntegrityError:
pass

auth_cache = dict()
def check_session(uuid, user_agent):
if (uuid, user_agent) in auth_cache \
and time.time() < auth_cache[uuid, user_agent]:
return True
with db() as d:
c = d.cursor()
c.execute("""select count(*), MAX((expires - 2440587.5)*86400)
from fm_sessions
where uuid=? and user_agent=? and expires > julianday('now')
and created < julianday('now')""", [uuid, user_agent])
l = c.fetchone()
good = l and l[0] == 1
if good:
auth_cache[uuid, user_agent] = l[1]
return good

with db() as d: with db() as d:
c = d.cursor() c = d.cursor()
load_settings(c) load_settings(c)
Expand All @@ -400,5 +448,5 @@ def sync_col(d, c):
d.commit() d.commit()
fts(d, c) fts(d, c)
sync_col(d, c) sync_col(d, c)
d.commit() sessions(d, c)
c.close() c.close()
171 changes: 107 additions & 64 deletions tembozapp/server.py
Expand Up @@ -32,13 +32,13 @@ def __call__(self, environ, start_response):
cookies = werkzeug.utils.parse_cookie(environ) cookies = werkzeug.utils.parse_cookie(environ)
auth_cookie = cookies.get('auth') auth_cookie = cookies.get('auth')
auth_login = None auth_login = None
ua = environ.get('HTTP_USER_AGENT')
if cookie_secret and auth_cookie: if cookie_secret and auth_cookie:
auth = auth_cookie.split(':', 1) auth = auth_cookie.split(':', 1)
if len(auth) == 2: if len(auth) == 2:
login, hash = auth login, session = auth
if login == param.settings['login'] \ if login == param.settings['login'] \
and hash == hmac.new(cookie_secret, login, and dbop.check_session(session, ua):
hashlib.sha256).hexdigest():
auth_login = login auth_login = login


if not auth_login: if not auth_login:
Expand Down Expand Up @@ -155,23 +155,23 @@ def login():
and passlib.hash.argon2.verify(f.get('password', ''), and passlib.hash.argon2.verify(f.get('password', ''),
param.settings['passwd']): param.settings['passwd']):
# set auth cookie # set auth cookie
cookie = login + ':' + hmac.new(cookie_secret, login, session = hmac.new(cookie_secret, login, hashlib.sha256).hexdigest()
hashlib.sha256).hexdigest() ua = flask.request.headers.get('User-Agent')
dbop.save_session(session, ua)
cookie = login + ':' + session
back = flask.request.args.get('back', '/') back = flask.request.args.get('back', '/')
back = back if back else '/' back = back if back else '/'
resp = flask.make_response( resp = flask.make_response(
flask.redirect(back)) flask.redirect(back))
resp.set_cookie('auth', cookie, httponly=True) resp.set_cookie('auth', cookie, max_age=14*86400, httponly=True)
return resp return resp
else: else:
return flask.redirect('/login?err=invalid+login+or+password') return flask.redirect('/login?err=invalid+login+or+password')
else: else:
return flask.render_template('login.html', return flask.render_template('login.html',
err=flask.request.args.get('err')) err=flask.request.args.get('err'))


@app.route("/") def view_common(do_items=True):
@app.route("/view")
def view():
# Query-string parameters for this page # Query-string parameters for this page
# show # show
# feed_uid # feed_uid
Expand All @@ -191,7 +191,19 @@ def view():
i = update.ratings_dict.get(show, 1) i = update.ratings_dict.get(show, 1)
show = update.ratings[i][0] show = update.ratings[i][0]
item_desc = update.ratings[i][1] item_desc = update.ratings[i][1]
# items updated after the provided julianday
updated = flask.request.args.get('updated', '')
where = update.ratings[i][3] where = update.ratings[i][3]
params = []
if updated:
try:
updated = float(updated)
params.append(updated)
# we want all changes, not just unread ones, so we can mark
# read articles as such in IndexedDB
where = 'fm_items.updated > ?'
except:
print >> param.log, 'invalid updated=' + repr(updated)
sort = flask.request.args.get('sort', 'seen') sort = flask.request.args.get('sort', 'seen')
i = update.sorts_dict.get(sort, 1) i = update.sorts_dict.get(sort, 1)
sort = update.sorts[i][0] sort = update.sorts[i][0]
Expand All @@ -200,7 +212,6 @@ def view():
# optimizations for mobile devices # optimizations for mobile devices
mobile = bool(flask.request.args.get('mobile', False)) mobile = bool(flask.request.args.get('mobile', False))
# SQL options # SQL options
params = []
# filter by filter rule ID # filter by filter rule ID
if show == 'filtered': if show == 'filtered':
try: try:
Expand Down Expand Up @@ -230,12 +241,16 @@ def view():
# search functionality using fts5 if available # search functionality using fts5 if available
search = flask.request.args.get('search') search = flask.request.args.get('search')
search_in = flask.request.args.get('search_in', 'title') search_in = flask.request.args.get('search_in', 'title')
#print >> param.log, 'search =', repr(search)
if search: if search:
#print >> param.log, 'dbop.fts_enabled =', dbop.fts_enabled
if dbop.fts_enabled: if dbop.fts_enabled:
fterm = fts5.fts5_term(search)
#print >> param.log, 'FTERM =', repr(fterm)
where += """ and item_uid in ( where += """ and item_uid in (
select rowid from search where %s '%s' select rowid from search where %s '%s'
)""" % ('item_title match' if search_in == 'title' else 'search=', )""" % ('item_title match' if search_in == 'title' else 'search=',
fts5.fts5_term(search)) fterm)
else: else:
search = search.lower() search = search.lower()
search_where = 'item_title' if search_in == 'title' else 'item_content' search_where = 'item_title' if search_in == 'title' else 'item_content'
Expand Down Expand Up @@ -265,60 +280,76 @@ def view():
'<li><a href="%s">%s</a></li>' % (change_param(sort=sort_name), '<li><a href="%s">%s</a></li>' % (change_param(sort=sort_name),
sort_desc) sort_desc)
for (sort_name, sort_desc, discard, discard) in update.sorts) for (sort_name, sort_desc, discard, discard) in update.sorts)
# fetch and format items
tag_dict, rows = dbop.view_sql(c, where, order_by, params,
param.overload_threshold)
items = [] items = []
for row in rows: if do_items:
(uid, creator, title, link, content, loaded, created, rated, # fetch and format items
delta_created, rating, filtered_by, feed_uid, feed_title, feed_html, #print >> param.log, 'where =', where, 'params =', params
feed_xml, feed_snr) = row tag_dict, rows = dbop.view_sql(c, where, order_by, params,
# redirect = '/redirect/%d' % uid param.overload_threshold)
redirect = link for row in rows:
since_when = since(delta_created) (uid, creator, title, link, content, loaded, created, rated,
creator = creator.replace('"', '\'') delta_created, rating, filtered_by, feed_uid, feed_title, feed_html,
if rating == -2: feed_xml, feed_snr, updated_ts) = row
if filtered_by: # redirect = '/redirect/%d' % uid
rule = filters.Rule.registry.get(filtered_by) redirect = link
if rule: since_when = since(delta_created)
title = rule.highlight_title(title) creator = creator.replace('"', '\'')
content = rule.highlight_content(content) if rating == -2:
elif filtered_by == 0: if filtered_by:
content = '%s<br><p>Filtered by feed-specific Python rule</p>' \ rule = filters.Rule.registry.get(filtered_by)
% content if rule:
if uid in tag_dict or (creator and (creator != 'Unknown')): title = rule.highlight_title(title)
# XXX should probably escape the Unicode here content = rule.highlight_content(content)
tag_info = ' '.join('<span class="item tag">%s</span>' % t elif filtered_by == 0:
for t in sorted(tag_dict.get(uid, []))) content = '%s<br><p>Filtered by feed-specific Python rule</p>' \
if creator and creator != 'Unknown': % content
tag_info = '%s<span class="author tag">%s</span>' \ if uid in tag_dict or (creator and (creator != 'Unknown')):
% (tag_info, creator) # XXX should probably escape the Unicode here
tag_info = '<div class="tag_info" id="tags_%s">' % uid \ tag_info = ' '.join('<span class="item tag">%s</span>' % t
+ tag_info + '</div>' for t in sorted(tag_dict.get(uid, [])))
tag_call = '<a href="javascript:toggle_tags(%s);">tags</a>' % uid if creator and creator != 'Unknown':
else: tag_info = '%s<span class="author tag">%s</span>' \
tag_info = '' % (tag_info, creator)
tag_call = '(no tags)' tag_info = '<div class="tag_info" id="tags_%s">' % uid \
items.append({ + tag_info + '</div>'
'uid': uid, tag_call = '<a href="javascript:toggle_tags(%s);">tags</a>' % uid
'since_when': since_when, else:
'creator': creator, tag_info = ''
'loaded': loaded, tag_call = '(no tags)'
'feed_uid': feed_uid, items.append({
'title': title, 'uid': uid,
'feed_html': feed_html, 'since_when': since_when,
'content': content, 'creator': creator,
'tag_info': tag_info, 'loaded': loaded,
'tag_call': tag_call, 'feed_uid': feed_uid,
'redirect': redirect, 'title': title,
'feed_title': feed_title, 'feed_html': feed_html,
}) 'content': content,

'tag_info': tag_info,
return flask.render_template('view.html', show=show, item_desc=item_desc, 'tag_call': tag_call,
feed_uid=feed_uid, ratings_list=ratings_list, 'redirect': redirect,
sort_desc=sort_desc, sort_list=sort_list, 'feed_title': feed_title,
items=items, 'feed_snr': feed_snr,
overload_threshold=param.overload_threshold) 'updated_ts': updated_ts,
'rating': rating,
})
return {
'show': show,
'item_desc': item_desc,
'feed_uid': feed_uid,
'ratings_list': ratings_list,
'sort_desc': sort_desc,
'sort_list': sort_list,
'items': items,
'overload_threshold': param.overload_threshold
}


@app.route("/")
@app.route("/view")
def view():
pvars = view_common()
return flask.render_template('view.html', **pvars)


@app.route("/xmlfeedback/<op>/<rand>/<arg>") @app.route("/xmlfeedback/<op>/<rand>/<arg>")
def ajax(op, rand, arg): def ajax(op, rand, arg):
Expand Down Expand Up @@ -772,3 +803,15 @@ def blogroll():
), ),
200 , {'Content-Type': 'application/json'} 200 , {'Content-Type': 'application/json'}
) )

@app.route("/offline")
def offline():
pvars = view_common(do_items=False)
return flask.render_template('offline.html', **pvars)

@app.route("/sync")
def sync():
pvars = view_common()
return (json.dumps(pvars['items'], indent=2),
200 , {'Content-Type': 'application/json'})

27 changes: 27 additions & 0 deletions tembozapp/static/item.mst
@@ -0,0 +1,27 @@
<div class="article" id="art{{uid}}">
<div class="headline">
<span class="buttons">
<span class="down" onclick="hide('{{uid}}')">&#9660;</span>
<span class="up" onclick="highlight('{{uid}}')">&#9650;</span>
</span>
<span class="ctarrow" id="ctarrow{{uid}}"
onclick="collapseToggle('{{uid}}')">&#9660;&nbsp;</span>
<a href="{{redirect}}" class="headline" target="_blank"
title="by {{creator}}, cached at {{loaded}}">{{i.title|safe}}</a>
<br><a href="/feed/{{feed_uid}}" title=""
class="source screen" id="feed{{uid}}">{{feed_title}}</a>
<a href="{{feed_html}}" title="" class="source print"
id="feedprint{{uid}}">{{feed_title}}</a>
{% if not request.args.feed_uid %}
<a href="/view?feed_uid={{feed_uid}}&show={{ show}}"
class="ff" target="_blank">&#9658;</a>
{% endif %}
{{since_when}}
{{tag_call|safe}}
<a href="/item/{{uid}}/edit" class="screen">edit</a>
<br>
</div>{{tag_info|safe}}
<div class="content" id="content{{uid}}">
{{content|safe}}
</div>
</div>

0 comments on commit 6fdf7fd

Please sign in to comment.