Skip to content
This repository has been archived by the owner on Jan 17, 2023. It is now read-only.

Commit

Permalink
Fix #2220, let sitehelper log you in if the website requests it
Browse files Browse the repository at this point in the history
Change auth.login() to return the login success
Change auth.login() to have options for asking about ownership information, and suppressing register-on-failed-login
Change server to do an ownership check on a shot on successful login
Add wantsauth script that is used to eagerly talk to the addon and try to initiate login
Change shot/controller to use wantsauth
  • Loading branch information
ianb committed Mar 17, 2017
1 parent ae3e9ff commit c53d27c
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 62 deletions.
45 changes: 36 additions & 9 deletions addon/webextension/background/auth.js
@@ -1,5 +1,5 @@
/* globals browser */
/* globals main, makeUuid, deviceInfo, analytics, catcher, defaultSentryDsn */
/* globals main, makeUuid, deviceInfo, analytics, catcher, defaultSentryDsn, communication */

window.auth = (function () {
let exports = {};
Expand Down Expand Up @@ -47,7 +47,7 @@ window.auth = (function () {
console.info("Registered login");
initialized = true;
saveAuthInfo(JSON.parse(req.responseText));
resolve();
resolve(true);
analytics.sendEvent("registered");
} else {
console.warn("Error in response:", req.responseText);
Expand All @@ -62,15 +62,19 @@ window.auth = (function () {
});
}

function login() {
function login(options) {
let { ownershipCheck, noRegister } = options || {};
return new Promise((resolve, reject) => {
let loginUrl = main.getBackend() + "/api/login";
let req = new XMLHttpRequest();
req.open("POST", loginUrl);
req.onload = catcher.watchFunction(() => {
if (req.status == 404) {
// No such user
resolve(register());
if (noRegister) {
resolve(false);
} else {
resolve(register());
}
} else if (req.status >= 300) {
console.warn("Error in response:", req.responseText);
reject(new Error("Could not log in: " + req.status));
Expand All @@ -80,17 +84,23 @@ window.auth = (function () {
reject(error);
} else {
initialized = true;
let jsonResponse = JSON.parse(req.responseText);
console.info("Page Shot logged in");
analytics.sendEvent("login");
saveAuthInfo(JSON.parse(req.responseText));
resolve();
saveAuthInfo(jsonResponse);
if (ownershipCheck) {
resolve({isOwner: jsonResponse.isOwner});
} else {
resolve(true);
}
}
});
req.setRequestHeader("content-type", "application/x-www-form-urlencoded");
req.send(uriEncode({
deviceId: registrationInfo.deviceId,
secret: registrationInfo.secret,
deviceInfo: JSON.stringify(deviceInfo())
deviceInfo: JSON.stringify(deviceInfo()),
ownershipCheck
}));
});
}
Expand All @@ -115,7 +125,9 @@ window.auth = (function () {
function uriEncode(obj) {
let s = [];
for (let key in obj) {
s.push(`${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`);
if (obj[key] !== undefined) {
s.push(`${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`);
}
}
return s.join("&");
}
Expand Down Expand Up @@ -171,5 +183,20 @@ window.auth = (function () {
});
};

communication.register("getAuthInfo", (sender, ownershipCheck) => {
let info = registrationInfo;
let done = Promise.resolve();
if (info.registered) {
done = login({ownershipCheck}).then((result) => {
if (result && result.isOwner) {
info.isOwner = true;
}
});
}
return done.then(() => {
return info;
});
});

return exports;
})();
6 changes: 5 additions & 1 deletion addon/webextension/manifest.json.template
Expand Up @@ -47,7 +47,11 @@
"content_scripts": [
{
"matches": ["http://localhost/*"],
"js": ["site-helper.js"],
"js": [
"catcher.js",
"selector/callBackground.js",
"sitehelper.js"
],
"run_at": "document_start"
}
],
Expand Down
48 changes: 0 additions & 48 deletions addon/webextension/site-helper.js

This file was deleted.

41 changes: 41 additions & 0 deletions addon/webextension/sitehelper.js
@@ -0,0 +1,41 @@
/* globals catcher, callBackground */
/** This is a content script added to all Page Shot pages, and allows the site to
communicate with the add-on */

window.sitehelper = (function () {

catcher.registerHandler((errorObj) => {
callBackground("reportError", errorObj);
});


function sendCustomEvent(name, detail) {
if (typeof detail == "object") {
// Note sending an object can lead to security problems, while a string
// is safe to transfer:
detail = JSON.stringify(detail);
}
document.dispatchEvent(new CustomEvent(name, {detail}));
}

document.addEventListener("delete-everything", catcher.watchFunction((event) => {
// FIXME: implement
alert("Not yet implemented");
}, false));

document.addEventListener("request-login", catcher.watchFunction((event) => {
let shotId = event.detail;
catcher.watchPromise(callBackground("getAuthInfo", shotId || null).then((info) => {
sendCustomEvent("login-successful", {deviceId: info.deviceId, isOwner: info.isOwner});
}));
}));

// Depending on the script loading order, the site might get the addon-present event,
// but probably won't - instead the site will ask for that event after it has loaded
document.addEventListener("request-addon-present", catcher.watchFunction(() => {
sendCustomEvent("addon-present");
}), false);

sendCustomEvent("addon-present");

})();
12 changes: 12 additions & 0 deletions server/src/pages/shot/controller.js
Expand Up @@ -11,6 +11,18 @@ let model;
exports.launch = function (data) {
let firstSet = ! model;
model = data;
if (window.wantsauth) {
if (window.wantsauth.getAuthData()) {
Object.assign(model, window.wantsauth.getAuthData());
model.isExtInstalled = true;
} else {
window.wantsauth.addAuthDataListener((data) => {
Object.assign(model, data);
model.isExtInstalled = true;
render();
});
}
}
model.hasSavedShot = false;
model.shot = new AbstractShot(data.backend, data.id, data.shot);
model.shot.contentUrl = `//${model.contentOrigin}/content/${model.shot.id}`;
Expand Down
3 changes: 3 additions & 0 deletions server/src/pages/shot/view.js
Expand Up @@ -70,6 +70,7 @@ class Head extends React.Component {
if (expired) {
return (
<reactruntime.HeadTemplate {...this.props}>
<script src={ this.props.staticLink("/static/js/wantsauth.js") } />
<script src={ this.props.staticLink("/static/js/shot-bundle.js") } async />
<link rel="stylesheet" href={ this.props.staticLink("/static/css/frame.css") } />
<meta name="viewport" content="width=device-width, initial-scale=1" />
Expand All @@ -79,6 +80,7 @@ class Head extends React.Component {
// FIXME: we need to review if the oembed form actually works and is valuable
return (
<reactruntime.HeadTemplate {...this.props}>
<script src={ this.props.staticLink("/static/js/wantsauth.js") } />
<script src={ this.props.staticLink("/static/js/shot-bundle.js") } async />
<link rel="stylesheet" href={ this.props.staticLink("/static/css/frame.css") } />
<meta name="viewport" content="width=device-width, initial-scale=1" />
Expand Down Expand Up @@ -256,6 +258,7 @@ class Body extends React.Component {

let myShotsHref = "/shots";
let myShotsText = <span className="back-to-index">My Shots <span className="arrow-icon"/></span>;
// FIXME: this means that on someone else's shot they won't see a My Shots link:
if (!this.props.isOwner) {
myShotsText = <span className="back-to-home">
<span className="sub">
Expand Down
25 changes: 21 additions & 4 deletions server/src/server.js
Expand Up @@ -526,7 +526,7 @@ app.post("/api/register", function (req, res) {
avatarurl: vars.avatarurl || null
}, canUpdate).then(function (userAbTests) {
if (userAbTests) {
sendAuthInfo(req, res, vars.deviceId, userAbTests);
sendAuthInfo(req, res, {deviceId: vars.deviceId, userAbTests});
// FIXME: send GA signal?
} else {
sendRavenMessage(req, "Attempted to register existing user", {
Expand Down Expand Up @@ -564,7 +564,8 @@ app.post("/api/update", function (req, res, next) {
}).catch(next);
});

function sendAuthInfo(req, res, deviceId, userAbTests) {
function sendAuthInfo(req, res, params) {
let { deviceId, userAbTests } = params;
if (deviceId.search(/^[a-zA-Z0-9_-]+$/) == -1) {
// FIXME: add logging message with deviceId
throw new Error("Bad deviceId");
Expand All @@ -579,7 +580,8 @@ function sendAuthInfo(req, res, deviceId, userAbTests) {
ok: "User created",
sentryPublicDSN: config.sentryPublicDSN,
abTests: userAbTests,
authHeader
authHeader,
isOwner: params.isOwner
};
// FIXME: I think there's a JSON sendResponse equivalent
simpleResponse(res, JSON.stringify(responseJson), 200);
Expand All @@ -601,7 +603,22 @@ app.post("/api/login", function (req, res) {
}
checkLogin(vars.deviceId, vars.secret, deviceInfo.addonVersion).then((userAbTests) => {
if (userAbTests) {
sendAuthInfo(req, res, vars.deviceId, userAbTests);
let sendParams = {
deviceId: vars.deviceId,
userAbTests
};
let sendParamsPromise = Promise.resolve(sendParams);
if (vars.ownershipCheck) {
sendParamsPromise = Shot.checkOwnership(vars.ownershipCheck, vars.deviceId).then((isOwner) => {
sendParams.isOwner = isOwner;
return sendParams;
});
}
sendParamsPromise.then((params) => {
sendAuthInfo(req, res, params);
}).catch((error) => {
errorResponse(res, "Error checking ownership", error);
});
if (config.gaId) {
let userAnalytics = ua(config.gaId, vars.deviceId, {strictCidFormat: false});
userAnalytics.event({
Expand Down
9 changes: 9 additions & 0 deletions server/src/servershot.js
Expand Up @@ -394,6 +394,15 @@ Shot.getRawValue = function (id, deviceId) {
});
};

Shot.checkOwnership = function (shotId, deviceId) {
return db.select(
`SELECT id FROM data WHERE id = $1 AND deviceid = $2`,
[shotId, deviceId]
).then((rows) => {
return !! rows.length;
});
};

Shot.getShotsForDevice = function (backend, deviceId, searchQuery) {
if (! deviceId) {
throw new Error("Empty deviceId: " + deviceId);
Expand Down
60 changes: 60 additions & 0 deletions static/js/wantsauth.js
@@ -0,0 +1,60 @@
/** This allows for early communication with the add-on to ask for authentication information
Including this script on a page indicates that the page would like to be authenticated
(and that it isn't currently authenticated)
A controller might use this like:
if (window.wantsauth) {
if (window.wantsauth.getAuthData()) {
add window.wantsauth.getAuthData() to model
} else {
window.wantsauth.addAuthDataListener((data) => {
add data to model
});
}
}
*/
window.wantsauth = (function () {
let exports = {};

let savedAuthData = null;
let authDataCallbacks = [];

// Note that this module is only loosely bound to any controller, but there
// is special logic for view pages where ownership is interesting in addition to
// authentication. As a result we have to parse the URL on our own:
let maybeShotId = location.href.replace(/^https?:\/\/[^/]+\//i, "");
maybeShotId = maybeShotId.replace(/\?.*/, "").replace(/#.*/, "");
if (maybeShotId.search(/[a-z0-9]+\/[a-z0-9.]+$/i) === -1) {
// Not a shot ID, which should look like {stuff}/{stuff}
maybeShotId = null;
}

// These events are used to communicate with sitehelper.js:
document.addEventListener("login-successful", (event) => {
let {deviceId, isOwner} = JSON.parse(event.detail);
savedAuthData = {
deviceId,
isOwner
};
for (let callback of authDataCallbacks) {
callback(savedAuthData);
}
}, false);

document.addEventListener("addon-present", () => {
document.dispatchEvent(new CustomEvent("request-login", {detail: maybeShotId}));
}, false);

document.dispatchEvent(new CustomEvent("request-addon-present"));

exports.getAuthData = function () {
return savedAuthData;
};

exports.addAuthDataListener = function (func) {
authDataCallbacks.push(func);
};

return exports;
})();

0 comments on commit c53d27c

Please sign in to comment.