<commit_before>// @flow

import type {
  Event,
  Repository,
  RequestOptions,
  Webhook,
  WebhookMutator,
} from '../types';
import type { EventPublisher } from 'spark-protocol';

import hogan from 'hogan.js';
import HttpError from '../lib/HttpError';
import logger from '../lib/logger';
import request from 'request';
import throttle from 'lodash/throttle';

const parseEventData = (event: Event): Object => {
  try {
    if (event.data) {
      return JSON.parse(event.data);
    }
    return {};
  } catch (error) {
    return {};
  }
};

const splitBufferIntoChunks = (
  buffer: Buffer,
  chunkSize: number,
): Array<Buffer> => {
  const chunks = [];
  let ii = 0;
  while (ii < buffer.length) {
    chunks.push(buffer.slice(ii, ii += chunkSize));
  }

  return chunks;
};

const MAX_WEBHOOK_ERRORS_COUNT = 10;
const WEBHOOK_THROTTLE_TIME = 1000 * 60; // 1min;
const MAX_RESPONSE_MESSAGE_CHUNK_SIZE = 512;
const MAX_RESPONSE_MESSAGE_SIZE = 100000;

class WebhookManager {
  _eventPublisher: EventPublisher;
  _subscriptionIDsByWebhookID: Map<string, string> = new Map();
  _errorsCountByWebhookID: Map<string, number> = new Map();
  _webhookRepository: Repository<Webhook>;

  constructor(
    webhookRepository: Repository<Webhook>,
    eventPublisher: EventPublisher,
  ) {
    this._webhookRepository = webhookRepository;
    this._eventPublisher = eventPublisher;

    (async (): Promise<void> => await this._init())();
  }

  create = async (model: WebhookMutator): Promise<Webhook> => {
    const webhook = await this._webhookRepository.create(model);
    this._subscribeWebhook(webhook);
    return webhook;
  };

  deleteByID = async (
    webhookID: string,
    userID: string,
  ): Promise<void> => {
    const webhook = await this._webhookRepository.getById(webhookID, userID);
    if (!webhook) {
      throw new HttpError('no webhook found', 404);
    }
    await this._webhookRepository.deleteById(webhookID);
    this._unsubscribeWebhookByID(webhookID);
  };

  getAll = async (userID: string): Promise<Array<Webhook>> =>
    await this._webhookRepository.getAll(userID);

  getByID = async (webhookID: string, userID: string): Promise<Webhook> => {
    const webhook = await this._webhookRepository.getById(webhookID, userID);
    if (!webhook) {
      throw new HttpError('no webhook found', 404);
    }

    return webhook;
  };

  _init = async (): Promise<void> => {
    const allWebhooks = await this._webhookRepository.getAll();
    allWebhooks.forEach(
      (webhook: Webhook): void => this._subscribeWebhook(webhook),
    );
  };

  _subscribeWebhook = (webhook: Webhook) => {
    const subscriptionID = this._eventPublisher.subscribe(
      webhook.event,
      this._onNewWebhookEvent(webhook),
      // todo separate filtering for MY_DEVICES and for public/private events
      {
        deviceID: webhook.deviceID,
        userID: webhook.ownerID,
      },
    );
    this._subscriptionIDsByWebhookID.set(webhook.id, subscriptionID);
  };

  _unsubscribeWebhookByID = (webhookID: string) => {
    const subscriptionID = this._subscriptionIDsByWebhookID.get(webhookID);
    if (!subscriptionID) {
      return;
    }

    this._eventPublisher.unsubscribe(subscriptionID);
    this._subscriptionIDsByWebhookID.delete(webhookID);
  };

  _onNewWebhookEvent = (webhook: Webhook): (event: Event) => void =>
    (event: Event) => {
      try {
        if (
          webhook.mydevices &&
          webhook.ownerID !== event.userID
        ) {
          return;
        }

        const webhookErrorCount =
          this._errorsCountByWebhookID.get(webhook.id) || 0;

        if (webhookErrorCount < MAX_WEBHOOK_ERRORS_COUNT) {
          this.runWebhook(webhook, event);
          return;
        }

        this._eventPublisher.publish({
          data: 'Too many errors, webhook disabled',
          name: this._compileErrorResponseTopic(
            webhook,
            event,
          ),
          userID: event.userID,
        });

        this.runWebhookThrottled(webhook, event);
      } catch (error) {
        logger.error(`webhookError: ${error}`);
      }
    };

  runWebhook = async (webhook: Webhook, event: Event): Promise<void> => {
    try {
      const webhookVariablesObject =
        this._getEventVariables(event);

      const requestJson = this._compileJsonTemplate(
        webhook.json,
        webhookVariablesObject,
      );

      const requestFormData = this._compileJsonTemplate(
        webhook.form,
        webhookVariablesObject,
      );

      const requestUrl = this._compileTemplate(
        webhook.url,
        webhookVariablesObject,
      );

      const requestQuery = this._compileJsonTemplate(
        webhook.query,
        webhookVariablesObject,
      );

      const responseTopic = this._compileTemplate(
        webhook.responseTopic,
        webhookVariablesObject,
      );

      const isJsonRequest = !!requestJson;
      const requestOptions = {
        auth: webhook.auth,
        body: isJsonRequest
          ? this._getRequestData(requestJson, event, webhook.noDefaults)
          : undefined,
        form: !isJsonRequest
          ? this._getRequestData(requestFormData, event, webhook.noDefaults)
          : undefined,
        headers: webhook.headers,
        json: true,
        method: webhook.requestType,
        qs: requestQuery,
        strictSSL: webhook.rejectUnauthorized,
        url: requestUrl,
      };

      const responseBody = await this._callWebhook(
        webhook,
        event,
        requestOptions,
      );

      if (!responseBody) {
        return;
      }

      const isResponseBodyAnObject = responseBody === Object(responseBody);

      const responseTemplate =
        webhook.responseTemplate && isResponseBodyAnObject && hogan
          .compile(webhook.responseTemplate)
          .render(responseBody);

      const responseEventData = responseTemplate || (isResponseBodyAnObject
        ? JSON.stringify(responseBody)
        : responseBody);

      const chunks = splitBufferIntoChunks(
        Buffer
          .from(responseEventData)
          .slice(0, MAX_RESPONSE_MESSAGE_SIZE),
        MAX_RESPONSE_MESSAGE_CHUNK_SIZE,
      );

      chunks.forEach((chunk: Buffer, index: number) => {
        const responseEventName =
          responseTopic && `${responseTopic}/${index}` ||
          `hook-response/${event.name}/${index}`;

        this._eventPublisher.publish({
          data: chunk,
          name: responseEventName,
          userID: event.userID,
        });
      });
    } catch (error) {
      logger.error(`webhookError: ${error}`);
    }
  };

