/*
* Copyright 2013 Jive Software
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* API for interacting with Jive communities.
* @module community
*/
/////////////////////////////////////////////////////////////////////////////////////////////////////
var jive = require('../../api');
var q = require('q');
var jiveClient = require('./../client/jive');
var returnOne = function(found ) {
if ( found == null || found.length < 1 ) {
return null;
} else {
// return first one
return found[0];
}
};
/**
* Saves the given community object into persistence. Will throw an Error if the community object
* does not contain a 'jiveUrl' property.
* @param {Object} community Community object, which must specify a jiveUrl property at minimum.
* @returns {Promise} Promise
*/
exports.save = function(community ) {
var jiveUrl = community['jiveUrl'];
var jiveCommunity = community['jiveCommunity'];
if ( !jiveUrl ) {
throw new Error("Invalid commmunity object, must specify a jiveUrl property.");
}
if ( !jiveCommunity ) {
community['jiveCommunity'] = exports.parseJiveCommunity(jiveUrl);
}
return jive.context.persistence.save( "community", community['jiveUrl'], community );
};
var find = function( filter, expectOne ) {
return jive.context.persistence.find("community", filter).then( function( found ) {
return expectOne ? returnOne( found ) : found;
} );
};
/**
* Searches persistence for communities that matches the given criteria filter object.
* @param filter
* @returns {Promise} Promise
*/
exports.find = function(filter) {
return find( filter );
};
/**
* Searches persistence for community that matches the given jiveUrl.
* If one is not fond,
* the promise will resolve with a null (undefined) value.
* @param jiveUrl
* @returns {Promise} Promise
*/
exports.findByJiveURL = function( jiveUrl ) {
return find( {
'jiveUrl' : jiveUrl
}, true );
};
/**
* Searches persistence for a community that matches the name of the given jive community.
* If one is not found, the promise will resolve a null (undefined) value.
* @param jiveCommunity
* @returns {Promise} Promise
*/
exports.findByCommunity = function( jiveCommunity ) {
return find( {
'jiveCommunity' : jiveCommunity
}, true );
};
/**
* Searches persistence for a community that matches the tenantID of the given jive community.
* If one is not found, the promise will resovle a null (undefined) value.
* @param tenantID
* @returns {Promise} Promise
*/
exports.findByTenantID = function( tenantID ) {
return find( {
'tenantId' : tenantID
}, true );
};
/**
* Parses the given jiveUrl for the name of the community.
* @param jiveUrl
* @returns Name of the community based on the jiveUrl.
*/
exports.parseJiveCommunity = function( jiveUrl ) {
var parts = require('url').parse(jiveUrl).host.split('www.');
return parts.length > 1 ? parts[1] : parts[0];
};
/**
* Requests an access token by oauth access code (oauth access code is given in registration requests and valid for few minutes).
* @param jiveUrl - the url of the jive community. this function will use this url to find the community in the persistence and get the client id and secret.
* @param oauthCode - the code needed to be use to get access to a specific registration scope (usually a group).
* @returns {Promise} Promise A promise for success and failure [use .then(...) and .catch(...)]
*/
exports.requestAccessToken = function (jiveUrl, oauthCode) {
var defer = q.defer();
exports.findByJiveURL(jiveUrl)
.then(function (communityObj) {
if (communityObj) {
var oauthConf = {
client_id: communityObj.clientId,
client_secret: communityObj.clientSecret,
code: oauthCode,
jiveUrl: jiveUrl
};
jiveClient.requestAccessToken(oauthConf, defer.resolve, defer.reject);
}
else {
defer.reject(new Error("No community found by the url: " + jiveUrl));
}
})
.catch(defer.reject);
return defer.promise;
};
var accessTokenRefresher = function(operationContext, oauth) {
var community = operationContext['community'];
var tokenPersistenceFunction = operationContext.tokenPersistenceFunction;
var d = q.defer();
var options = {};
if ( community ) {
options['client_id'] = community['clientId'];
options['client_secret'] = community['clientSecret'];
options['refresh_token'] = oauth['refresh_token'];
options['jiveUrl'] = community['jiveUrl'];
jiveClient.refreshAccessToken(options,
function (response) {
if (response.statusCode >= 200 && response.statusCode <= 299) {
var accessTokenResponse = response['entity'];
var resolve = function() {
d.resolve(accessTokenResponse);
};
if(tokenPersistenceFunction) {
var promise = tokenPersistenceFunction(accessTokenResponse, community);
if(promise) {
promise.then(resolve, resolve);
} else {
resolve();
}
} else {
resolve();
}
} else {
jive.logger.error('error refreshing access token for ', community);
d.reject(response);
}
}, function (result) {
// failure
jive.logger.error('error refreshing access token for ', community, result);
d.reject(result);
}
);
} else {
d.reject();
}
return d.promise;
};
var oAuthHandler;
var getOAuthHandler = function() {
if ( !oAuthHandler ) {
oAuthHandler = jive.util.oauth.buildOAuthHandler(accessTokenRefresher);
}
return oAuthHandler;
};
/**
* Make a request to the current community.
* Automatically handle access token refresh flow if failure.
* @param community
* @param {Object} options Request options.
* @param {String} options.path Path relative to given community's jiveURL; used if options.url doesn't exist.
* @param {String} options.url Full request URL. options.path is not used if options.url is provided.
* @param {Object} options.headers Map of header key-values.
* @param {Object} options.oauth Map of oauth properties.
* @param {String} options.oauth.access_token OAuth access token.
* @param {String} options.oauth.refresh_token OAuth refresh token.
* @param {function} options.tokenPersistenceFunction Callback function that will be invoked with new oauth access and refresh
* tokens ({'access_token' : '...', 'refresh_token' : '...' }). If not provided, the community will be updated with the new access tokens.
* @returns {Promise} Promise
*/
exports.doRequest = function( community, options ) {
if ( !community ) {
throw new Error("Community is required.");
}
if ( typeof community !== 'object' ) {
throw new Error("Community must be an object.");
}
options = options || {};
var path = options.path,
url = options.url,
headers = options.headers || {},
oauth = options.oauth || community.oauth,
jiveUrl = community.jiveUrl,
tokenPersistenceFunction = options.tokenPersistenceFunction;
if ( !url ) {
// construct from path and jiveURL
if( path.charAt(0) !== '/' ) {
// ensure there a starting /
path = "/" + path;
}
if( jiveUrl.slice(-1) === '/') {
// Trim the last /
jiveUrl = jiveUrl.slice(0, -1);
}
url = jiveUrl + path;
}
if (!oauth) {
jive.logger.info("No oauth credentials found. Continuing without them.");
return jive.util.buildRequest( url, options.method, options.postBody, headers, options.requestOptions );
}
// oauth
if (!tokenPersistenceFunction && !options.oauth) {
tokenPersistenceFunction = function(updatedOAuth) {
community.oauth = updatedOAuth;
return exports.save(community);
};
}
headers.Authorization = 'Bearer ' + oauth['access_token'];
return getOAuthHandler().doOperation(
// operation
function(operationContext, oauth) {
var community = operationContext['community'];
return exports.findByCommunity(community['jiveCommunity']).then( function( community ) {
if ( !community ) {
return q.reject();
}
headers.Authorization = 'Bearer ' + oauth.access_token;
return jive.util.buildRequest( url, options.method, options.postBody, headers, options.requestOptions );
});
},
// operation context
{
'community' : community,
'tokenPersistenceFunction' : tokenPersistenceFunction
},
// oauth
{
'access_token' : oauth['access_token'],
'refresh_token' : oauth['refresh_token']
}
);
};
function validateRegistration(registration) {
if ( jive.context.config['development'] == true ) {
jive.logger.warn("Warning - development mode is on. Accepting extension registration request regardless of source!");
return q.resolve(true);
}
var validationBlock = JSON.parse( JSON.stringify(registration) );
var jiveSignature = validationBlock['jiveSignature'];
var clientSecret = validationBlock['clientSecret'];
var jiveSignatureUrl = validationBlock['jiveSignatureURL'];
delete validationBlock['jiveSignature'];
var crypto = require("crypto");
var sha256 = crypto.createHash("sha256");
sha256.update(clientSecret, "utf8");
validationBlock['clientSecret'] = sha256.digest("hex");
var buffer = '';
validationBlock = jive.util.sortObject(validationBlock);
for (var key in validationBlock) {
if (validationBlock.hasOwnProperty(key)) {
var value = validationBlock[key];
buffer += key + ':' + value + '\n';
}
}
var headers = {
'X-Jive-MAC' : jiveSignature
};
jive.logger.debug("Received registration block: " + JSON.stringify(validationBlock) );
jive.logger.debug("Shipping validation request to appsmarket - endpoint: " + jiveSignatureUrl );
return jive.util.buildRequest(jiveSignatureUrl, 'POST', buffer, headers);
}
/**
* Processes the incoming community addon registration request object.
* Validation rules:<br>
* <ul>
* <li>If jive.context.config['development'] == true, addon registration validation will be skipped.</li>
* <li>If not, then registration.jiveSignatureURL will be invoked to validate the registration block.
* Failure will cause the return promise reject callback to be fired.</li>
* </ul>
*
* Upon successful registration, a community object will be persisted or updated (if one already exists) based on the
* contents of the registration object.
*
* @param {Object} registration Community addon registration object.
* @param {String} registration.jiveSignature Signature provided by Jive used for registration validation.
* @param {String} registration.clientSecret Secret provided by the addon service.
* @param {String} registration.jiveSignatureURL URL used for validating the registration.
* @param {String} registration.jiveUrl URL of the originating Jive community.
* @param {String} registration.tenantId Tenant ID of the originating Jive community. This is a durable, global ID.
* @param {String} registration.clientId Client ID assigned by the Jive community to the addon as part of this registration request.
* @param {String} registration.clientSecret Client Secret assigned by the Jive community to the addon as part of this registration request.
* @param {String} registration.authorizationCode Optional. If provided, the system will automatically attempt an OAuth2
* access and refresh token exchange with the originating Jive community using this authorization code. The code will be
* persisted along with the community object associated with this registration call.
* @param registration
* @returns {Promise} Promise
* */
exports.register = function( registration ) {
var deferred = q.defer();
validateRegistration(registration).then(
// success
function() {
var registrationToSave = JSON.parse( JSON.stringify(registration) );
jive.logger.debug("Successful registration request, proceeding.");
var jiveSignature = registration['jiveSignature'];
var authorizationCode = registration['code'];
var scope = registration['scope'];
var tenantId = registration['tenantId'];
var jiveUrl = registration['jiveUrl'];
var clientId = registration['clientId'];
var clientSecret = registration['clientSecret'];
if ( !clientId ) {
// use global one
clientId = jive.context.config['clientId'];
}
if ( !clientSecret ) {
// use global one
clientSecret = jive.context.config['clientSecret'];
}
// do access token exchange
function persistCommunity(oauthResponse) {
exports.findByJiveURL(jiveUrl).then(function (community) {
community = community || {};
var oauth;
if ( oauthResponse ) {
oauth = community['oauth'] || oauthResponse['entity'];
oauth['code'] = authorizationCode || oauth['code'];
oauth['jiveSignature'] = jiveSignature || oauth['jiveSignature'];
oauth['scope'] = oauthResponse['entity']['scope'] || oauth['scope'];
oauth['expiresIn'] = oauthResponse['entity']['expires_in'] || oauth['expiresIn'];
oauth['accessToken'] = oauthResponse['entity']['access_token'] || oauth['accessToken'];
}
community['jiveUrl' ] = jiveUrl || community['jiveUrl' ];
community['version' ] = 'post-samurai';
community['tenantId' ] = tenantId || community['tenantId' ];
community['clientId' ] = clientId || community['clientId' ];
community['clientSecret' ] = clientSecret || community['clientSecret' ];
if ( oauth ) {
community[ 'oauth' ] = oauth;
}
exports.save(community).then(
function () {
// successful save:
// emit a registration saved event and resolve
jive.events.emit("registeredJiveInstanceSuccess", community);
deferred.resolve();
},
function (error) {
// error saving
jive.events.emit("registeredJiveInstanceFailed", community);
deferred.reject(error);
}
);
});
}
if ( authorizationCode ) {
jiveClient.requestAccessToken(
{
'client_secret' : clientSecret,
'client_id' : clientId,
'code' : authorizationCode,
'jiveUrl' : jiveUrl,
'scope' : scope,
'tenantId' : tenantId
},
function(response) {
// successfully exchanged for access token:
// save registration
persistCommunity(response);
},
function(error) {
// failed to exchange for access token
jive.events.emit("registeredJiveInstanceFailed", registrationToSave);
deferred.reject( error );
}
);
} else {
persistCommunity();
}
},
// error
function(err) {
jive.logger.debug("Unsuccessful registration request: " + err? JSON.stringify(err) : '');
jive.events.emit("registeredJiveInstanceFailed", err );
deferred.reject(new Error("Failed jive signature validation: "
+ JSON.stringify(err)));
}
);
return deferred.promise;
};