Commit 8f725ae6 authored by greebie's avatar greebie

Permit queries via Url.

This project enables the user to include a default filter query as part of a url.

The syntax works with the #annotations: fragment.  If a Q: or Query: (case insensitive)
is included, whatever follows the query will be included in a search filter as a
default.

user tags may be included in this query string. For instance user:USERNAME will
filter for the user called USERNAME. Using an @hypothes.is will search for exactly
that user. So far, the user, tag or any tags are the main permissible items.

So http://www.example.com/path/to/file#annotations:Q:user:USERNAMEtag:TAGany:KEYWORD

will filter the page for annotations by USERNAME tagged TAG and containing KEYWORD.
parent 8115a133
'use strict'; 'use strict';
var annotationIDs = require('./util/annotation-ids'); var annotationQuery = require('./util/extract-annotation-query');
var settings = require('../shared/settings'); var settings = require('../shared/settings');
var docs = 'https://h.readthedocs.io/en/latest/embedding.html'; var docs = 'https://h.readthedocs.io/en/latest/embedding.html';
...@@ -33,7 +33,7 @@ function config(window_) { ...@@ -33,7 +33,7 @@ function config(window_) {
} }
} }
// Extract the direct linked ID from the URL. // Extract the default query from the URL.
// //
// The Chrome extension or proxy may already have provided this config // The Chrome extension or proxy may already have provided this config
// via a tag injected into the DOM, which avoids the problem where the page's // via a tag injected into the DOM, which avoids the problem where the page's
...@@ -41,9 +41,9 @@ function config(window_) { ...@@ -41,9 +41,9 @@ function config(window_) {
// //
// In environments where the config has not been injected into the DOM, // In environments where the config has not been injected into the DOM,
// we try to retrieve it from the URL here. // we try to retrieve it from the URL here.
var directLinkedID = annotationIDs.extractIDFromURL(window_.location.href); var directLinkedID = annotationQuery.extractAnnotationQuery(window_.location.href);
if (directLinkedID) { if (directLinkedID) {
options.annotations = directLinkedID; Object.assign(options, directLinkedID);
} }
return options; return options;
} }
......
...@@ -26,7 +26,7 @@ module.exports = class Sidebar extends Host ...@@ -26,7 +26,7 @@ module.exports = class Sidebar extends Host
super super
this.hide() this.hide()
if options.openSidebar || options.annotations if options.openSidebar || options.annotations || options.query
this.on 'panelReady', => this.show() this.on 'panelReady', => this.show()
if @plugins.BucketBar? if @plugins.BucketBar?
......
'use strict'; 'use strict';
/** /**
* Extracts a direct-linked annotation ID from the fragment of a URL. * Extracts an annotation selection or default filter from a url.
* *
* @param {string} url - The URL which may contain a '#annotations:<ID>' * @param {string} url - The URL which may contain a '#annotations:<ID>'
* fragment. * fragment.
* @return {string?} The annotation ID if present * @return {Object} - An object with either an annotation ID or a filter string.
*/ */
function extractIDFromURL(url) { function extractAnnotationQuery(url) {
var filter = {};
try { try {
// Annotation IDs are url-safe-base64 identifiers // Annotation IDs are url-safe-base64 identifiers
// See https://tools.ietf.org/html/rfc4648#page-7 // See https://tools.ietf.org/html/rfc4648#page-7
var annotFragmentMatch = url.match(/#annotations:([A-Za-z0-9_-]+)$/); var annotFragmentMatch = url.match(/#annotations:([A-Za-z0-9_-]+)$/);
if (annotFragmentMatch) { var queryFragmentMatch = url.match(/#annotations:(query|q):(.+)$/i);
return annotFragmentMatch[1]; if (queryFragmentMatch) {
filter.query = queryFragmentMatch[2];
} else if (annotFragmentMatch) {
filter.annotations = annotFragmentMatch[1];
} else { } else {
return null; filter = null;
} }
} catch (err) { } catch (err) {
return null; filter = null;
} }
return filter;
} }
module.exports = { module.exports = {
extractIDFromURL: extractIDFromURL, extractAnnotationQuery: extractAnnotationQuery,
}; };
'use strict';
var annotationIds = require('../extract-annotation-query');
describe('annotation queries', function () {
var annotation = annotationIds.extractAnnotationQuery('http://localhost:3000#annotations:alphanum3ric_-only');
var queryVarA = annotationIds.extractAnnotationQuery('http://localhost:3000#annotations:q:user:USERNAME');
var queryVarB = annotationIds.extractAnnotationQuery('http://localhost:3000#annotations:QuerY:user:USERNAME');
var invalid = annotationIds.extractAnnotationQuery('http://localhost:3000#annotations:\"TRYINGTOGETIN\";EVILSCRIPT()');
it ('accepts regular annotation id', function () {
assert.equal(annotation.annotations, 'alphanum3ric_-only');
});
it ('returns null for query when annotation id exists', function() {
assert.equal(annotation.query, null);
});
it ('returns null on invalid query / id', function() {
assert.equal(invalid, null);
});
it ('produces a null annotation when valid query exists', function () {
assert.equal(queryVarA.annotations, null);
});
it ('accepts query style A ("q:")', function () {
assert.equal(queryVarA.query, 'user:USERNAME');
});
it ('accepts query style B ("query:")', function () {
assert.equal (queryVarB.query, 'user:USERNAME');
});
});
...@@ -16,6 +16,9 @@ function hostPageConfig(window) { ...@@ -16,6 +16,9 @@ function hostPageConfig(window) {
var paramWhiteList = [ var paramWhiteList = [
// Direct-linked annotation ID // Direct-linked annotation ID
'annotations', 'annotations',
// Default query passed by url
'query',
// Config param added by the extension, Via etc. indicating how Hypothesis // Config param added by the extension, Via etc. indicating how Hypothesis
// was added to the page. // was added to the page.
......
...@@ -16,6 +16,8 @@ var uiConstants = require('../ui-constants'); ...@@ -16,6 +16,8 @@ var uiConstants = require('../ui-constants');
var util = require('./util'); var util = require('./util');
var validQuery = require('../util/validate-query');
/** /**
* Default starting tab. * Default starting tab.
*/ */
...@@ -37,9 +39,10 @@ TAB_SORTKEYS_AVAILABLE[uiConstants.TAB_ANNOTATIONS] = ['Newest', 'Oldest', 'Loca ...@@ -37,9 +39,10 @@ TAB_SORTKEYS_AVAILABLE[uiConstants.TAB_ANNOTATIONS] = ['Newest', 'Oldest', 'Loca
TAB_SORTKEYS_AVAILABLE[uiConstants.TAB_NOTES] = ['Newest', 'Oldest']; TAB_SORTKEYS_AVAILABLE[uiConstants.TAB_NOTES] = ['Newest', 'Oldest'];
TAB_SORTKEYS_AVAILABLE[uiConstants.TAB_ORPHANS] = ['Newest', 'Oldest', 'Location']; TAB_SORTKEYS_AVAILABLE[uiConstants.TAB_ORPHANS] = ['Newest', 'Oldest', 'Location'];
function initialSelection(settings) { function initialSelection(settings) {
var selection = {}; var selection = {};
if (settings.annotations) { if (settings.annotations && !validQuery(settings.query)) {
selection[settings.annotations] = true; selection[settings.annotations] = true;
} }
return freeze(selection); return freeze(selection);
...@@ -81,7 +84,7 @@ function init(settings) { ...@@ -81,7 +84,7 @@ function init(settings) {
// IDs of annotations that should be highlighted // IDs of annotations that should be highlighted
highlighted: [], highlighted: [],
filterQuery: null, filterQuery: validQuery(settings.query),
selectedTab: TAB_DEFAULT, selectedTab: TAB_DEFAULT,
......
'use strict';
var queryUrl = require('../validate-query');
describe('queryUrl', function () {
var qURL;
var longqURL;
var trickyqURL;
var upperURL;
beforeEach(function () {
qURL = queryUrl('user:user_name');
longqURL = queryUrl('user:user_nameany:hello');
trickyqURL = queryUrl('user_bob__helloany:hello');
upperURL = queryUrl('something');
});
it ('returns false on a non-query', function () {
assert.equal(queryUrl({foo:'bar'}), null);
});
it('returns an annotation string as a query', function () {
assert.equal(qURL, 'user:user_name');
});
it('accepts longer queries', function () {
assert.equal(longqURL, 'user:user_name any: hello');
});
it ('is not tricked by confounding usernames or queries', function() {
assert.equal(trickyqURL, 'user_bob__hello any: hello');
});
it ('accepts upper and lower case values', function () {
assert.equal(upperURL, 'something');
});
});
'use strict';
// A set of functions to prepare a query from a url request
//
// These functions take values from var annotations
// produced in a url fragment
// ( e.g. http://www.example.com/path/to/file
// #annotations:query__user__username)
// in settings
// and converts it to a search query, detecting
// tags like "user:", "any:" and "tag:".
//
function returnQueryFromAnnotationFragment (query){
var result = null;
try {
if (query) {
result = query.replace(/(user|any|tag|text):/gi,
function (tag) {
// temporarily fix bug where
// any:term does not work
if (tag === 'any:') {
return ' ' + tag + ' ';
} else {
return ' ' + tag;
}
}).trim();
}
} catch (e) {
result = null;
}
return (result);
}
module.exports = returnQueryFromAnnotationFragment;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment