Source: resources/Notification.js

/* Copyright 2015-2016 PayPal, Inc. */
"use strict";

var generate = require('../generate');
var api = require('../api');
var https = require('https');
var crypto = require('crypto');
var crc32 = require('buffer-crc32');

/**
 * Exposes REST endpoints for creating and managing webhooks
 * @return {Object} webhook functions
 */
function webhook() {
    var baseURL = '/v1/notifications/webhooks/';
    var operations = ['create', 'list', 'get', 'del', 'delete'];

    var ret = {
        baseURL: baseURL,
        replace: function replace(id, data, config, cb) {
            api.executeHttp('PATCH', this.baseURL + id, data, config, cb);
        },
        eventTypes: function eventTypes(id, config, cb) {
            api.executeHttp('GET', this.baseURL + id + '/event-types', {}, config, cb);
        }
    };
    ret = generate.mixin(ret, operations);
    return ret;
}

/**
 * Exposes REST endpoints for working with subscribed webhooks events
 *
 * https://developer.paypal.com/webapps/developer/docs/integration/direct/rest-webhooks-overview/#events
 * @return {Object} webhook event functions
 */
function webhookEvent() {
    var baseURL = '/v1/notifications/webhooks-events/';
    var operations = ['list', 'get'];

    /**
     * Instead of calling this method, it is recommended that you initiate a GET request in your code for the webhook
     * event data and use the returned information from the webhook or use the updated verify() function. See
     * https://github.com/paypal/PayPal-node-SDK/wiki/Webhook-Validation
     *
     * @example
     * var paypal = require('paypal-rest-sdk');
     * function(request, response) {
     *     try {
     *         // Get the Webhook event id from the incoming event request
     *         var webhookEventId = JSON.parse(request.body).id;
     *
     *         paypal.notification.webhookEvent.get(webhookEventId, function (error, webhookEvent) {
     *             if (error) {
     *                 console.log(error);
     *                 // The webhook event data could not be found.
     *                 // Send a HTTP 503 response status code ( http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.4 )
     *                 // to signal to PayPal to resend the request at a later time.
     *                 response.sendStatus(503);
     *             } else {
     *                 // Proceed to use the data from PayPal
     *                 console.log("Get webhookEvent Response");
     *                 console.log(JSON.stringify(webhookEvent));
     *                 response.sendStatus(200);
     *             }
     *         });
     *     } catch (e) {
     *         // The webhook id could not be found or any other error occurred.
     *         // Send a HTTP 503 response status code ( http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5.4 )
     *         // to signal to PayPal to resend the request at a later time
     *         response.sendStatus(503);
     *     }
     * }
     *
     * @deprecated
     * @param  {String}   body raw body of request
     * @param  {Function} cb   callback function
     */
    function getAndVerify(body, cb) {
        var response = false;
        var err = null;
        try {
            var webhookEventId = JSON.parse(body).id;
            api.executeHttp('GET', baseURL + webhookEventId, {}, function (error, res) {
                if (error) {
                    cb(error, response);
                } else {
                    cb(err, true);
                }
            });
        } catch (e) {
            err = new Error("Webhook Event Id attribute not found. Possible reason could be invalid JSON Object.");
            cb(err, response);
        }
    }

    /**
     * @param {Object} headers from request
     * @param {String} raw body of request
     * @param {String} webhook id
     * @param {Function} callback function
     */
    function verify(headers, body, webhookId, callback) {
        // In an effort not to break existing applications, accept old arguments temporarily
        if (arguments.length > 4) {
            /* jshint validthis: true */
            return verifyLegacy.apply(this, arguments);
        }

        if (typeof headers !== 'object') {
            return callback(new Error("headers is not an object"), false);
        }

        // Normalizes headers
        Object.keys(headers).forEach(function (header) {
            headers[header.toUpperCase()] = headers[header];
        });

        var webhookEventBody = (typeof body === "string") ? JSON.parse(body) : body;

        var payload = {
            'auth_algo': headers['PAYPAL-AUTH-ALGO'],
            'cert_url': headers['PAYPAL-CERT-URL'],
            'transmission_id': headers['PAYPAL-TRANSMISSION-ID'],
            'transmission_sig': headers['PAYPAL-TRANSMISSION-SIG'],
            'transmission_time': headers['PAYPAL-TRANSMISSION-TIME'],
            'webhook_id': webhookId,
            'webhook_event': webhookEventBody
        };

        api.executeHttp('POST', '/v1/notifications/verify-webhook-signature', payload, callback);
    }

    function verifyLegacy(certURL, transmissionId, timeStamp, webhookId, eventBody, ppTransmissionSig, cb) {
        // Emit a warning that the arguments have changed
        if (process.env.NODE_ENV === 'development') {
            console.log('PayPal-Node-SDK: Webhook verify arguments have changed. Please check the latest documentation on https://developer.paypal.com/docs/integration/direct/rest-webhooks-overview/#event-signature.');
        }

        var headers = {
            // This is currently the default auth algorithm. If this changes, need to change. Legacy method did
            // not pass in the algorithm.
            'PAYPAL-AUTH-ALGO': 'SHA256withRSA',
            'PAYPAL-CERT-URL': certURL,
            'PAYPAL-TRANSMISSION-ID': transmissionId,
            'PAYPAL-TRANSMISSION-SIG': ppTransmissionSig,
            'PAYPAL-TRANSMISSION-TIME': timeStamp
        };

        function legacyCallback(error, response) {
            if (error) {
                cb(error, false);
            } else {
                // Verification status must be SUCCESS
                if (response.verification_status === "SUCCESS") {
                    cb(null, true);
                } else {
                    cb(null, false);
                }
            }
        }

        return verify(headers, eventBody, webhookId, legacyCallback);
    }

    var ret = {
        baseURL: baseURL,
        verify: verify,
        getAndVerify: getAndVerify,
        resend: function resend(id, config, cb) {
            api.executeHttp('POST', this.baseURL + id + '/resend', {}, config, cb);
        }
    };
    ret = generate.mixin(ret, operations);
    return ret;
}

/**
 * Exposes REST endpoint for listing available event types for webhooks
 * @return {Object} webhook event type functions
 */
function webhookEventType() {
    var baseURL = '/v1/notifications/webhooks-event-types/';
    var operations = ['list'];

    var ret = {
        baseURL: baseURL
    };
    ret = generate.mixin(ret, operations);
    return ret;
}

/**
 * Exposes the namespace for webhook and webhook event functionalities
 * 
 * https://developer.paypal.com/webapps/developer/docs/api/#notifications
 * @return {Object} notification functions
 */
function notification() {
    return {
        webhook: webhook(),
        webhookEvent: webhookEvent(),
        webhookEventType: webhookEventType()
    };
}

module.exports = notification;