Skip to content
This repository has been archived by the owner on Apr 3, 2019. It is now read-only.

Commit

Permalink
fix(avatars): add profile server client to proxy remote images
Browse files Browse the repository at this point in the history
  • Loading branch information
zaach committed Aug 11, 2014
1 parent d61d9e2 commit 899f189
Show file tree
Hide file tree
Showing 18 changed files with 589 additions and 111 deletions.
20 changes: 18 additions & 2 deletions app/scripts/lib/auth-errors.js
Expand Up @@ -50,7 +50,8 @@ function () {
EMAIL_REQUIRED: 1011,
YEAR_OF_BIRTH_REQUIRED: 1012,
UNUSABLE_IMAGE: 1013,
NO_CAMERA: 1014
NO_CAMERA: 1014,
URL_REQUIRED: 1015
};

var CODE_TO_MESSAGES = {
Expand Down Expand Up @@ -89,7 +90,8 @@ function () {
1011: t('Valid email required'),
1012: t('Year of birth required'),
1013: t('A usable image was not found'),
1014: t('Could not initialize camera')
1014: t('Could not initialize camera'),
1015: t('Valid URL required')
};

return {
Expand Down Expand Up @@ -173,6 +175,20 @@ function () {
is: function (error, type) {
var code = this.toCode(type);
return error.errno === code;
},

normalizeXHRError: function (xhr) {
if (! xhr || xhr.status === 0) {
return this.toError('SERVICE_UNAVAILABLE');
}

var errObj = xhr.responseJSON;

if (! errObj) {
return this.toError('UNEXPECTED_ERROR');
}

return this.toError(errObj.errno);
}
};
});
27 changes: 11 additions & 16 deletions app/scripts/lib/oauth-client.js
Expand Up @@ -26,20 +26,6 @@ function ($, p, Session, ConfigLoader, OAuthErrors) {
}
}

function normalizeError(xhr) {
if (! xhr || xhr.status === 0) {
return OAuthErrors.toError('SERVICE_UNAVAILABLE');
}

var errObj = xhr.responseJSON;

if (! errObj) {
return OAuthErrors.toError('UNEXPECTED_ERROR');
}

return OAuthErrors.toError(errObj.errno);
}

