1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-30 10:42:41 +01:00
phorge-phorge/webroot/rsrc/externals/javelin/lib/Quicksand.js
epriestley e869e7df0b When logged-out users hit a "Login Required" dialog, try to choose a better "next" URI
Summary:
Ref T10004. After a user logs in, we send them to the "next" URI cookie if there is one, but currently don't always do a very good job of selecting a "next" URI, especially if they tried to do something with a dialog before being asked to log in.

In particular, if a logged-out user clicks an action like "Edit Blocking Tasks" on a Maniphest task, the default behavior is to send them to the standalone page for that dialog after they log in. This can be pretty confusing.

See T2691 and D6416 for earlier efforts here. At that time, we added a mechanism to //manually// override the default behavior, and fixed the most common links. This worked, but I'd like to fix the //default// beahvior so we don't need to remember to `setObjectURI()` correctly all over the place.

ApplicationEditor has also introduced new cases which are more difficult to get right. While we could get them right by using the override and being careful about things, this also motivates fixing the default behavior.

Finally, we have better tools for fixing the default behavior now than we did in 2013.

Instead of using manual overrides, have JS include an "X-Phabricator-Via" header in Ajax requests. This is basically like a referrer header, and will contain the page the user's browser is on.

In essentially every case, this should be a very good place (and often the best place) to send them after login. For all pages currently using `setObjectURI()`, it should produce the same behavior by default.

I'll remove the `setObjectURI()` mechanism in the next diff.

Test Plan: Clicked various workflow actions while logged out, saw "next" get set to a reasonable value, was redirected to a sensible, non-confusing page after login (the page with whatever button I clicked on it).

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10004

Differential Revision: https://secure.phabricator.com/D14804
2015-12-17 08:30:03 -08:00

345 lines
9.7 KiB
JavaScript

