diff options
-rw-r--r-- | python/fatcat_web/editing_routes.py | 16 | ||||
-rw-r--r-- | python/fatcat_web/hacks.py | 27 | ||||
-rw-r--r-- | python/fatcat_web/routes.py | 30 | ||||
-rw-r--r-- | python/fatcat_web/templates/entity_macros.html | 14 | ||||
-rw-r--r-- | python/fatcat_web/templates/release_view.html | 4 | ||||
-rw-r--r-- | python/fatcat_web/templates/webcapture_view.html | 18 | ||||
-rw-r--r-- | python/pytest.ini | 1 | ||||
-rw-r--r-- | python/tests/web_auth.py | 6 | ||||
-rw-r--r-- | rust/migrations/2019-01-01-000000_init/up.sql | 4 | ||||
-rw-r--r-- | rust/src/auth.rs | 29 | ||||
-rw-r--r-- | rust/tests/test_auth.rs | 7 |
11 files changed, 122 insertions, 34 deletions
diff --git a/python/fatcat_web/editing_routes.py b/python/fatcat_web/editing_routes.py index 29db3443..98e5c27d 100644 --- a/python/fatcat_web/editing_routes.py +++ b/python/fatcat_web/editing_routes.py @@ -30,7 +30,9 @@ def form_editgroup_get_or_create(api, edit_form): return None app.log.warning(ae) abort(ae.status) - # TODO: check here that editgroup hasn't been merged already + if eg.changelog_index: + edit_form.editgroup_id.errors.append("Editgroup has already been accepted") + return None else: # if no editgroup, create one from description try: @@ -67,6 +69,8 @@ def container_create(): abort(ae.status) # redirect to new entity return redirect('/container/{}'.format(edit.ident)) + else: + status = 400 elif form.errors: status = 400 app.log.info("form errors (did not validate): {}".format(form.errors)) @@ -102,6 +106,8 @@ def container_edit(ident): # redirect to entity revision # TODO: container_rev_view return redirect('/container/{}'.format(edit.ident)) + else: + status = 400 elif form.errors: status = 400 app.log.info("form errors (did not validate): {}".format(form.errors)) @@ -144,6 +150,8 @@ def file_create(): abort(ae.status) # redirect to new entity return redirect('/file/{}'.format(edit.ident)) + else: + status = 400 elif form.errors: status = 400 app.log.info("form errors (did not validate): {}".format(form.errors)) @@ -181,6 +189,8 @@ def file_edit(ident): # redirect to entity revision # TODO: file_rev_view return redirect('/file/{}'.format(edit.ident)) + else: + status = 400 elif form.errors: status = 400 app.log.info("form errors (did not validate): {}".format(form.errors)) @@ -234,6 +244,8 @@ def release_create(): abort(ae.status) # redirect to new release return redirect('/release/{}'.format(edit.ident)) + else: + status = 400 elif form.errors: status = 400 app.log.info("form errors (did not validate): {}".format(form.errors)) @@ -270,6 +282,8 @@ def release_edit(ident): # redirect to entity revision # TODO: release_rev_view return redirect('/release/{}'.format(edit.ident)) + else: + status = 400 elif form.errors: status = 400 app.log.info("form errors (did not validate): {}".format(form.errors)) diff --git a/python/fatcat_web/hacks.py b/python/fatcat_web/hacks.py new file mode 100644 index 00000000..9e6f6ab5 --- /dev/null +++ b/python/fatcat_web/hacks.py @@ -0,0 +1,27 @@ + +import re + +STRIP_EXTLINK_XML_RE = re.compile(r"<ext-link.*xlink:type=\"simple\">") + +def strip_extlink_xml(unstr): + unstr = unstr.replace("</ext-link>", "") + unstr = STRIP_EXTLINK_XML_RE.sub("", unstr) + return unstr + +def test_strip_extlink_xml(): + assert strip_extlink_xml("asdf") == "asdf" + assert strip_extlink_xml("""LOCKSS (2014) Available: <ext-link xmlns:xlink="http://www.w3.org/1999/xlink" ext-link-type="uri" xlink:href="http://lockss.org/" xlink:type="simple">http://lockss.org/</ext-link>. Accessed: 2014 November 1.""") == \ + """LOCKSS (2014) Available: http://lockss.org/. Accessed: 2014 November 1.""" + +def wayback_suffix(entity): + """ + Takes a webcapture entity and returns a suffix to be appended to wayback URLs + """ + ret = "" + if entity.original_url: + if entity.timestamp: + ret = entity.timestamp.strftime("%Y%m%d%H%M%S/") + else: + ret = "*/" + ret += entity.original_url + return ret diff --git a/python/fatcat_web/routes.py b/python/fatcat_web/routes.py index 3479a937..f7f10f44 100644 --- a/python/fatcat_web/routes.py +++ b/python/fatcat_web/routes.py @@ -13,6 +13,7 @@ from fatcat_web import app, api, auth_api, priv_api from fatcat_web.auth import handle_token_login, handle_logout, load_user, handle_ia_xauth from fatcat_web.cors import crossdomain from fatcat_web.search import * +from fatcat_web.hacks import strip_extlink_xml, wayback_suffix ### Views ################################################################### @@ -231,6 +232,8 @@ def webcapture_view(ident): entity.releases.append(api.get_release(r)) except ApiException as ae: abort(ae.status) + entity.wayback_suffix = wayback_suffix(entity) + print("SUFFIX: {}".format(entity.wayback_suffix)) return render_template('webcapture_view.html', webcapture=entity) @app.route('/release/lookup', methods=['GET']) @@ -266,8 +269,6 @@ def release_history(ident): def release_view(ident): try: entity = api.get_release(ident, expand="container,files,filesets,webcaptures") - container = entity.container - filesets = entity.filesets except ApiException as ae: abort(ae.status) if entity.state == "redirect": @@ -278,13 +279,23 @@ def release_view(ident): entity.container.es = container_to_elasticsearch(entity.container, force_bool=False) if entity.state == "active": entity.es = release_to_elasticsearch(entity, force_bool=False) - for fs in filesets: + for fs in entity.filesets: fs.total_size = sum([f.size for f in fs.manifest]) - entity.filesets = filesets + for wc in entity.webcaptures: + wc.wayback_suffix = wayback_suffix(wc) + for ref in entity.refs: + # this is a UI hack to get rid of XML crud in unstructured refs like: + # LOCKSS (2014) Available: <ext-link + # xmlns:xlink="http://www.w3.org/1999/xlink" ext-link-type="uri" + # xlink:href="http://lockss.org/" + # xlink:type="simple">http://lockss.org/</ext-link>. Accessed: 2014 + # November 1. + if ref.extra and ref.extra.get('unstructured'): + ref.extra['unstructured'] = strip_extlink_xml(ref.extra['unstructured']) authors = [c for c in entity.contribs if c.role in ('author', None)] authors = sorted(authors, key=lambda c: c.index or 99999999) return render_template('release_view.html', release=entity, - authors=authors, container=container) + authors=authors, container=entity.container) @app.route('/work/<ident>/history', methods=['GET']) def work_history(ident): @@ -382,6 +393,9 @@ def editgroup_accept(ident): except ApiException as ae: app.log.info(ae) abort(ae.status) + # clear active_editgroup_id cookie; this doesn't cover all cases + if eg.editgroup_id == session.get('active_editgroup_id'): + session.pop('active_editgroup_id') return redirect('/editgroup/{}'.format(ident)) @app.route('/editgroup/<ident>/unsubmit', methods=['POST']) @@ -650,7 +664,11 @@ def logout(): @app.route('/auth/account') @login_required def auth_account(): - editor = api.get_editor(session['editor']['editor_id']) + # auth check on account page + user_api = auth_api(session['api_token']) + resp = user_api.auth_check() + assert(resp.success) + editor = user_api.get_editor(session['editor']['editor_id']) session['editor'] = editor.to_dict() load_user(editor.editor_id) return render_template('auth_account.html') diff --git a/python/fatcat_web/templates/entity_macros.html b/python/fatcat_web/templates/entity_macros.html index 90c8e952..cefb0378 100644 --- a/python/fatcat_web/templates/entity_macros.html +++ b/python/fatcat_web/templates/entity_macros.html @@ -73,16 +73,22 @@ {%- endmacro %} -{% macro url_list(urls) -%} +{% macro url_list(urls, wayback_suffix="") -%} <table class="ui very basic compact single line fixed table"> <tbody> {% for url in urls %} + {% if url.rel == "wayback" %} + {% set suffix = wayback_suffix %} + {% else %} + {% set suffix = "" %} + {% endif %} + {% set entity = release %} <tr><td class="two wide right aligned">{{ url.rel }} - <td class="eight wide"><small><code><a href="{{ url.url }}"> + <td class="eight wide"><small><code><a href="{{ url.url }}{{ suffix }}"> {% if url.url.count('/') >= 3 and url.rel != "dweb" %} - {{ '/'.join(url.url.split('/')[0:2]) }}/<b>{{ ''.join(url.url.split('/')[2]) }}</b>/{{ '/'.join(url.url.split('/')[3:]) }} + {{ '/'.join(url.url.split('/')[0:2]) }}/<b>{{ ''.join(url.url.split('/')[2]) }}</b>/{{ '/'.join(url.url.split('/')[3:]) }}{{ suffix }} {% else %} - {{ url.url }} + {{ url.url }}{{ suffix }} {% endif %} </a></code></small> {% endfor %} diff --git a/python/fatcat_web/templates/release_view.html b/python/fatcat_web/templates/release_view.html index 4849842c..403af653 100644 --- a/python/fatcat_web/templates/release_view.html +++ b/python/fatcat_web/templates/release_view.html @@ -267,7 +267,7 @@ <br><small><code><a href="/webcapture/{{ webcapture.ident }}">webcapture:{{ webcapture.ident }}</a></code></small> <td class="single line"> {% for url in webcapture.archive_urls[:5] %} - <a href="{{ url.url }}{% if url.rel == "wayback" %}*/{{ webcapture.original_url }}{% endif %}">{{ url.url.split('/')[2] }}</a> ({{ url.rel }})<br> + <a href="{{ url.url }}{% if url.rel == "wayback" %}{{ webcapture.wayback_suffix }}{% endif %}">{{ url.url.split('/')[2] }}</a> ({{ url.rel }})<br> {% endfor %} {% if webcapture.urls|length > 5 %} + {{ file.urls|length - 5 }} more URLs @@ -318,7 +318,7 @@ {% if entity.files != [] and entity.files[0].urls != [] %} <a href="{{ entity.files[0].urls[0].url }}" class="ui top attached fluid huge green button"><i class="file pdf outline icon"></i>Download Full Text</a> {% elif entity.webcaptures != [] and entity.webcaptures[0].archive_urls != [] and entity.webcaptures[0].archive_urls[0].rel == "wayback" %} -<a href="{{ entity.webcaptures[0].archive_urls[0].url }}*/{{ entity.webcaptures[0].original_url }}" class="ui top attached fluid huge green button"><i class="file archive outline icon"></i>View Web Archive</a> +<a href="{{ entity.webcaptures[0].archive_urls[0].url }}{{ entity.webcaptures[0].wayback_suffix }}" class="ui top attached fluid huge green button"><i class="file archive outline icon"></i>View Web Archive</a> {% else %} <span class="ui top attached fluid huge grey button"><i class="file cross icon"></i>No Full Text Available</span> {% endif %} diff --git a/python/fatcat_web/templates/webcapture_view.html b/python/fatcat_web/templates/webcapture_view.html index 1dd179f6..919f31e1 100644 --- a/python/fatcat_web/templates/webcapture_view.html +++ b/python/fatcat_web/templates/webcapture_view.html @@ -33,6 +33,14 @@ This Web Capture is not associated with any fatcat release. {% endif %} +<br> +<h3>Archive URLs</h3> +{% if webcapture.archive_urls != None %} + {{ entity_macros.url_list(webcapture.archive_urls, webcapture.wayback_suffix) }} +{% else %} +No known public archive for this webcapture. +{% endif %} + <h3>CDX Rows ({{ webcapture.cdx|count }})</h3> {% if webcapture.cdx %} @@ -57,15 +65,7 @@ {% endfor %} </div> {% else %} -This File Set is empty (contains no files). -{% endif %} - -<br> -<h3>Archive URLs</h3> -{% if webcapture.archive_urls != None %} - {{ entity_macros.url_list(webcapture.archive_urls) }} -{% else %} -No known public archive for this webcapture. +This web capture is empty (contains no resources). {% endif %} </div> diff --git a/python/pytest.ini b/python/pytest.ini index 50a6f867..444333ea 100644 --- a/python/pytest.ini +++ b/python/pytest.ini @@ -13,5 +13,6 @@ addopts = --pylint --pylint-rcfile=.pylintrc --pylint-error-types=EF --pylint-jo filterwarnings = ignore:.*common_exception_handling.*StopIteration:PendingDeprecationWarning ignore:passing extensions and flags as constants is deprecated:DeprecationWarning + ignore:.*authlib.specs.*:authlib.deprecate.AuthlibDeprecationWarning log_level = INFO diff --git a/python/tests/web_auth.py b/python/tests/web_auth.py index 2fa9363b..b5839c6f 100644 --- a/python/tests/web_auth.py +++ b/python/tests/web_auth.py @@ -21,6 +21,9 @@ def test_ia_xauth_fail(full_app): data=dict(email="abcd@example.com", password="god")) assert rv.status_code == 401 + rv = app.get('/auth/account', follow_redirects=False) + assert rv.status_code == 302 + @responses.activate def test_ia_xauth(full_app): @@ -41,6 +44,9 @@ def test_ia_xauth(full_app): data=dict(email="abcd@example.com", password="god")) assert rv.status_code == 200 + rv = app.get('/auth/account', follow_redirects=False) + assert rv.status_code == 200 + def test_basic_auth_views(app): rv = app.get('/auth/login') diff --git a/rust/migrations/2019-01-01-000000_init/up.sql b/rust/migrations/2019-01-01-000000_init/up.sql index b97660eb..5211b29a 100644 --- a/rust/migrations/2019-01-01-000000_init/up.sql +++ b/rust/migrations/2019-01-01-000000_init/up.sql @@ -619,8 +619,8 @@ INSERT INTO webcapture_rev_cdx (webcapture_rev, surt, timestamp, url, mimetype, ('00000000-0000-0000-7777-FFF000000003', 'org,asheesh)/robots.txt', '2003-02-17T04:47:19Z', 'http://asheesh.org:80/robots.txt', 'text/html', 404, 'a637f1d27d9bcb237310ed29f19c07e1c8cf0aa5', 'ffc1005680cb620eec4c913437dfabbf311b535cfe16cbaeb2faec1f92afc362'); INSERT INTO webcapture_rev_url (webcapture_rev, rel, url) VALUES - ('00000000-0000-0000-7777-FFF000000002', 'wayback', 'http://web.archive.org/web/201801010001/http://example.org'), - ('00000000-0000-0000-7777-FFF000000003', 'wayback', 'http://web.archive.org/web/201801010001/https://asheesh.org'), + ('00000000-0000-0000-7777-FFF000000002', 'wayback', 'http://web.archive.org/web/'), + ('00000000-0000-0000-7777-FFF000000003', 'wayback', 'http://web.archive.org/web/'), ('00000000-0000-0000-7777-FFF000000003', 'warc', 'https://example.org/something.warc.gz'); INSERT INTO webcapture_ident (id, is_live, rev_id, redirect_id) VALUES diff --git a/rust/src/auth.rs b/rust/src/auth.rs index 7e9b945a..a62c2f58 100644 --- a/rust/src/auth.rs +++ b/rust/src/auth.rs @@ -229,7 +229,7 @@ impl AuthConfectionary { if let Some(duration) = duration { let expires = now_utc + duration; mac.add_first_party_caveat(&format!( - "time < {:?}", + "time < {}", &expires.to_rfc3339_opts(SecondsFormat::Secs, true) )); }; @@ -291,12 +291,15 @@ impl AuthConfectionary { let mut created: Option<DateTime<Utc>> = None; for caveat in mac.first_party_caveats() { if caveat.predicate().starts_with("time > ") { - created = Some( + let ts: chrono::ParseResult<DateTime<Utc>> = DateTime::parse_from_rfc3339(caveat.predicate().get(7..).unwrap()) - .unwrap() - .with_timezone(&Utc), - ); - break; + .map(|x| x.with_timezone(&Utc)); + if let Ok(ts) = ts { + created = Some(ts); + break; + } else { + info!("couldn't parse macaroon time constraint: {}", caveat.predicate()); + } } } let created = match created { @@ -337,10 +340,16 @@ impl AuthConfectionary { verifier.satisfy_general(|p: &str| -> bool { // not expired (based on time) if p.starts_with("time < ") { - let expires: DateTime<Utc> = DateTime::parse_from_rfc3339(p.get(7..).unwrap()) - .unwrap() - .with_timezone(&Utc); - expires < Utc::now() + let expires: chrono::ParseResult<DateTime<Utc>> = + DateTime::parse_from_rfc3339(p.get(7..).unwrap()) + .map(|x| x.with_timezone(&Utc)); + if let Ok(when) = expires { + //info!("checking time constraint: {} < {}", Utc::now(), when); + Utc::now() < when + } else { + info!("couldn't parse macaroon time constraint: {}", p); + false + } } else { false } diff --git a/rust/tests/test_auth.rs b/rust/tests/test_auth.rs index c0d81753..2faf78ec 100644 --- a/rust/tests/test_auth.rs +++ b/rust/tests/test_auth.rs @@ -34,6 +34,13 @@ fn test_auth_db() { let editor_row = c.parse_macaroon_token(&conn, &token, None).unwrap(); assert_eq!(editor_row.id, editor_id.to_uuid()); + // create token w/ expiration + let token = c.create_token(editor_id, Some(chrono::Duration::days(1))).unwrap(); + + // verify token w/ expiration + let editor_row = c.parse_macaroon_token(&conn, &token, None).unwrap(); + assert_eq!(editor_row.id, editor_id.to_uuid()); + // revoke token auth::revoke_tokens(&conn, editor_id).unwrap(); |