hellojs-xiaotian
Version:
A clientside Javascript library for standardizing requests to OAuth2 web services (and OAuth1 - with a shim)
587 lines (474 loc) • 13.5 kB
JavaScript
(function(hello) {
var contactsUrl = 'https://www.google.com/m8/feeds/contacts/default/full?v=3.0&alt=json&max-results=@{limit|1000}&start-index=@{start|1}';
hello.init({
google: {
name: 'Google Sign-In',
// See: http://code.google.com/apis/accounts/docs/OAuth2UserAgent.html
oauth: {
version: 2,
auth: 'https://accounts.google.com/o/oauth2/v2/auth',
grant: 'https://www.googleapis.com/oauth2/v4/token'
},
// Authorization scopes
scope: {
basic: 'openid profile',
email: 'email',
birthday: '',
events: '',
photos: 'https://picasaweb.google.com/data/',
videos: 'http://gdata.youtube.com',
files: 'https://www.googleapis.com/auth/drive.readonly',
publish: '',
publish_files: 'https://www.googleapis.com/auth/drive',
share: '',
create_event: '',
offline_access: ''
},
scope_delim: ' ',
login: function(p) {
if (p.qs.response_type === 'code') {
// Let's set this to an offline access to return a refresh_token
p.qs.access_type = 'offline';
}
else if (p.qs.response_type.indexOf('id_token') > -1) {
p.qs.nonce = parseInt(Math.random() * 1e12, 10).toString(36);
}
// Reauthenticate
// https://developers.google.com/identity/protocols/
if (p.options.force) {
p.qs.approval_prompt = 'force';
}
},
// API base URI
base: 'https://www.googleapis.com/',
// Map GET requests
get: {
me: 'oauth2/v3/userinfo?alt=json',
// Deprecated Sept 1, 2014
//'me': 'oauth2/v1/userinfo?alt=json',
// See: https://developers.google.com/+/api/latest/people/list
'me/following': contactsUrl,
'me/followers': contactsUrl,
'me/contacts': contactsUrl,
'me/albums': 'https://picasaweb.google.com/data/feed/api/user/default?alt=json&max-results=@{limit|100}&start-index=@{start|1}',
'me/album': function(p, callback) {
var key = p.query.id;
delete p.query.id;
callback(key.replace('/entry/', '/feed/'));
},
'me/photos': 'https://picasaweb.google.com/data/feed/api/user/default?alt=json&kind=photo&max-results=@{limit|100}&start-index=@{start|1}',
// See: https://developers.google.com/drive/v2/reference/files/list
'me/file': 'drive/v2/files/@{id}',
'me/files': 'drive/v2/files?q=%22@{parent|root}%22+in+parents+and+trashed=false&maxResults=@{limit|100}',
// See: https://developers.google.com/drive/v2/reference/files/list
'me/folders': 'drive/v2/files?q=%22@{id|root}%22+in+parents+and+mimeType+=+%22application/vnd.google-apps.folder%22+and+trashed=false&maxResults=@{limit|100}',
// See: https://developers.google.com/drive/v2/reference/files/list
'me/folder': 'drive/v2/files?q=%22@{id|root}%22+in+parents+and+trashed=false&maxResults=@{limit|100}'
},
// Map POST requests
post: {
// Google Drive
'me/files': uploadDrive,
'me/folders': function(p, callback) {
p.data = {
title: p.data.name,
parents: [{id: p.data.parent || 'root'}],
mimeType: 'application/vnd.google-apps.folder'
};
callback('drive/v2/files');
}
},
// Map PUT requests
put: {
'me/files': uploadDrive
},
// Map DELETE requests
del: {
'me/files': 'drive/v2/files/@{id}',
'me/folder': 'drive/v2/files/@{id}'
},
// Map PATCH requests
patch: {
'me/file': 'drive/v2/files/@{id}'
},
wrap: {
me: function(o) {
if (o.sub) {
o.id = o.sub;
}
if (o.id) {
o.last_name = o.family_name || (o.name ? o.name.familyName : null);
o.first_name = o.given_name || (o.name ? o.name.givenName : null);
if (o.emails && o.emails.length) {
o.email = o.emails[0].value;
}
formatPerson(o);
}
return o;
},
'me/friends': function(o) {
if (o.items) {
paging(o);
o.data = o.items;
o.data.forEach(formatPerson);
delete o.items;
}
return o;
},
'me/contacts': formatFriends,
'me/followers': formatFriends,
'me/following': formatFriends,
'me/share': formatFeed,
'me/feed': formatFeed,
'me/albums': gEntry,
'me/photos': formatPhotos,
'default': gEntry
},
xhr: function(p) {
if (p.method === 'post' || p.method === 'put') {
toJSON(p);
}
else if (p.method === 'patch') {
hello.utils.extend(p.query, p.data);
p.data = null;
}
return true;
},
// Don't even try submitting via form.
// This means no POST operations in <=IE9
form: false
}
});
function toInt(s) {
return parseInt(s, 10);
}
function formatFeed(o) {
paging(o);
o.data = o.items;
delete o.items;
return o;
}
// Format: ensure each record contains a name, id etc.
function formatItem(o) {
if (o.error) {
return;
}
if (!o.name) {
o.name = o.title || o.message;
}
if (!o.picture) {
o.picture = o.thumbnailLink;
}
if (!o.thumbnail) {
o.thumbnail = o.thumbnailLink;
}
if (o.mimeType === 'application/vnd.google-apps.folder') {
o.type = 'folder';
o.files = 'https://www.googleapis.com/drive/v2/files?q=%22' + o.id + '%22+in+parents';
}
return o;
}
function formatImage(image) {
return {
source: image.url,
width: image.width,
height: image.height
};
}
function formatPhotos(o) {
if ('feed' in o) {
o.data = 'entry' in o.feed ? o.feed.entry.map(formatEntry) : [];
delete o.feed;
}
return o;
}
// Google has a horrible JSON API
function gEntry(o) {
paging(o);
if ('feed' in o && 'entry' in o.feed) {
o.data = o.feed.entry.map(formatEntry);
delete o.feed;
}
// Old style: Picasa, etc.
else if ('entry' in o) {
return formatEntry(o.entry);
}
// New style: Google Drive
else if ('items' in o) {
o.data = o.items.map(formatItem);
delete o.items;
}
else {
formatItem(o);
}
return o;
}
function formatPerson(o) {
o.name = o.displayName || o.name;
o.picture = o.picture || (o.image ? o.image.url : null);
o.thumbnail = o.picture;
}
function formatFriends(o, headers, req) {
paging(o);
var r = [];
if ('feed' in o && 'entry' in o.feed) {
var token = req.query.access_token;
for (var i = 0; i < o.feed.entry.length; i++) {
var a = o.feed.entry[i];
a.id = a.id.$t;
a.name = a.title.$t;
delete a.title;
if (a.gd$email) {
a.email = (a.gd$email && a.gd$email.length > 0) ? a.gd$email[0].address : null;
a.emails = a.gd$email;
delete a.gd$email;
}
if (a.updated) {
a.updated = a.updated.$t;
}
if (a.link) {
var pic = (a.link.length > 0) ? a.link[0].href : null;
if (pic && a.link[0].gd$etag) {
pic += (pic.indexOf('?') > -1 ? '&' : '?') + 'access_token=' + token;
a.picture = pic;
a.thumbnail = pic;
}
delete a.link;
}
if (a.category) {
delete a.category;
}
}
o.data = o.feed.entry;
delete o.feed;
}
return o;
}
function formatEntry(a) {
var group = a.media$group;
var photo = group.media$content.length ? group.media$content[0] : {};
var mediaContent = group.media$content || [];
var mediaThumbnail = group.media$thumbnail || [];
var pictures = mediaContent
.concat(mediaThumbnail)
.map(formatImage)
.sort(function(a, b) {
return a.width - b.width;
});
var i = 0;
var _a;
var p = {
id: a.id.$t,
name: a.title.$t,
description: a.summary.$t,
updated_time: a.updated.$t,
created_time: a.published.$t,
picture: photo ? photo.url : null,
pictures: pictures,
images: [],
thumbnail: photo ? photo.url : null,
width: photo.width,
height: photo.height
};
// Get feed/children
if ('link' in a) {
for (i = 0; i < a.link.length; i++) {
var d = a.link[i];
if (d.rel.match(/\#feed$/)) {
p.upload_location = p.files = p.photos = d.href;
break;
}
}
}
// Get images of different scales
if ('category' in a && a.category.length) {
_a = a.category;
for (i = 0; i < _a.length; i++) {
if (_a[i].scheme && _a[i].scheme.match(/\#kind$/)) {
p.type = _a[i].term.replace(/^.*?\#/, '');
}
}
}
// Get images of different scales
if ('media$thumbnail' in group && group.media$thumbnail.length) {
_a = group.media$thumbnail;
p.thumbnail = _a[0].url;
p.images = _a.map(formatImage);
}
_a = group.media$content;
if (_a && _a.length) {
p.images.push(formatImage(_a[0]));
}
return p;
}
function paging(res) {
// Contacts V2
if ('feed' in res && res.feed.openSearch$itemsPerPage) {
var limit = toInt(res.feed.openSearch$itemsPerPage.$t);
var start = toInt(res.feed.openSearch$startIndex.$t);
var total = toInt(res.feed.openSearch$totalResults.$t);
if ((start + limit) < total) {
res.paging = {
next: '?start=' + (start + limit)
};
}
}
else if ('nextPageToken' in res) {
res.paging = {
next: '?pageToken=' + res.nextPageToken
};
}
}
// Construct a multipart message
function Multipart() {
// Internal body
var body = [];
var boundary = (Math.random() * 1e10).toString(32);
var counter = 0;
var lineBreak = '\r\n';
var delim = lineBreak + '--' + boundary;
var ready = function() {};
var dataUri = /^data\:([^;,]+(\;charset=[^;,]+)?)(\;base64)?,/i;
// Add file
function addFile(item) {
var fr = new FileReader();
fr.onload = function(e) {
addContent(btoa(e.target.result), item.type + lineBreak + 'Content-Transfer-Encoding: base64');
};
fr.readAsBinaryString(item);
}
// Add content
function addContent(content, type) {
body.push(lineBreak + 'Content-Type: ' + type + lineBreak + lineBreak + content);
counter--;
ready();
}
// Add new things to the object
this.append = function(content, type) {
// Does the content have an array
if (typeof (content) === 'string' || !('length' in Object(content))) {
// Converti to multiples
content = [content];
}
for (var i = 0; i < content.length; i++) {
counter++;
var item = content[i];
// Is this a file?
// Files can be either Blobs or File types
if (
(typeof (File) !== 'undefined' && item instanceof File) ||
(typeof (Blob) !== 'undefined' && item instanceof Blob)
) {
// Read the file in
addFile(item);
}
// Data-URI?
// Data:[<mime type>][;charset=<charset>][;base64],<encoded data>
// /^data\:([^;,]+(\;charset=[^;,]+)?)(\;base64)?,/i
else if (typeof (item) === 'string' && item.match(dataUri)) {
var m = item.match(dataUri);
addContent(item.replace(dataUri, ''), m[1] + lineBreak + 'Content-Transfer-Encoding: base64');
}
// Regular string
else {
addContent(item, type);
}
}
};
this.onready = function(fn) {
ready = function() {
if (counter === 0) {
// Trigger ready
body.unshift('');
body.push('--');
fn(body.join(delim), boundary);
body = [];
}
};
ready();
};
}
// Upload to Drive
// If this is PUT then only augment the file uploaded
// PUT https://developers.google.com/drive/v2/reference/files/update
// POST https://developers.google.com/drive/manage-uploads
function uploadDrive(p, callback) {
var data = {};
// Test for DOM element
if (p.data &&
(typeof (HTMLInputElement) !== 'undefined' && p.data instanceof HTMLInputElement)
) {
p.data = {file: p.data};
}
if (!p.data.name && Object(Object(p.data.file).files).length && p.method === 'post') {
p.data.name = p.data.file.files[0].name;
}
if (p.method === 'post') {
p.data = {
title: p.data.name,
parents: [{id: p.data.parent || 'root'}],
file: p.data.file
};
}
else {
// Make a reference
data = p.data;
p.data = {};
// Add the parts to change as required
if (data.parent) {
p.data.parents = [{id: p.data.parent || 'root'}];
}
if (data.file) {
p.data.file = data.file;
}
if (data.name) {
p.data.title = data.name;
}
}
// Extract the file, if it exists from the data object
// If the File is an INPUT element lets just concern ourselves with the NodeList
var file;
if ('file' in p.data) {
file = p.data.file;
delete p.data.file;
if (typeof (file) === 'object' && 'files' in file) {
// Assign the NodeList
file = file.files;
}
if (!file || !file.length) {
callback({
error: {
code: 'request_invalid',
message: 'There were no files attached with this request to upload'
}
});
return;
}
}
// Set type p.data.mimeType = Object(file[0]).type || 'application/octet-stream';
// Construct a multipart message
var parts = new Multipart();
parts.append(JSON.stringify(p.data), 'application/json');
// Read the file into a base64 string... yep a hassle, i know
// FormData doesn't let us assign our own Multipart headers and HTTP Content-Type
// Alas GoogleApi need these in a particular format
if (file) {
parts.append(file);
}
parts.onready(function(body, boundary) {
p.headers['content-type'] = 'multipart/related; boundary="' + boundary + '"';
p.data = body;
callback('upload/drive/v2/files' + (data.id ? '/' + data.id : '') + '?uploadType=multipart');
});
}
function toJSON(p) {
if (typeof (p.data) === 'object') {
// Convert the POST into a javascript object
try {
p.data = JSON.stringify(p.data);
p.headers['content-type'] = 'application/json';
}
catch (e) {}
}
}
})(hello);