Source: jive-sdk-api/lib/community/community.js

/*
 * 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;
};