/**
* @requires javelin-install
* @provides javelin-quicksand
* @javelin
*/
/**
* Sink into a hopeless, cold mire of limitless depth from which there is
* no escape.
*
* Captures navigation events (like clicking links and using the back button)
* and expresses them in Javascript instead, emulating complex native browser
* behaviors in a language and context ill-suited to the task.
*
* By doing this, you abandon all hope and retreat to a world devoid of light
* or goodness. However, it allows you to have persistent UI elements which are
* not disrupted by navigation. A tempting trade, surely?
*
* To cast your soul into the darkness, use:
*
* JX.Quicksand
* .setFrame(node)
* .start();
*/
JX.install('Quicksand', {
statics: {
_id: null,
_onpage: 0,
_cursor: 0,
_current: 0,
_content: {},
_responses: {},
_history: [],
_started: false,
_frameNode: null,
_contentNode: null,
_uriPatternBlacklist: [],
/**
* Start Quicksand, accepting a fate of eternal torment.
*/
start: function(first_response) {
var self = JX.Quicksand;
if (self._started) {
return;
}
JX.Stratcom.listen('click', 'tag:a', self._onclick);
JX.Stratcom.listen('history:change', null, self._onchange);
self._started = true;
var path = JX.$U(window.location).getRelativeURI();
self._id = window.history.state || 0;
var id = self._id;
self._onpage = id;
self._history.push({path: path, id: id});
self._responses[id] = first_response;
},
/**
* Set the frame node which Quicksand controls content for.
*/
setFrame: function(frame) {
var self = JX.Quicksand;
self._frameNode = frame;
return self;
},
getCurrentPageID: function() {
var self = JX.Quicksand;
if (self._id === null) {
self._id = window.history.state || 0;
}
return self._id;
},
/**
* Respond to the user clicking a link.
*
* After a long list of checks, we may capture and simulate the resulting
* navigation.
*/
_onclick: function(e) {
var self = JX.Quicksand;
if (!self._frameNode) {
// If Quicksand has no frame, bail.
return;
}
if (JX.Stratcom.pass()) {
// If something else handled the event, bail.
return;
}
if (!e.isNormalClick()) {
// If this is a right-click, control click, etc., bail.
return;
}
if (e.getNode('workflow')) {
// Because JX.Workflow also passes these events, it might still want
// the event. Don't trigger if there's a workflow node in the stack.
return;
}
var a = e.getNode('tag:a');
var href = a.href;
if (!href || !href.length) {
// If the <a /> the user clicked has no href, or the href is empty,
// bail.
return;
}
if (href[0] == '#') {
// If this is an anchor on the current page, bail.
return;
}
var uri = new JX.$U(href);
var here = new JX.$U(window.location);
if (uri.getDomain() != here.getDomain()) {
// If the link is off-domain, bail.
return;
}
if (uri.getFragment() && uri.getPath() == here.getPath()) {
// If the link has an anchor but points at the current path, bail.
// This is presumably a long-form anchor on the current page.
// TODO: This technically gets links which change query parameters
// wrong: they are navigation events but we won't Quicksand them.
return;
}
if (self._isURIOnBlacklist(uri)) {
// This URI is blacklisted as not navigable via Quicksand.
return;
}
// The fate of this action is sealed. Suck it into the depths.
e.kill();
// If we're somewhere in history (that is, the user has pressed the
// back button one or more times, putting us in a state where pressing
// the forward button would do something) and we're navigating forward,
// all the stuff ahead of us is about to become unreachable when we
// navigate. Throw it away.
var discard = (self._history.length - self._cursor) - 1;
for (var ii = 0; ii < discard; ii++) {
var obsolete = self._history.pop();
self._responses[obsolete.id] = false;
}
// Set up the new state and fire a request to fetch the page data.
var path = JX.$U(uri).getRelativeURI();
var id = ++self._id;
self._history.push({path: path, id: id});
JX.History.push(path, id);
self._cursor = (self._history.length - 1);
self._responses[id] = null;
self._current = id;
new JX.Workflow(href, {__quicksand__: true})
.setHandler(JX.bind(null, self._onresponse, id))
.start();
},
/**
* Receive a response from the server with page data e.g. content.
*
* Usually we'll dump it into the page, but if the user clicked very fast
* it might already be out of date.
*/
_onresponse: function(id, r) {
var self = JX.Quicksand;
// Before possibly updating the document, check if this response is still
// relevant.
// We don't save the new response if the user has already destroyed
// the navigation. They can do this by pressing back, then clicking
// another link before the response can load.
if (self._responses[id] === false) {
return;
}
// Otherwise, this data is still relevant (either data on the current
// page, or data for a page that's still somewhere in history), so we
// save it.
var new_content = JX.$H(r.content).getFragment();
self._content[id] = new_content;
self._responses[id] = r;
// If it's the current page, draw it into the browser. It might not be
// the current page if the user already clicked another link.
if (self._current == id) {
self._draw(true);
}
},
/**
* Draw the current page.
*
* After a navigation event or the arrival of page content, we paint it
* onto the page.
*/
_draw: function(from_server) {
var self = JX.Quicksand;
if (self._onpage == self._current) {
// Don't bother redrawing if we're already on the current page.
return;
}
if (!self._responses[self._current]) {
// If we don't have this page yet, we can't draw it. We'll draw it
// when it arrives.
return;
}
// Otherwise, we're going to replace the page content. First, save the
// current page content. Modern computers have lots and lots of RAM, so
// there is no way this could ever create a problem.
var old = window.document.createDocumentFragment();
while (self._frameNode.firstChild) {
JX.DOM.appendContent(old, self._frameNode.firstChild);
}
self._content[self._onpage] = old;
// Now, replace it with the new content.
JX.DOM.setContent(self._frameNode, self._content[self._current]);
// Let other things redraw, etc as necessary
JX.Stratcom.invoke(
'quicksand-redraw',
null,
{
newResponse: self._responses[self._current],
newResponseID: self._current,
oldResponse: self._responses[self._onpage],
oldResponseID: self._onpage,
fromServer: from_server
});
self._onpage = self._current;
// Scroll to the top of the page and trigger any layout adjustments.
// TODO: Maybe store the scroll position?
JX.DOM.scrollToPosition(0, 0);
JX.Stratcom.invoke('resize');
},
/**
* Handle navigation events.
*
* In general, we're going to pull the content out of our history and dump
* it into the document.
*/
_onchange: function(e) {
var self = JX.Quicksand;
var data = e.getData();
data.state = data.state || null;
// Check if we're going back to the first page we started Quicksand on.
// We don't have a state value, but can look at the path.
if (data.state === null) {
if (JX.$U(window.location).getPath() == self._history[0].path) {
data.state = 0;
}
}
// Figure out where in history the user jumped to.
if (data.state !== null) {
self._current = data.state;
// Point the cursor at the right place in history.
for (var ii = 0; ii < self._history.length; ii++) {
if (self._history[ii].id == self._current) {
self._cursor = ii;
break;
}
}
// Redraw the page.
self._draw(false);
}
},
/**
* Set a list of regular expressions which blacklist URIs as not navigable
* via Quicksand.
*
* If a user clicks a link to one of these URIs, a normal page navigation
* event will occur instead of a Quicksand navigation.
*
* @param list<string> List of regular expressions.
* @return self
*/
setURIPatternBlacklist: function(items) {
var self = JX.Quicksand;
var list = [];
for (var ii = 0; ii < items.length; ii++) {
list.push(new RegExp('^' + items[ii] + '$'));
}
self._uriPatternBlacklist = list;
return self;
},
/**
* Test if a @{class:JX.URI} is on the URI pattern blacklist.
*
* @param JX.URI URI to test.
* @return bool True if the URI is on the blacklist.
*/
_isURIOnBlacklist: function(uri) {
var self = JX.Quicksand;
var list = self._uriPatternBlacklist;
var path = uri.getPath();
for (var ii = 0; ii < list.length; ii++) {
if (list[ii].test(path)) {
return true;
}
}
return false;
}
}
});