UNPKG

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
(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);