  runWebhookThrottled = throttle(
    this.runWebhook,
    WEBHOOK_THROTTLE_TIME,
    { leading: false, trailing: true },
  );

  _callWebhook = (
    webhook: Webhook,
    event: Event,
    requestOptions: RequestOptions,
  ): Promise<*> => new Promise(
    (resolve, reject) => request(
      requestOptions,
      (
        error: ?Error,
        response: http$IncomingMessage,
        responseBody: string | Buffer | Object,
      ) => {
        // todo check response.statusCode > 300 also.
        if (error) {
          this._incrementWebhookErrorCounter(webhook.id);

          this._eventPublisher.publish({
            data: error.message,
            name: this._compileErrorResponseTopic(
              webhook,
              event,
            ),
            userID: event.userID,
          });

          reject(error);
          return;
        }

        this._resetWebhookErrorCounter(webhook.id);

        this._eventPublisher.publish({
          name: `hook-sent/${event.name}`,
          userID: event.userID,
        });

        resolve(responseBody);
      },
    ),
  );

  _getEventVariables = (event: Event): Object => {
    const defaultWebhookVariables = {
      PARTICLE_DEVICE_ID: event.deviceID,
      PARTICLE_EVENT_NAME: event.name,
      PARTICLE_EVENT_VALUE: event.data,
      PARTICLE_PUBLISHED_AT: event.publishedAt,
      // old event names, added for compatibility
      SPARK_CORE_ID: event.deviceID,
      SPARK_EVENT_NAME: event.name,
      SPARK_EVENT_VALUE: event.data,
      SPARK_PUBLISHED_AT: event.publishedAt,
    };

    const eventDataVariables = parseEventData(event);

    return {
      ...defaultWebhookVariables,
      ...eventDataVariables,
    };
  };

  _getRequestData = (
    customData: ?Object,
    event: Event,
    noDefaults: boolean,
  ): ?Object => {
    const defaultEventData = {
      coreid: event.deviceID,
      data: event.data,
      event: event.name,
      published_at: event.publishedAt,
    };

    return noDefaults
      ? customData
      : { ...defaultEventData, ...(customData || {}) };
  };

  _compileTemplate = (template?: ?string, variables: Object): ?string =>
    template && hogan
      .compile(template)
      .render(variables);

  _compileJsonTemplate = (template?: ?Object, variables: Object): ?Object => {
    if (!template) {
      return;
    }

    const compiledTemplate = this._compileTemplate(
      JSON.stringify(template),
      variables,
    );
    if (!compiledTemplate) {
      return;
    }

    return JSON.parse(compiledTemplate);
  };

  _compileErrorResponseTopic = (webhook: Webhook, event: Event): string => {
    const variables = this._getEventVariables(event);
    return this._compileTemplate(
      webhook.errorResponseTopic,
      variables,
    ) || `hook-error/${event.name}`;
  };

  _incrementWebhookErrorCounter = (webhookID: string) => {
    const errorsCount = this._errorsCountByWebhookID.get(webhookID) || 0;
    this._errorsCountByWebhookID.set(webhookID, errorsCount + 1);
  };

  _resetWebhookErrorCounter = (webhookID: string) => {
    this._errorsCountByWebhookID.set(webhookID, 0);
  };
}

export default WebhookManager;
<commit_msg>handling errors for responses with status > 400<commit_after>// @flow

import type {
  Event,
  Repository,
  RequestOptions,
  Webhook,
  WebhookMutator,
} from '../types';
import type { EventPublisher } from 'spark-protocol';

import hogan from 'hogan.js';
import HttpError from '../lib/HttpError';
import logger from '../lib/logger';
import request from 'request';
import throttle from 'lodash/throttle';

const parseEventData = (event: Event): Object => {
  try {
    if (event.data) {
      return JSON.parse(event.data);
    }
    return {};
  } catch (error) {
    return {};
  }
};

const splitBufferIntoChunks = (
  buffer: Buffer,
  chunkSize: number,
): Array<Buffer> => {
  const chunks = [];
  let ii = 0;
  while (ii < buffer.length) {
    chunks.push(buffer.slice(ii, ii += chunkSize));
  }

  return chunks;
};

const MAX_WEBHOOK_ERRORS_COUNT = 10;
const WEBHOOK_THROTTLE_TIME = 1000 * 60; // 1min;
const MAX_RESPONSE_MESSAGE_CHUNK_SIZE = 512;
const MAX_RESPONSE_MESSAGE_SIZE = 100000;

class WebhookManager {
  _eventPublisher: EventPublisher;
  _subscriptionIDsByWebhookID: Map<string, string> = new Map();
  _errorsCountByWebhookID: Map<string, number> = new Map();
  _webhookRepository: Repository<Webhook>;

  constructor(
    webhookRepository: Repository<Webhook>,
    eventPublisher: EventPublisher,
  ) {
    this._webhookRepository = webhookRepository;
    this._eventPublisher = eventPublisher;

    (async (): Promise<void> => await this._init())();
  }

  create = async (model: WebhookMutator): Promise<Webhook> => {
    const webhook = await this._webhookRepository.create(model);
    this._subscribeWebhook(webhook);
    return webhook;
  };

  deleteByID = async (
    webhookID: string,
    userID: string,
  ): Promise<void> => {
    const webhook = await this._webhookRepository.getById(webhookID, userID);
    if (!webhook) {
      throw new HttpError('no webhook found', 404);
    }
    await this._webhookRepository.deleteById(webhookID);
    this._unsubscribeWebhookByID(webhookID);
  };

  getAll = async (userID: string): Promise<Array<Webhook>> =>
    await this._webhookRepository.getAll(userID);

  getByID = async (webhookID: string, userID: string): Promise<Webhook> => {
    const webhook = await this._webhookRepository.getById(webhookID, userID);
    if (!webhook) {
      throw new HttpError('no webhook found', 404);
    }

    return webhook;
  };

  _init = async (): Promise<void> => {
    const allWebhooks = await this._webhookRepository.getAll();
    allWebhooks.forEach(
      (webhook: Webhook): void => this._subscribeWebhook(webhook),
    );
  };

  _subscribeWebhook = (webhook: Webhook) => {
    const subscriptionID = this._eventPublisher.subscribe(
      webhook.event,
      this._onNewWebhookEvent(webhook),
      // todo separate filtering for MY_DEVICES and for public/private events
      {
        deviceID: webhook.deviceID,
        userID: webhook.ownerID,
      },
    );
    this._subscriptionIDsByWebhookID.set(webhook.id, subscriptionID);
  };

