diff --git a/forceng.js b/forceng.js index 435b9a1..dbf91f7 100644 --- a/forceng.js +++ b/forceng.js @@ -1,11 +1,12 @@ /** * ForceNG - REST toolkit for Salesforce.com * Author: Christophe Coenraets @ccoenraets - * Version: 0.6.1 + * Edited: Krzysztof Pintscher k.pintscher@polsource.com @niou-ns + * Version: 0.8 */ angular.module('forceng', []) - .factory('force', function ($rootScope, $q, $window, $http) { + .factory('force', function ($rootScope, $q, $window, $http, $httpParamSerializer, $timeout) { // The login URL for the OAuth process // To override default, pass loginURL in init(props) @@ -18,13 +19,13 @@ angular.module('forceng', []) // The force.com API version to use. // To override default, pass apiVersion in init(props) - apiVersion = 'v33.0', + apiVersion = 'v39.0', - // Keep track of OAuth data (access_token, refresh_token, and instance_url) + // Keep track of OAuth data (access_token, refresh_token, instance_url, user_id and org_id) oauth, - // By default we store fbtoken in sessionStorage. This can be overridden in init() - tokenStore = {}, + // By default we store token in sessionStorage. This can be overridden in init() + tokenStore = $window.sessionStorage, // if page URL is http://localhost:3000/myapp/index.html, context is /myapp context = window.location.pathname.substring(0, window.location.pathname.lastIndexOf("/")), @@ -50,9 +51,17 @@ angular.module('forceng', []) // Reference to the Salesforce OAuth plugin oauthPlugin, + // Reference to the Salesforce Network plugin + networkPlugin, + + // Where or not to use cordova for oauth and network calls + useCordova = window.cordova ? true : false, + // Whether or not to use a CORS proxy. Defaults to false if app running in Cordova or in a VF page // Can be overriden in init() - useProxy = (window.cordova || window.SfdcApp) ? false : true; + useProxy = (window.cordova || window.SfdcApp || window.sforce) ? false : true, + + retry = false; /* * Determines the request base URL. @@ -99,7 +108,8 @@ angular.module('forceng', []) return parts.join("&"); } - function refreshTokenWithPlugin(deferred) { + function refreshTokenWithPlugin() { + var deferred = $q.defer(); oauthPlugin.authenticate( function (response) { oauth.access_token = response.accessToken; @@ -110,16 +120,21 @@ angular.module('forceng', []) console.log('Error refreshing oauth access token using the oauth plugin'); deferred.reject(); }); + return deferred.promise; } - function refreshTokenWithHTTPRequest(deferred) { + function refreshTokenWithHTTPRequest() { + var deferred = $q.defer(); var params = { 'grant_type': 'refresh_token', 'refresh_token': oauth.refresh_token, 'client_id': appId }, - headers = {}, + headers = { + 'Content-Type': 'application/json', + 'Target-URL': loginURL + }, url = useProxy ? proxyURL : loginURL; @@ -130,15 +145,11 @@ angular.module('forceng', []) url = url + '/services/oauth2/token?' + toQueryString(params); - if (!useProxy) { - headers["Target-URL"] = loginURL; - } - $http({ headers: headers, method: 'POST', url: url, - params: params + data: params }) .success(function (data, status, headers, config) { console.log('Token refreshed'); @@ -150,16 +161,19 @@ angular.module('forceng', []) console.log('Error while trying to refresh token'); deferred.reject(); }); + return deferred.promise; } function refreshToken() { - var deferred = $q.defer(); - if (oauthPlugin) { - refreshTokenWithPlugin(deferred); + if (useCordova) { + if (!oauthPlugin) { + console.error('Salesforce Mobile SDK OAuth plugin not available'); + } else { + return refreshTokenWithPlugin(); + } } else { - refreshTokenWithHTTPRequest(deferred); + return refreshTokenWithHTTPRequest(); } - return deferred.promise; } /** @@ -183,6 +197,7 @@ angular.module('forceng', []) oauthCallbackURL = params.oauthCallbackURL || oauthCallbackURL; proxyURL = params.proxyURL || proxyURL; useProxy = params.useProxy === undefined ? useProxy : params.useProxy; + useCordova = params.useCordova === undefined ? useCordova : params.useCordova; if (params.accessToken) { if (!oauth) oauth = {}; @@ -198,6 +213,35 @@ angular.module('forceng', []) if (!oauth) oauth = {}; oauth.refresh_token = params.refreshToken; } + + if (params.userId) { + if (!oauth) oauth = {}; + oauth.user_id = params.userId; + } + + if (params.orgId) { + if (!oauth) oauth = {}; + oauth.org_id = params.orgId; + } + + // Load previously saved token + if (tokenStore['forceOAuth']) { + oauth = JSON.parse(tokenStore['forceOAuth']); + } + + if (useCordova) { + document.addEventListener("deviceready", function () { + try { + networkPlugin = cordova.require("com.salesforce.plugin.network"); + } catch(e) { + // fail silently + } + if (!networkPlugin) { + console.log('Salesforce Mobile SDK Network plugin not available'); + } + }); + } + } console.log("useProxy: " + useProxy); @@ -217,7 +261,7 @@ angular.module('forceng', []) */ function oauthCallback(url) { - // Parse the OAuth data received from Facebook + // Parse the OAuth data received from Salesforce var queryString, obj; @@ -225,6 +269,10 @@ angular.module('forceng', []) queryString = url.substr(url.indexOf('#') + 1); obj = parseQueryString(queryString); oauth = obj; + // Paring out user id + var oauthId = oauth.id.split('/'); + oauth.user_id = oauthId.pop(); + oauth.org_id = oauthId.pop(); tokenStore['forceOAuth'] = JSON.stringify(oauth); if (deferredLogin) deferredLogin.resolve(); } else if (url.indexOf("error=") > 0) { @@ -241,7 +289,7 @@ angular.module('forceng', []) */ function login() { deferredLogin = $q.defer(); - if (window.cordova) { + if (useCordova) { loginWithPlugin(); } else { loginWithBrowser(); @@ -249,9 +297,27 @@ angular.module('forceng', []) return deferredLogin.promise; } + function logout() { + if (useCordova) { + oauthPlugin = cordova.require("com.salesforce.plugin.oauth"); + if (!oauthPlugin) { + console.error('Salesforce Mobile SDK OAuth plugin not available'); + } else { + oauthPlugin.logout(); + } + } else { + tokenStore.clear(); + $window.location.reload(); + } + } + function loginWithPlugin() { document.addEventListener("deviceready", function () { - oauthPlugin = cordova.require("com.salesforce.plugin.oauth"); + try { + oauthPlugin = cordova.require("com.salesforce.plugin.oauth"); + } catch(e) { + // fail silently + } if (!oauthPlugin) { console.error('Salesforce Mobile SDK OAuth plugin not available'); if (deferredLogin) deferredLogin.reject({status: 'Salesforce Mobile SDK OAuth plugin not available'}); @@ -260,7 +326,12 @@ angular.module('forceng', []) oauthPlugin.getAuthCredentials( function (creds) { // Initialize ForceJS - init({accessToken: creds.accessToken, instanceURL: creds.instanceUrl, refreshToken: creds.refreshToken}); + init({accessToken: creds.accessToken, instanceURL: creds.instanceUrl, refreshToken: creds.refreshToken, userId: creds.userId, orgId: creds.orgId}); + if (oauth) { + tokenStore['forceOAuth'] = JSON.stringify(oauth); + } else { + console.log('oauth object is not present'); + } if (deferredLogin) deferredLogin.resolve(); }, function (error) { @@ -285,7 +356,43 @@ angular.module('forceng', []) * @returns {string} | undefined */ function getUserId() { - return (typeof(oauth) !== 'undefined') ? oauth.id.split('/').pop() : undefined; + return (typeof(oauth) !== 'undefined') ? oauth.user_id : undefined; + } + + /** + * Gets the user's Org ID (if logged in) + * @returns {string} | undefined + */ + function getOrgId() { + return (typeof(oauth) !== 'undefined') ? oauth.org_id : undefined; + } + + function getSFAccountManager() { + return cordova.require('com.salesforce.plugin.sfaccountmanager'); + } + + function getCurrentUser() { + var deferred = $q.defer(); + if (useCordova) { + var sfAccountManager = getSFAccountManager(); + sfAccountManager.getCurrentUser(function(result) { + deferred.resolve(result); + }, function(error) { + deferred.reject(error); + }) + } else { + chatter({path: '/users/me'}) + .then(function(result) { + deferred.resolve(result); + }, function(error) { + deferred.reject(error); + }); + } + return deferred.promise; + } + + function getInstanceUrl() { + return oauth.instance_url ? oauth.instance_url : ((useProxy && proxyURL) ? proxyURL.replace('my.salesforce', 'content.force') : ''); } /** @@ -293,7 +400,56 @@ angular.module('forceng', []) * @returns {boolean} */ function isAuthenticated() { - return (oauth && oauth.access_token) ? true : false; + var deferred = $q.defer(); + if (useCordova) { + oauthPlugin = cordova.require("com.salesforce.plugin.oauth"); + if (!oauthPlugin) { + console.error('Salesforce Mobile SDK OAuth plugin not available'); + } else { + oauthPlugin.authenticate(function(creds) { + init({accessToken: creds.accessToken, instanceURL: creds.instanceUrl, refreshToken: creds.refreshToken, userId: creds.userId, orgId: creds.orgId}); + if (oauth) { + tokenStore['forceOAuth'] = JSON.stringify(oauth); + } else { + console.log('oauth object is not present'); + } + deferred.resolve(); + }, + function(error) { + // Remove current session - try to login again + oauthPlugin.logout(); + // Kill the page + // deferred.reject(error); + }); + } + } else { + (oauth && oauth.access_token) ? deferred.resolve() : deferred.reject(); + } + return deferred.promise; + } + + /** + * @param path: full path or path relative to end point - required + * @param endPoint: undefined or endpoint - optional + * @return object with {endPoint:XX, path:relativePathToXX} + * + * For instance for undefined, '/services/data' => {endPoint:'/services/data', path:'/'} + * undefined, '/services/apex/abc' => {endPoint:'/services/apex', path:'/abc'} + * '/services/data, '/versions' => {endPoint:'/services/data', path:'/versions'} + */ + function computeEndPointIfMissing(endPoint, path) { + if (endPoint !== undefined) { + return {endPoint:endPoint, path:path}; + } + else { + var parts = path.split('/').filter(function(s) { return s !== ""; }); + if (parts.length >= 2) { + return {endPoint: '/' + parts.slice(0,2).join('/'), path: '/' + parts.slice(2).join('/')}; + } + else { + return {endPoint: '', path:path}; + } + } } /** @@ -304,16 +460,83 @@ angular.module('forceng', []) * params: queryString parameters as a map - Optional * data: JSON object to send in the request body - Optional */ + function request(obj) { + var d = new Date(); + var id = d.getTime() + Math.random().toString(36).substring(2,5); + $rootScope.$broadcast('$requestStarted', {id: id}); + // NB: networkPlugin will be defined only if plugin was detected on init + if (networkPlugin) { + return requestWithPlugin(obj, id); + } else { + return requestWithBrowser(obj, id); + } + } + + var _refreshTokenInitialized = false; + var _blockedRequests = []; + + function requestWithPlugin(obj, id) { + var deferred = $q.defer(); + Object.assign(obj, computeEndPointIfMissing(obj.endPoint, obj.path)); + + if (obj.params && obj.path.indexOf('?') === -1) { + obj.path += '?' + $httpParamSerializer(obj.params); + } + + networkPlugin.sendRequest(obj.endPoint, obj.path, function(result) { + $rootScope.$broadcast('$requestCompleted', {id: id}); + if (result.notifications && result.notifications.length > 0) { + for (var i = 0, j = result.notifications.length; i < j; i++) { + if (result.notifications[i].level !== '' && result.notifications[i].message !== '') { + $rootScope.$broadcast('$showNotification', {'level': result.notifications[i].level, 'message': result.notifications[i].message}); + } + } + } + deferred.resolve(result); + }, function(result) { + // Token got revoked? + if (result === 'Instance URL is null') { + _blockedRequests.push({obj: obj, id: id, deferred: deferred}); + if (!_refreshTokenInitialized) { + _refreshTokenInitialized = true; + refreshTokenWithPlugin() + .then(() => { + _refreshTokenInitialized = false; + _blockedRequests.forEach(function(request){ + request.deferred.resolve(requestWithPlugin(request.obj, request.id)); + }); + _blockedRequests = []; + }); + } + } else { + $rootScope.$broadcast('$requestCompleted', {id: id}); + deferred.reject(result); + } + }, obj.method, obj.data, obj.headerParams); + return deferred.promise + } + function requestWithBrowser(obj, id) { var method = obj.method || 'GET', headers = {}, url = getRequestBaseURL(), - deferred = $q.defer(); + deferred = $q.defer(), + responseType = obj.responseType; if (!oauth || (!oauth.access_token && !oauth.refresh_token)) { - deferred.reject('No access token. Login and try again.'); - return deferred.promise; + if (!retry) { + retry = true; + console.log("%c forceng: First try, might be missing access token or 'init' wasn't completed yet. Let's try again. ", 'background: #000; color: #bada55'); + $timeout(function(){ + deferred.resolve(requestWithBrowser(obj, id)); + }); + return deferred.promise; + } else { + deferred.reject('No access token. Login and try again.'); + $rootScope.$broadcast('$requestCompleted', {id: id}); + return deferred.promise; + } } // dev friendly API: Add leading '/' if missing so url + path concat always works @@ -321,7 +544,11 @@ angular.module('forceng', []) obj.path = '/' + obj.path; } - url = url + obj.path; + if (!obj.ignoreUrl) { + url = url + obj.path; + } else { + url = obj.url; + } headers["Authorization"] = "Bearer " + oauth.access_token; if (obj.contentType) { @@ -336,22 +563,34 @@ angular.module('forceng', []) method: method, url: url, params: obj.params, - data: obj.data + data: obj.data, + responseType: responseType }) .success(function (data, status, headers, config) { + $rootScope.$broadcast('$requestCompleted', {id: id}); deferred.resolve(data); }) .error(function (data, status, headers, config) { + $rootScope.$broadcast('$requestCompleted', {id: id}); if (status === 401 && oauth.refresh_token) { refreshToken() - .success(function () { - // Try again with the new token - request(obj); - }) - .error(function () { - console.error(data); - deferred.reject(data); - }); + .then(function () { + // Try again with the new token + deferred.resolve(request(obj)); + }, function () { + // New token failed, let's try to log in + delete tokenStore['forceOAuth']; + login().then(function(){ + deferred.resolve(request(obj)); + }, function() { + // Everything failed, throw error + console.error(data); + deferred.reject(data); + }); + }); + } else if (status === -1) { + // Probably VF Session got expired + logout(); } else { console.error(data); deferred.reject(data); @@ -519,15 +758,91 @@ angular.module('forceng', []) } + /** + * Create a SObject Tree + * @param data + * @returns {*} + */ + function createTree(data) { + + return request({ + method: 'POST', + contentType: 'application/json', + path: '/services/data/' + apiVersion + '/composite/tree', + data: data + }); + + } + + /** + * Create a Batch Requests + * @param data + * @returns {*} + */ + function createBatchRequests(data) { + + return request({ + method: 'POST', + contentType: 'application/json', + path: '/services/data/' + apiVersion + '/composite/batch', + data: { + batchRequests: data + } + }); + + } + + /** + * Create single Batch Request body + * @param {String} url + * @param {String} method + * @param {Object} data + * @returns {Object} + */ + function createBatchRequest(url, method, data) { + if (url.charAt(0) !== "/") { + url = "/" + url; + } + url = apiVersion + url + return { + url: url, + method: method, + richInput: data + }; + }; + + function createVFRequest(url) { + return oauth.instance_url + '/secur/frontdoor.jsp?sid=' + oauth.access_token + '&retURL=' + url; + } + + function getApiVersion() { + return apiVersion; + } + + function getToken() { + return oauth.access_token; + } + // The public API return { init: init, login: login, + logout: logout, getUserId: getUserId, + getOrgId: getOrgId, + getCurrentUser: getCurrentUser, + getInstanceUrl: getInstanceUrl, + getApiVersion: getApiVersion, + getToken: getToken, isAuthenticated: isAuthenticated, request: request, + requestWithBrowser: requestWithBrowser, query: query, create: create, + createTree: createTree, + createBatchRequests: createBatchRequests, + createBatchRequest: createBatchRequest, + createVFRequest: createVFRequest, update: update, del: del, upsert: upsert,