OAuthClient.prototype = {
_getOauthUrl: function _getOauthUrl() {
var configLoader = new ConfigLoader();
Expand All @@ -66,7 +52,7 @@ function ($, p, Session, ConfigLoader, OAuthErrors) {
return this._getOauthUrl().then(function (url) {
return p.jQueryXHR($.post(url + GET_CODE, params))
.then(null, function(xhr) {
var err = normalizeError(xhr);
var err = OAuthErrors.normalizeXHRError(xhr);
throw err;
});
});
Expand All @@ -76,10 +62,19 @@ function ($, p, Session, ConfigLoader, OAuthErrors) {
return this._getOauthUrl().then(function (url) {
return p.jQueryXHR($.get(url + GET_CLIENT + id))
.then(null, function(xhr) {
var err = normalizeError(xhr);
var err = OAuthErrors.normalizeXHRError(xhr);
throw err;
});
});
},

// params = { assertion, client_id, scope }
getToken: function (params) {
/* jshint camelcase: false */

// Use the special 'token' response type
params.response_type = 'token';
return this.getCode(params);
}
};

Expand Down
90 changes: 90 additions & 0 deletions app/scripts/lib/profile-client.js
@@ -0,0 +1,90 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

'use strict';

define([
'jquery',
'underscore',
'lib/promise',
'lib/session',
'lib/config-loader',
'lib/oauth-client',
'lib/assertion',
'lib/auth-errors'
],
function ($, _, p, Session, ConfigLoader, OAuthClient, Assertion, AuthErrors) {

function ProfileClient(options) {
this.profileUrl = options.profileUrl;

// an OAuth access token
this.token = options.token;
}

ProfileClient.prototype._request = function (path, type, data, headers) {
var url = this.profileUrl;

var request = {
url: url + path,
type: type,
headers: {
Authorization: 'Bearer ' + this.token
}
};

if (data) {
request.data = data;
}
if (headers) {
_.merge(request.headers, headers);
}

return p.jQueryXHR($.ajax(request))
.then(null, function(xhr) {
var err = ProfileErrors.normalizeXHRError(xhr);
throw err;
});
};

// Returns the user's profile data
// including: email, uid
ProfileClient.prototype.getProfile = function () {
return this._request('/v1/profile', 'get');
};

// Returns remote image data
ProfileClient.prototype.getRemoteImage = function (url) {
return this._request('/v1/remote_image/' + encodeURIComponent(url), 'get');
};

var t = function (msg) {
return msg;
};

var ERROR_TO_CODE = {
UNAUTHORIZED: 100,
INVALID_PARAMETER: 101,
// local only errors.
SERVICE_UNAVAILABLE: 998,
UNEXPECTED_ERROR: 999
};

var CODE_TO_MESSAGES = {
// errors returned by the profile server
100: t('Unexpected error'),
101: t('Invalid parameter in request body: %(param)s'),
// local only errors.
998: t('System unavailable, try again soon'),
999: t('Unexpected error')
};

var ProfileErrors = ProfileClient.Errors = _.extend({}, AuthErrors, {
ERROR_TO_CODE: ERROR_TO_CODE,
CODE_TO_MESSAGES: CODE_TO_MESSAGES
});

return ProfileClient;
});

2 changes: 1 addition & 1 deletion app/scripts/lib/session.js
Expand Up @@ -14,7 +14,7 @@ define([
var NAMESPACE = '__fxa_session';

// and should not be saved to sessionStorage
var DO_NOT_PERSIST = ['client_id', 'prefillPassword', 'prefillYear', 'error', 'service'];
var DO_NOT_PERSIST = ['client_id', 'prefillPassword', 'prefillYear', 'error', 'service', 'cropImgWidth', 'cropImgHeight', 'cropImgSrc'];

// Don't clear service because the signup page needs that state
// even when user credentials are cleared.
Expand Down
32 changes: 18 additions & 14 deletions app/scripts/views/settings/avatar_change.js
Expand Up @@ -58,6 +58,22 @@ function ($, _, FormView, Template, Session, AuthErrors) {
var self = this;
var file = e.target.files[0];

// Define our callbacks here to avoid a circular DOM reference
var imgOnload = function () {
// Store the width and height for the cropper view
Session.set('cropImgWidth', this.width);
Session.set('cropImgHeight', this.height);
require(['../bower_components/jquery-ui/ui/draggable'], function () {
self.navigate('settings/avatar/crop');
});
};

var imgOnerrer = function () {
self.navigate('settings/avatar', {
error: AuthErrors.toMessage('UNUSABLE_IMAGE')
});
};

if (file.type.match('image.*')) {
var reader = new self.FileReader();

Expand All @@ -68,20 +84,8 @@ function ($, _, FormView, Template, Session, AuthErrors) {

var img = new Image();
img.src = src;
img.onload = function () {

require(['../bower_components/jquery-ui/ui/draggable'], function (ui) {
Session.set('cropImgWidth', img.width);
Session.set('cropImgHeight', img.height);

self.navigate('settings/avatar/crop');
});
};
img.onerror = function () {
self.navigate('settings/avatar', {
error: AuthErrors.toMessage('UNUSABLE_IMAGE')
});
};
img.onload = imgOnload;
img.onerror = imgOnerrer;
};
reader.readAsDataURL(file);
} else {
Expand Down
89 changes: 79 additions & 10 deletions app/scripts/views/settings/avatar_url.js
Expand Up @@ -9,9 +9,38 @@ define([
'views/form',
'stache!templates/settings/avatar_url',
'lib/session',
'lib/oauth-client',
'lib/profile-client',
'lib/assertion',
'lib/auth-errors'
],
function (_, FormView, Template, Session, AuthErrors) {
function (_, FormView, Template, Session, OAuthClient, ProfileClient, Assertion, AuthErrors) {
// A short/effective regex taken from http://mathiasbynens.be/demo/url-regex
var urlRegex = /https?:\/\/(-\.)?([^\s\/?\.#-]+\.?)+(\/[^\s]*)?$/i;

function getProfileClient() {
/* jshint camelcase: false */
var config = Session.config;
var params = {
client_id: config.oauthClientId
};
var oauthClient = new OAuthClient({
oauthUrl: config.oauthUrl
});

return Assertion.generate(config.oauthUrl)
.then(function(assertion) {
params.assertion = assertion;
return oauthClient.getToken(params);
})
.then(function(result) {
var profileClient = new ProfileClient({
token: result.access_token,
profileUrl: config.profileUrl
});
return profileClient;
});
}

var View = FormView.extend({
// user must be authenticated to see Settings
Expand All @@ -26,24 +55,64 @@ function (_, FormView, Template, Session, AuthErrors) {
};
},

isValidEnd: function () {
return this._validateUrl();
},

showValidationErrorsEnd: function () {
if (! this._validateUrl()) {
this.showValidationError('.url', AuthErrors.toError('URL_REQUIRED'));
}
},

_validateUrl: function () {
var url = $.trim(this.$('.url').val());

return !!(url && urlRegex.test(url));
},

// Load the remote image into a canvas and prepare it for cropping
submit: function () {
var self = this;
var src = this.$('.url').val();
var img = new Image();
img.src = src;
img.onload = function () {
Session.set('cropImgWidth', img.width);
Session.set('cropImgHeight', img.height);
self.navigate('settings/avatar/crop');

// Define our callbacks here to avoid a circular DOM reference
var imgOnload = function () {
// Store the width and height for the cropper view
Session.set('cropImgWidth', this.width);
Session.set('cropImgHeight', this.height);
require(['../bower_components/jquery-ui/ui/draggable'], function () {
self.navigate('settings/avatar/crop');
});
};
img.onerror = function () {

var imgOnerrer = function () {
self.navigate('settings/avatar', {
error: AuthErrors.toMessage('UNUSABLE_IMAGE')
});
};
Session.set('cropImgSrc', src);

return this.getRemoteImageSrc(this.$('.url').val())
.then(function (src) {
var img = new Image();
img.src = src;

img.onload = imgOnload;
img.onerror = imgOnerrer;

Session.set('cropImgSrc', src);
});
},

getRemoteImageSrc: function (url) {
return getProfileClient()
.then(function(profileClient) {
return profileClient.getRemoteImage(url);
})
.then(function(src) {
return 'data:image/jpeg;base64,' + src;
});
}

});

return View;
Expand Down

0 comments on commit 899f189

Please sign in to comment.