  _unsubscribeWebhookByID = (webhookID: string) => {
    const subscriptionID = this._subscriptionIDsByWebhookID.get(webhookID);
    if (!subscriptionID) {
      return;
    }

    this._eventPublisher.unsubscribe(subscriptionID);
    this._subscriptionIDsByWebhookID.delete(webhookID);
  };

  _onNewWebhookEvent = (webhook: Webhook): (event: Event) => void =>
    (event: Event) => {
      try {
        if (
          webhook.mydevices &&
          webhook.ownerID !== event.userID
        ) {
          return;
        }

        const webhookErrorCount =
          this._errorsCountByWebhookID.get(webhook.id) || 0;

        if (webhookErrorCount < MAX_WEBHOOK_ERRORS_COUNT) {
          this.runWebhook(webhook, event);
          return;
        }

        this._eventPublisher.publish({
          data: 'Too many errors, webhook disabled',
          name: this._compileErrorResponseTopic(
            webhook,
            event,
          ),
          userID: event.userID,
        });

        this.runWebhookThrottled(webhook, event);
      } catch (error) {
        logger.error(`webhookError: ${error}`);
      }
    };

  runWebhook = async (webhook: Webhook, event: Event): Promise<void> => {
    try {
      const webhookVariablesObject =
        this._getEventVariables(event);

      const requestJson = this._compileJsonTemplate(
        webhook.json,
        webhookVariablesObject,
      );

      const requestFormData = this._compileJsonTemplate(
        webhook.form,
        webhookVariablesObject,
      );

      const requestUrl = this._compileTemplate(
        webhook.url,
        webhookVariablesObject,
      );

      const requestQuery = this._compileJsonTemplate(
        webhook.query,
        webhookVariablesObject,
      );

      const responseTopic = this._compileTemplate(
        webhook.responseTopic,
        webhookVariablesObject,
      );

      const isJsonRequest = !!requestJson;
      const requestOptions = {
        auth: webhook.auth,
        body: isJsonRequest
          ? this._getRequestData(requestJson, event, webhook.noDefaults)
          : undefined,
        form: !isJsonRequest
          ? this._getRequestData(requestFormData, event, webhook.noDefaults)
          : undefined,
        headers: webhook.headers,
        json: true,
        method: webhook.requestType,
        qs: requestQuery,
        strictSSL: webhook.rejectUnauthorized,
        url: requestUrl,
      };

      const responseBody = await this._callWebhook(
        webhook,
        event,
        requestOptions,
      );

      if (!responseBody) {
        return;
      }

      const isResponseBodyAnObject = responseBody === Object(responseBody);

      const responseTemplate =
        webhook.responseTemplate && isResponseBodyAnObject && hogan
          .compile(webhook.responseTemplate)
          .render(responseBody);

      const responseEventData = responseTemplate || (isResponseBodyAnObject
        ? JSON.stringify(responseBody)
        : responseBody);

      const chunks = splitBufferIntoChunks(
        Buffer
          .from(responseEventData)
          .slice(0, MAX_RESPONSE_MESSAGE_SIZE),
        MAX_RESPONSE_MESSAGE_CHUNK_SIZE,
      );

      chunks.forEach((chunk: Buffer, index: number) => {
        const responseEventName =
          responseTopic && `${responseTopic}/${index}` ||
          `hook-response/${event.name}/${index}`;

        this._eventPublisher.publish({
          data: chunk,
          name: responseEventName,
          userID: event.userID,
        });
      });
    } catch (error) {
      logger.error(`webhookError: ${error}`);
    }
  };

  runWebhookThrottled = throttle(
    this.runWebhook,
    WEBHOOK_THROTTLE_TIME,
    { leading: false, trailing: true },
  );

  _callWebhook = (
    webhook: Webhook,
    event: Event,
    requestOptions: RequestOptions,
  ): Promise<*> => new Promise(
    (resolve, reject) => request(
      requestOptions,
      (
        error: ?Error,
        response: http$IncomingMessage,
        responseBody: string | Buffer | Object,
      ) => {
        const onResponseError = (errorMessage: ?string) => {
          this._incrementWebhookErrorCounter(webhook.id);

          this._eventPublisher.publish({
            data: errorMessage,
            name: this._compileErrorResponseTopic(
              webhook,
              event,
            ),
            userID: event.userID,
          });

          reject(new Error(errorMessage));
        };

        if (error) {
          onResponseError(error.message);
          return;
        }
        if (response.statusCode >= 400) {
          onResponseError(response.statusMessage);
          return;
        }

        this._resetWebhookErrorCounter(webhook.id);

        this._eventPublisher.publish({
          name: `hook-sent/${event.name}`,
          userID: event.userID,
        });

        resolve(responseBody);
      },
    ),
  );

  _getEventVariables = (event: Event): Object => {
    const defaultWebhookVariables = {
      PARTICLE_DEVICE_ID: event.deviceID,
      PARTICLE_EVENT_NAME: event.name,
      PARTICLE_EVENT_VALUE: event.data,
      PARTICLE_PUBLISHED_AT: event.publishedAt,
      // old event names, added for compatibility
      SPARK_CORE_ID: event.deviceID,
      SPARK_EVENT_NAME: event.name,
      SPARK_EVENT_VALUE: event.data,
      SPARK_PUBLISHED_AT: event.publishedAt,
    };

    const eventDataVariables = parseEventData(event);

    return {
      ...defaultWebhookVariables,
      ...eventDataVariables,
    };
  };

  _getRequestData = (
    customData: ?Object,
    event: Event,
    noDefaults: boolean,
  ): ?Object => {
    const defaultEventData = {
      coreid: event.deviceID,
      data: event.data,
      event: event.name,
      published_at: event.publishedAt,
    };

    return noDefaults
      ? customData
      : { ...defaultEventData, ...(customData || {}) };
  };

  _compileTemplate = (template?: ?string, variables: Object): ?string =>
    template && hogan
      .compile(template)
      .render(variables);

  _compileJsonTemplate = (template?: ?Object, variables: Object): ?Object => {
    if (!template) {
      return;
    }

    const compiledTemplate = this._compileTemplate(
      JSON.stringify(template),
      variables,
    );
    if (!compiledTemplate) {
      return;
    }

    return JSON.parse(compiledTemplate);
  };

  _compileErrorResponseTopic = (webhook: Webhook, event: Event): string => {
    const variables = this._getEventVariables(event);
    return this._compileTemplate(
      webhook.errorResponseTopic,
      variables,
    ) || `hook-error/${event.name}`;
  };

  _incrementWebhookErrorCounter = (webhookID: string) => {
    const errorsCount = this._errorsCountByWebhookID.get(webhookID) || 0;
    this._errorsCountByWebhookID.set(webhookID, errorsCount + 1);
  };

  _resetWebhookErrorCounter = (webhookID: string) => {
    this._errorsCountByWebhookID.set(webhookID, 0);
  };
}

export default WebhookManager;
<|endoftext|>


<commit_before>/**
 * mediawiki.client has some convenience functions for user agent checks
 *
 * TODO this should be combined with or bootstrap jquery.client.js
 */
(function (mw) {

	var userAgent = navigator.userAgent;

	mw.isMobileDevice = function () {
		return ( mw.isIphone() || mw.isIpod() || mw.isIpad() || mw.isAndroid() || mw.isWindowsPhone() || mw.getConfig("EmbedPlayer.ForceNativeComponent") === true || mw.getConfig("EmbedPlayer.SimulateMobile") === true )
	};
	mw.isNativeApp = function () {
		return mw.getConfig("EmbedPlayer.ForceNativeComponent");
	};
	mw.isIphone = function () {
		return ( mw.getConfig("EmbedPlayer.ForceNativeComponent") !== true && userAgent.indexOf('iPhone') != -1 && !mw.isIpad() ) || mw.isIpod();
	};
	mw.isIE = function () {
		return (/msie/.test(userAgent.toLowerCase()) || /trident/.test(userAgent.toLowerCase()));
	};
	mw.isChromeCast = function(){
		return (/CrKey/.test(userAgent));
	};
	mw.isIE7 = function () {
		return (/msie 7/.test(userAgent.toLowerCase()));
	};
	mw.isIE8 = function () {
		return document.documentMode === 8;
	};
	mw.isIE9 = function () {
		return document.documentMode === 9;
	};
    mw.isIE11 = function () {
        return (/trident\/7.0/.test(userAgent.toLowerCase()));
    };
	mw.isEdge = function () {
        return (/edge/.test(userAgent.toLowerCase()));
    };
	mw.isDesktopSafari = function () {
		return (/safari/).test(userAgent.toLowerCase()) && !mw.isMobileDevice() && !mw.isChrome();
	};
	mw.isSafari = function () {
		return (/safari/).test(userAgent.toLowerCase()) && !mw.isChrome() && !mw.isEdge();
	};
	mw.isIE9Comp = function () {
		return (/msie 7/.test(userAgent.toLowerCase()) && /trident\/5/.test(userAgent.toLowerCase()));
	};
	mw.isIE10Comp = function () {
		return (/msie 7/.test(userAgent.toLowerCase()) && /trident\/6/.test(userAgent.toLowerCase()));
	};
	// Uses hack described at:
	// http://www.bdoran.co.uk/2010/07/19/detecting-the-iphone4-and-resolution-with-javascript-or-php/
	mw.isIphone4 = function () {
		return ( mw.isIphone() && ( window.devicePixelRatio && window.devicePixelRatio >= 2 ) );
	};
	mw.isIpod = function () {
		return (  userAgent.indexOf('iPod') != -1 );
	};
	mw.isIpad = function () {
		return ( userAgent.indexOf('iPad') != -1 );
	};
	mw.isIpad2 = function () {
		return ( mw.isIpad() && window.devicePixelRatio && window.devicePixelRatio < 2 );
	};
	mw.isIpad3 = function () {
		return  /OS 3_/.test(userAgent) && mw.isIpad();
	};

	// Note on those Android checks: Windows Phone browser has "Android" in its userAgent.
	// https://msdn.microsoft.com/en-us/library/hh869301%28v=vs.85%29.aspx
	// So the Android checks must make sure the string does not include "Windows".

	mw.isAndroid44 = function () {
		return ( userAgent.indexOf('Android 4.4') != -1  && userAgent.indexOf('Windows') === -1 );
	};
	mw.isAndroid43 = function () {
		return ( userAgent.indexOf('Android 4.3') != -1  && userAgent.indexOf('Windows') === -1 );
	};
	mw.isAndroid42 = function () {
		return ( userAgent.indexOf('Android 4.2') != -1  && userAgent.indexOf('Windows') === -1 );
	};
	mw.isAndroid41 = function () {
		return ( userAgent.indexOf('Android 4.1') != -1  && userAgent.indexOf('Windows') === -1 );
	};
	mw.isAndroid40 = function () {
		return ( userAgent.indexOf('Android 4.0') != -1  && userAgent.indexOf('Windows') === -1 );
	};
	mw.isAndroid2 = function () {
		return ( userAgent.indexOf('Android 2.') != -1  && userAgent.indexOf('Windows') === -1 );
	};
	mw.isAndroid = function () {
		return ( userAgent.indexOf('Android') != -1 && userAgent.indexOf('Windows') === -1);
	};
	mw.isAndroid4andUp = function () {
		return ( (userAgent.indexOf('Android 4.') != -1) || (userAgent.indexOf('Android 5.') != -1) || (userAgent.indexOf('Android 6.') != -1) ) && userAgent.indexOf('Windows') === -1;
	};

	mw.isFirefox = function () {
		return ( userAgent.indexOf('Firefox') != -1 );
	};
	mw.isChrome = function () {
		return ( userAgent.indexOf('Chrome') != -1 && !mw.isEdge() );
	};
	mw.isAndroidNativeBrowser = function () {
		return (mw.isAndroid() && !mw.isFirefox() && !mw.isChrome());
	};
	mw.isAndroidChromeNativeBrowser = function () {
		return ( mw.isAndroid() && mw.isChrome() );
	};
	mw.isOldAndroidChromeNativeBrowser = function () {
		var regExpResult = userAgent.match(/Chrome\/([0-9][0-9])/);
		if ( regExpResult instanceof Array && regExpResult.length > 1 ){
			return mw.isAndroidChromeNativeBrowser() && parseInt( regExpResult[1] ) < 30;
		}
		return false;
	};
	mw.isMobileChrome = function () {
		return ( mw.isAndroid4andUp()
			&&
			userAgent.indexOf('Chrome') != -1
			)
	};
	mw.isWindowsPhone = function () {
		return userAgent.indexOf('Windows Phone') != -1 ;
	};
	mw.isIOS = function () {
		return ( mw.isIphone() || mw.isIpod() || mw.isIpad() );
	};
	mw.isIOS3 = function () {
		return /OS 3_/.test(userAgent) && mw.isIOS();
	};
	mw.isIOS4 = function () {
		return /OS 4_/.test(userAgent) && mw.isIOS();
	};
	mw.isIOS5 = function () {
		return /OS 5_/.test(userAgent) && mw.isIOS();
	};

	mw.isIOS6 = function () {
		return /OS 6_/.test(userAgent) && mw.isIOS();
	};

	mw.isIOS7 = function () {
		return /OS 7_/.test(userAgent) && mw.isIOS();
	};

	mw.isIOS71 = function () {
		return /OS 7_1/.test(userAgent) && mw.isIOS();
	};

	mw.isIOS8 = function () {
		// Known Limitation - It will return false for iOS8 Simulator
		return ( /OS 8_/.test(userAgent) || /Version\/8/.test(userAgent) ) && mw.isIOS();
	};
	mw.isIOS9 = function () {
		// Known Limitation - It will return false for iOS8 Simulator
		return ( /OS 9_/.test(userAgent) || /Version\/9/.test(userAgent) ) && mw.isIOS();
	};

	mw.isIOSBelow9 = function () {
		// mw.isIOSV() methods check mw.isIOS(), but because of the OR operator it will be checked multiple times.
		// Short-circuit to save many calls.
		return mw.isIOS() && (mw.isIOS3() || mw.isIOS4() || mw.isIOS5() || mw.isIOS6() || mw.isIOS7() || mw.isIOS8());
	};

	mw.isIOS8_9 = function () {
		return mw.isIOS8() || mw.isIOS9();
	};

	mw.isSilk = function () {
		return /\bSilk\b/.test(userAgent);
	};

	// Does the client has native touch bindings?
	mw.hasNativeTouchBindings = function () {
		return (mw.isAndroid41() || mw.isAndroid42() || ( mw.isAndroid() && mw.isFirefox() ));
	};

	// Detect small mobile device ( smartphones )
	mw.isDeviceLessThan480P = function () {
		return matchMedia('only screen and (max-device-width: 480px)').matches;
	};

	mw.hasMouseEvents = function () {
		return !mw.isMobileDevice();
	};

	mw.isTouchDevice = function () {
		return !!('ontouchstart' in window);
	};
	/**
	 * platform detection
	 */
	mw.isMacintosh = function() {
		return navigator.platform.indexOf('Mac') > -1
	};
	mw.isWindows = function() {
		return navigator.platform.indexOf('Win') > -1
	};
	/**
	 * Fallforward system by default prefers flash.
	 *
	 * This is separate from the EmbedPlayer library detection to provide
	 * package loading control NOTE: should be phased out in favor of browser
	 * feature detection where possible
	 *
	 */
	mw.isHTML5FallForwardNative = function () {
		if (mw.isMobileHTML5()) {
			return true;
		}
		// Check for url flag to force html5:
		if (document.URL.indexOf('forceMobileHTML5') != -1) {
			return true;
		}
		// Fall forward native:
		// if the browser supports flash ( don't use html5 )
		if (mw.supportsFlash()) {
			return false;
		}
		// No flash return true if the browser supports html5 video tag with
		// basic support for canPlayType:
		if (mw.supportsHTML5()) {
			return true;
		}

		return false;
	};

	mw.isMobileHTML5 = function () {
		// Check for a mobile html5 user agent:
		if (mw.isIphone() ||
			mw.isIpod() ||
			mw.isIpad() ||
			mw.isAndroid2()
			) {
			return true;
		}
		return false;
	};

	mw.supportsHTML5 = function () {
		// Blackberry is evil in its response to canPlayType calls.
		if (userAgent.indexOf('BlackBerry') != -1) {
			return false;
		}
		var dummyvid = document.createElement("video");
		if (dummyvid.canPlayType) {
			return true;
		}
		return false;
	};

	/**
	 * If the browser supports flash
	 * @return {boolean} true or false if flash > 10 is supported.
	 */
	mw.supportsFlash = function () {
		if (mw.getConfig('EmbedPlayer.DisableHTML5FlashFallback')) {
			return false;
		}

		// Desktop safari flash has "power saving bug" as well as cross domain request issues
		// by default we disable flash on desktop safari.
		if ( ( mw.isDesktopSafari() && !mw.getConfig('ForceFlashOnDesktopSafari') ) || mw.isEdge() ) {
			return false;
		}
		var majorVersion = this.getFlashVersion().split(',').shift();
		if (majorVersion < 10) {
			return false;
		} else {
			return true;
		}
	};

	mw.supportSilverlight = function () {
		return Silverlight.isInstalled("4.0");
	};

	/**
	 * parse JSON string
	 * @return {object} parsed json object. If parsing fails (syntax error): returns retValue if specified. If no retValue was specified - returns an empty object.
	 */
	mw.parseJSON = function (json, retValue) {
		try {
			var parsedJson = JSON.parse(json);
		} catch (e) {
			mw.log("JSON parse syntax error: " + e);
			return retValue ? retValue : null;
		}
		return parsedJson;
	}

	/**
	 * Checks for flash version
	 * @return {string} flash version string
	 */
	mw.getFlashVersion = function () {
		// navigator browsers:
		if (navigator.plugins && navigator.plugins.length) {
			try {
				if (navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin) {
					return (navigator.plugins["Shockwave Flash 2.0"] || navigator.plugins["Shockwave Flash"]).description.replace(/\D+/g, ",").match(/^,?(.+),?$/)[1];
				}
			} catch (e) {
			}
		}
		// IE
		try {
			try {
				if (typeof ActiveXObject != 'undefined') {
					// avoid fp6 minor version lookup issues
					// see: http://blog.deconcept.com/2006/01/11/getvariable-setvariable-crash-internet-explorer-flash-6/
					var axo = new ActiveXObject('ShockwaveFlash.ShockwaveFlash.6');
					try {
						axo.AllowScriptAccess = 'always';
					} catch (e) {
						return '6,0,0';
					}
				}
			} catch (e) {
			}
			return new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version').replace(/\D+/g, ',').match(/^,?(.+),?$/)[1];
		} catch (e) {
		}
		return '0,0,0';
	};

})(window.mediaWiki);
<commit_msg>mw.isSafariDesktop checks also not mw.isEdge (#2891)<commit_after>/**
 * mediawiki.client has some convenience functions for user agent checks
 *
 * TODO this should be combined with or bootstrap jquery.client.js
 */
(function (mw) {

	var userAgent = navigator.userAgent;

	mw.isMobileDevice = function () {
		return ( mw.isIphone() || mw.isIpod() || mw.isIpad() || mw.isAndroid() || mw.isWindowsPhone() || mw.getConfig("EmbedPlayer.ForceNativeComponent") === true || mw.getConfig("EmbedPlayer.SimulateMobile") === true )
	};
	mw.isNativeApp = function () {
		return mw.getConfig("EmbedPlayer.ForceNativeComponent");
	};
	mw.isIphone = function () {
		return ( mw.getConfig("EmbedPlayer.ForceNativeComponent") !== true && userAgent.indexOf('iPhone') != -1 && !mw.isIpad() ) || mw.isIpod();
	};
	mw.isIE = function () {
		return (/msie/.test(userAgent.toLowerCase()) || /trident/.test(userAgent.toLowerCase()));
	};
	mw.isChromeCast = function(){
		return (/CrKey/.test(userAgent));
	};
	mw.isIE7 = function () {
		return (/msie 7/.test(userAgent.toLowerCase()));
	};
	mw.isIE8 = function () {
		return document.documentMode === 8;
	};
	mw.isIE9 = function () {
		return document.documentMode === 9;
	};
    mw.isIE11 = function () {
        return (/trident\/7.0/.test(userAgent.toLowerCase()));
    };
	mw.isEdge = function () {
        return (/edge/.test(userAgent.toLowerCase()));
    };
	mw.isDesktopSafari = function () {
		return mw.isSafari() && !mw.isMobileDevice();
	};
	mw.isSafari = function () {
		return (/safari/).test(userAgent.toLowerCase()) && !mw.isChrome() && !mw.isEdge();
	};
	mw.isIE9Comp = function () {
		return (/msie 7/.test(userAgent.toLowerCase()) && /trident\/5/.test(userAgent.toLowerCase()));
	};
	mw.isIE10Comp = function () {
		return (/msie 7/.test(userAgent.toLowerCase()) && /trident\/6/.test(userAgent.toLowerCase()));
	};
	// Uses hack described at:
	// http://www.bdoran.co.uk/2010/07/19/detecting-the-iphone4-and-resolution-with-javascript-or-php/
	mw.isIphone4 = function () {
		return ( mw.isIphone() && ( window.devicePixelRatio && window.devicePixelRatio >= 2 ) );
	};
	mw.isIpod = function () {
		return (  userAgent.indexOf('iPod') != -1 );
	};
	mw.isIpad = function () {
		return ( userAgent.indexOf('iPad') != -1 );
	};
	mw.isIpad2 = function () {
		return ( mw.isIpad() && window.devicePixelRatio && window.devicePixelRatio < 2 );
	};
	mw.isIpad3 = function () {
		return  /OS 3_/.test(userAgent) && mw.isIpad();
	};

	// Note on those Android checks: Windows Phone browser has "Android" in its userAgent.
	// https://msdn.microsoft.com/en-us/library/hh869301%28v=vs.85%29.aspx
	// So the Android checks must make sure the string does not include "Windows".

	mw.isAndroid44 = function () {
		return ( userAgent.indexOf('Android 4.4') != -1  && userAgent.indexOf('Windows') === -1 );
	};
	mw.isAndroid43 = function () {
		return ( userAgent.indexOf('Android 4.3') != -1  && userAgent.indexOf('Windows') === -1 );
	};
	mw.isAndroid42 = function () {
		return ( userAgent.indexOf('Android 4.2') != -1  && userAgent.indexOf('Windows') === -1 );
	};
	mw.isAndroid41 = function () {
		return ( userAgent.indexOf('Android 4.1') != -1  && userAgent.indexOf('Windows') === -1 );
	};
	mw.isAndroid40 = function () {
		return ( userAgent.indexOf('Android 4.0') != -1  && userAgent.indexOf('Windows') === -1 );
	};
	mw.isAndroid2 = function () {
		return ( userAgent.indexOf('Android 2.') != -1  && userAgent.indexOf('Windows') === -1 );
	};
	mw.isAndroid = function () {
		return ( userAgent.indexOf('Android') != -1 && userAgent.indexOf('Windows') === -1);
	};
	mw.isAndroid4andUp = function () {
		return ( (userAgent.indexOf('Android 4.') != -1) || (userAgent.indexOf('Android 5.') != -1) || (userAgent.indexOf('Android 6.') != -1) ) && userAgent.indexOf('Windows') === -1;
	};

	mw.isFirefox = function () {
		return ( userAgent.indexOf('Firefox') != -1 );
	};
	mw.isChrome = function () {
		return ( userAgent.indexOf('Chrome') != -1 && !mw.isEdge() );
	};
	mw.isAndroidNativeBrowser = function () {
		return (mw.isAndroid() && !mw.isFirefox() && !mw.isChrome());
	};
	mw.isAndroidChromeNativeBrowser = function () {
		return ( mw.isAndroid() && mw.isChrome() );
	};
	mw.isOldAndroidChromeNativeBrowser = function () {
		var regExpResult = userAgent.match(/Chrome\/([0-9][0-9])/);
		if ( regExpResult instanceof Array && regExpResult.length > 1 ){
			return mw.isAndroidChromeNativeBrowser() && parseInt( regExpResult[1] ) < 30;
		}
		return false;
	};
	mw.isMobileChrome = function () {
		return ( mw.isAndroid4andUp()
			&&
			userAgent.indexOf('Chrome') != -1
			)
	};
	mw.isWindowsPhone = function () {
		return userAgent.indexOf('Windows Phone') != -1 ;
	};
	mw.isIOS = function () {
		return ( mw.isIphone() || mw.isIpod() || mw.isIpad() );
	};
	mw.isIOS3 = function () {
		return /OS 3_/.test(userAgent) && mw.isIOS();
	};
	mw.isIOS4 = function () {
		return /OS 4_/.test(userAgent) && mw.isIOS();
	};
	mw.isIOS5 = function () {
		return /OS 5_/.test(userAgent) && mw.isIOS();
	};

	mw.isIOS6 = function () {
		return /OS 6_/.test(userAgent) && mw.isIOS();
	};

	mw.isIOS7 = function () {
		return /OS 7_/.test(userAgent) && mw.isIOS();
	};

	mw.isIOS71 = function () {
		return /OS 7_1/.test(userAgent) && mw.isIOS();
	};

	mw.isIOS8 = function () {
		// Known Limitation - It will return false for iOS8 Simulator
		return ( /OS 8_/.test(userAgent) || /Version\/8/.test(userAgent) ) && mw.isIOS();
	};
	mw.isIOS9 = function () {
		// Known Limitation - It will return false for iOS8 Simulator
		return ( /OS 9_/.test(userAgent) || /Version\/9/.test(userAgent) ) && mw.isIOS();
	};

	mw.isIOSBelow9 = function () {
		// mw.isIOSV() methods check mw.isIOS(), but because of the OR operator it will be checked multiple times.
		// Short-circuit to save many calls.
		return mw.isIOS() && (mw.isIOS3() || mw.isIOS4() || mw.isIOS5() || mw.isIOS6() || mw.isIOS7() || mw.isIOS8());
	};

	mw.isIOS8_9 = function () {
		return mw.isIOS8() || mw.isIOS9();
	};

	mw.isSilk = function () {
		return /\bSilk\b/.test(userAgent);
	};

	// Does the client has native touch bindings?
	mw.hasNativeTouchBindings = function () {
		return (mw.isAndroid41() || mw.isAndroid42() || ( mw.isAndroid() && mw.isFirefox() ));
	};

	// Detect small mobile device ( smartphones )
	mw.isDeviceLessThan480P = function () {
		return matchMedia('only screen and (max-device-width: 480px)').matches;
	};

	mw.hasMouseEvents = function () {
		return !mw.isMobileDevice();
	};

	mw.isTouchDevice = function () {
		return !!('ontouchstart' in window);
	};
	/**
	 * platform detection
	 */
	mw.isMacintosh = function() {
		return navigator.platform.indexOf('Mac') > -1
	};
	mw.isWindows = function() {
		return navigator.platform.indexOf('Win') > -1
	};
	/**
	 * Fallforward system by default prefers flash.
	 *
	 * This is separate from the EmbedPlayer library detection to provide
	 * package loading control NOTE: should be phased out in favor of browser
	 * feature detection where possible
	 *
	 */
	mw.isHTML5FallForwardNative = function () {
		if (mw.isMobileHTML5()) {
			return true;
		}
		// Check for url flag to force html5:
		if (document.URL.indexOf('forceMobileHTML5') != -1) {
			return true;
		}
		// Fall forward native:
		// if the browser supports flash ( don't use html5 )
		if (mw.supportsFlash()) {
			return false;
		}
		// No flash return true if the browser supports html5 video tag with
		// basic support for canPlayType:
		if (mw.supportsHTML5()) {
			return true;
		}

		return false;
	};

	mw.isMobileHTML5 = function () {
		// Check for a mobile html5 user agent:
		if (mw.isIphone() ||
			mw.isIpod() ||
			mw.isIpad() ||
			mw.isAndroid2()
			) {
			return true;
		}
		return false;
	};

	mw.supportsHTML5 = function () {
		// Blackberry is evil in its response to canPlayType calls.
		if (userAgent.indexOf('BlackBerry') != -1) {
			return false;
		}
		var dummyvid = document.createElement("video");
		if (dummyvid.canPlayType) {
			return true;
		}
		return false;
	};

	/**
	 * If the browser supports flash
	 * @return {boolean} true or false if flash > 10 is supported.
	 */
	mw.supportsFlash = function () {
		if (mw.getConfig('EmbedPlayer.DisableHTML5FlashFallback')) {
			return false;
		}

		// Desktop safari flash has "power saving bug" as well as cross domain request issues
		// by default we disable flash on desktop safari.
		if ( ( mw.isDesktopSafari() && !mw.getConfig('ForceFlashOnDesktopSafari') ) || mw.isEdge() ) {
			return false;
		}
		var majorVersion = this.getFlashVersion().split(',').shift();
		if (majorVersion < 10) {
			return false;
		} else {
			return true;
		}
	};

	mw.supportSilverlight = function () {
		return Silverlight.isInstalled("4.0");
	};

	/**
	 * parse JSON string
	 * @return {object} parsed json object. If parsing fails (syntax error): returns retValue if specified. If no retValue was specified - returns an empty object.
	 */
	mw.parseJSON = function (json, retValue) {
		try {
			var parsedJson = JSON.parse(json);
		} catch (e) {
			mw.log("JSON parse syntax error: " + e);
			return retValue ? retValue : null;
		}
		return parsedJson;
	}

	/**
	 * Checks for flash version
	 * @return {string} flash version string
	 */
	mw.getFlashVersion = function () {
		// navigator browsers:
		if (navigator.plugins && navigator.plugins.length) {
			try {
				if (navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin) {
					return (navigator.plugins["Shockwave Flash 2.0"] || navigator.plugins["Shockwave Flash"]).description.replace(/\D+/g, ",").match(/^,?(.+),?$/)[1];
				}
			} catch (e) {
			}
		}
		// IE
		try {
			try {
				if (typeof ActiveXObject != 'undefined') {
					// avoid fp6 minor version lookup issues
					// see: http://blog.deconcept.com/2006/01/11/getvariable-setvariable-crash-internet-explorer-flash-6/
					var axo = new ActiveXObject('ShockwaveFlash.ShockwaveFlash.6');
					try {
						axo.AllowScriptAccess = 'always';
					} catch (e) {
						return '6,0,0';
					}
				}
			} catch (e) {
			}
			return new ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version').replace(/\D+/g, ',').match(/^,?(.+),?$/)[1];
		} catch (e) {
		}
		return '0,0,0';
	};

})(window.mediaWiki);
<|endoftext|>


<commit_before>const httpErrors = require('http-errors');
const axios = require('axios');

// Load global variables and functions
const globals = require('../globals');
const { logRESTCall } = require('../lib/log_rest_call');
const { apiPostNewRelicMetric } = require('../api/newrelic_metric');

// eslint-disable-next-line consistent-return
async function handlerPostNewRelicMetric(request, reply) {
    try {
        logRESTCall(request);

        const payload = [];
        const metrics = [];
        const attributes = {};
        const ts = new Date().getTime(); // Timestamp in millisec

        // TODO sanity check parameters in REST call

        // Add static fields to attributes
        if (globals.config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static')) {
            const staticAttributes = globals.config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static');

            if (staticAttributes !== null && staticAttributes.length > 0) {
                // eslint-disable-next-line no-restricted-syntax
                for (const item of staticAttributes) {
                    attributes[item.name] = item.value;
                }
            }
        }

        // Add attributes passed as parameters
        if (request.body.attributes && request.body.attributes.length > 0) {
            // eslint-disable-next-line no-restricted-syntax
            for (const item of request.body.attributes) {
                attributes[item.name] = item.value;
            }
        }

        // Build New Relic metric common block
        const common = {
            timestamp: request.body.timestamp > 0 ? request.body.timestamp : ts,
            attributes,
        };

        // Interval is required for metric types "count" and "summary"
        if (request.body.type.toLowerCase() === 'count' || request.body.type.toLowerCase() === 'summary') {
            common['interval.ms'] = request.body.interval;
        }

        metrics.push({
            name: request.body.name,
            type: request.body.type,
            value: request.body.value,
        });

        // Build final payload
        payload.push({
            common,
            metrics,
        });

        globals.logger.debug(`NEWRELIC METRIC: Payload: ${JSON.stringify(payload, null, 2)}`);

        // Preapare call to remote host
        const remoteUrl = globals.config.get('Butler.uptimeMonitor.storeNewRelic.url');

        // Add headers
        const headers = {
            'Content-Type': 'application/json; charset=utf-8',
            'Api-Key': globals.config.get('Butler.thirdPartyToolsCredentials.newRelic.insertApiKey'),
        };

        if (globals.config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header') !== null) {
            // eslint-disable-next-line no-restricted-syntax
            for (const header of globals.config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header')) {
                headers[header.name] = header.value;
            }
        }

        const res = await axios.post(remoteUrl, payload, { headers, timeout: 5000 });
        globals.logger.debug(`NEWRELIC METRIC: Result code from posting metric to New Relic: ${res.status}, ${res.statusText}`);

        if (res.status === 202) {
            // Posting done without error
            globals.logger.verbose(`NEWRELIC METRIC: Sent metric to New Relic`);
            reply.type('text/plain').code(202).send(res.statusText);
            // reply.type('application/json; charset=utf-8').code(201).send(JSON.stringify(request.body));
        } else {
            reply.send(httpErrors(res.status, `Failed posting metric to New Relic: ${res.statusText}`));
        }

        // Required parameter is missing
        // reply.send(httpErrors(400, 'Required parameter missing'));
    } catch (err) {
        globals.logger.error(
            `NEWRELIC METRIC: Failed posting metric to New Relic: ${JSON.stringify(request.body, null, 2)}, error is: ${JSON.stringify(
                err,
                null,
                2
            )}`
        );
        reply.send(httpErrors(500, 'Failed posting metric to New Relic'));
    }
}

// eslint-disable-next-line no-unused-vars
module.exports = async (fastify, options) => {
    if (
        globals.config.has('Butler.restServerEndpointsEnable.newRelic.postNewRelicMetric') &&
        globals.config.get('Butler.restServerEndpointsEnable.newRelic.postNewRelicMetric') === true
    ) {
        globals.logger.debug('Registering REST endpoint POST /v4/newrelic/metric');
        fastify.post('/v4/newrelic/metric', apiPostNewRelicMetric, handlerPostNewRelicMetric);
    }
};
<commit_msg>fix: Incorrect New Relic API url used when posting metrics via Butler's REST API Fixes #468<commit_after>const httpErrors = require('http-errors');
const axios = require('axios');

// Load global variables and functions
const globals = require('../globals');
const { logRESTCall } = require('../lib/log_rest_call');
const { apiPostNewRelicMetric } = require('../api/newrelic_metric');

// eslint-disable-next-line consistent-return
async function handlerPostNewRelicMetric(request, reply) {
    try {
        logRESTCall(request);

        const payload = [];
        const metrics = [];
        const attributes = {};
        const ts = new Date().getTime(); // Timestamp in millisec

        // TODO sanity check parameters in REST call

        // Add static fields to attributes
        if (globals.config.has('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static')) {
            const staticAttributes = globals.config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.attribute.static');

            if (staticAttributes !== null && staticAttributes.length > 0) {
                // eslint-disable-next-line no-restricted-syntax
                for (const item of staticAttributes) {
                    attributes[item.name] = item.value;
                }
            }
        }

        // Add attributes passed as parameters
        if (request.body.attributes && request.body.attributes.length > 0) {
            // eslint-disable-next-line no-restricted-syntax
            for (const item of request.body.attributes) {
                attributes[item.name] = item.value;
            }
        }

        // Build New Relic metric common block
        const common = {
            timestamp: request.body.timestamp > 0 ? request.body.timestamp : ts,
            attributes,
        };

        // Interval is required for metric types "count" and "summary"
        if (request.body.type.toLowerCase() === 'count' || request.body.type.toLowerCase() === 'summary') {
            common['interval.ms'] = request.body.interval;
        }

        metrics.push({
            name: request.body.name,
            type: request.body.type,
            value: request.body.value,
        });

        // Build final payload
        payload.push({
            common,
            metrics,
        });

        globals.logger.debug(`NEWRELIC METRIC: Payload: ${JSON.stringify(payload, null, 2)}`);

        // Preapare call to remote host
        const remoteUrl = globals.config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.url');

        // Add headers
        const headers = {
            'Content-Type': 'application/json; charset=utf-8',
            'Api-Key': globals.config.get('Butler.thirdPartyToolsCredentials.newRelic.insertApiKey'),
        };

        if (globals.config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header') !== null) {
            // eslint-disable-next-line no-restricted-syntax
            for (const header of globals.config.get('Butler.restServerEndpointsConfig.newRelic.postNewRelicMetric.header')) {
                headers[header.name] = header.value;
            }
        }

        const res = await axios.post(remoteUrl, payload, { headers, timeout: 5000 });
        globals.logger.debug(`NEWRELIC METRIC: Result code from posting metric to New Relic: ${res.status}, ${res.statusText}`);

        if (res.status === 202) {
            // Posting done without error
            globals.logger.verbose(`NEWRELIC METRIC: Sent metric to New Relic`);
            reply.type('text/plain').code(202).send(res.statusText);
            // reply.type('application/json; charset=utf-8').code(201).send(JSON.stringify(request.body));
        } else {
            reply.send(httpErrors(res.status, `Failed posting metric to New Relic: ${res.statusText}`));
        }

        // Required parameter is missing
        // reply.send(httpErrors(400, 'Required parameter missing'));
    } catch (err) {
        globals.logger.error(
            `NEWRELIC METRIC: Failed posting metric to New Relic: ${JSON.stringify(request.body, null, 2)}, error is: ${JSON.stringify(
                err,
                null,
                2
            )}`
        );
        reply.send(httpErrors(500, 'Failed posting metric to New Relic'));
    }
}

// eslint-disable-next-line no-unused-vars
module.exports = async (fastify, options) => {
    if (
        globals.config.has('Butler.restServerEndpointsEnable.newRelic.postNewRelicMetric') &&
        globals.config.get('Butler.restServerEndpointsEnable.newRelic.postNewRelicMetric') === true
    ) {
        globals.logger.debug('Registering REST endpoint POST /v4/newrelic/metric');
        fastify.post('/v4/newrelic/metric', apiPostNewRelicMetric, handlerPostNewRelicMetric);
    }
};
<|endoftext|>