text: /**
   * Copyright 2017 The AMP HTML Authors. All Rights Reserved.
   *
   * 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.
   */
  
  import {
    BatchSegmentDef,
    RequestDef,
    TransportSerializerDef,
    TransportSerializers,
    defaultSerializer,
  } from './transport-serializer';
  import {IframeTransport} from './iframe-transport';
  import {Services} from '../../../src/services';
  import {WindowInterface} from '../../../src/window-interface';
  import {
    assertHttpsUrl,
    checkCorsUrl,
    isAmpScriptUri,
    parseUrlDeprecated,
  } from '../../../src/url';
  import {createPixel} from '../../../src/pixel';
  import {dev, user, userAssert} from '../../../src/log';
  import {getAmpAdResourceId} from '../../../src/ad-helper';
  import {getMode} from '../../../src/mode';
  import {getTopWindow} from '../../../src/service';
  
  import {loadPromise} from '../../../src/event-helper';
  import {removeElement} from '../../../src/dom';
  import {toWin} from '../../../src/types';
  import {toggle} from '../../../src/style';
  
  /** @const {string} */
  const TAG_ = 'amp-analytics/transport';
  
  /**
   * Transport defines the ways how the analytics pings are going to be sent.
   */
  export class Transport {
    /**
     * @param {!AmpDoc} ampdoc
     * @param {!JsonObject} options
     */
    constructor(ampdoc, options = /** @type {!JsonObject} */ ({})) {
      /** @private {!AmpDoc} */
      this.ampdoc_ = ampdoc;
  
      /** @private {!Window} */
      this.win_ = ampdoc.win;
  
      /** @private {!JsonObject} */
      this.options_ = options;
  
      /** @private {string|undefined} */
      this.referrerPolicy_ = /** @type {string|undefined} */ (
        this.options_['referrerPolicy']
      );
  
      // no-referrer is only supported in image transport
      if (this.referrerPolicy_ === 'no-referrer') {
        this.options_['beacon'] = false;
        this.options_['xhrpost'] = false;
      }
  
      /** @private {boolean} */
      this.useBody_ = !!this.options_['useBody'];
  
      /** @private {?IframeTransport} */
      this.iframeTransport_ = null;
  
      /** @private {boolean} */
      this.isInabox_ = getMode(this.win_).runtime == 'inabox';
    }
  
    /**
     * @param {string} url
     * @param {!Array<!BatchSegmentDef>} segments
     * @param {boolean} inBatch
     */
    sendRequest(url, segments, inBatch) {
      if (!url || segments.length === 0) {
        dev().info(TAG_, 'Empty request not sent: ', url);
        return;
      }
      const serializer = this.getSerializer_();
      /**
       * @param {boolean} withPayload
       * @return {!RequestDef}
       */
      function generateRequest(withPayload) {
        const request = inBatch
          ? serializer.generateBatchRequest(url, segments, withPayload)
          : serializer.generateRequest(url, segments[0], withPayload);
        if (!isAmpScriptUri(request.url)) {
          assertHttpsUrl(request.url, 'amp-analytics request');
          checkCorsUrl(request.url);
        }
        return request;
      }
  
      const getRequest = cacheFuncResult(generateRequest);
  
      if (this.options_['iframe']) {
        if (!this.iframeTransport_) {
          dev().error(TAG_, 'iframe transport was inadvertently deleted');
          return;
        }
        this.iframeTransport_.sendRequest(getRequest(false).url);
        return;
      }
  
      if (this.options_['amp-script']) {
        Transport.forwardRequestToAmpScript(this.ampdoc_, {
          url,
          payload: getRequest(true).payload,
        });
        return;
      }
  
      if (
        this.options_['beacon'] &&
        Transport.sendRequestUsingBeacon(this.win_, getRequest(this.useBody_))
      ) {
        return;
      }
      if (
        this.options_['xhrpost'] &&
        Transport.sendRequestUsingXhr(this.win_, getRequest(this.useBody_))
      ) {
        return;
      }
      const image = this.options_['image'];
      if (image) {
        const suppressWarnings =
          typeof image == 'object' && image['suppressWarnings'];
        Transport.sendRequestUsingImage(
          this.win_,
          getRequest(false),
          suppressWarnings,
          /** @type {string|undefined} */ (this.referrerPolicy_)
        );
        return;
      }
      user().warn(TAG_, 'Failed to send request', url, this.options_);
    }
  
    /**
     * amp-analytics will create an iframe for vendors in
     * extensions/amp-analytics/0.1/vendors/* who have transport/iframe defined.
     * This is limited to MRC-accreddited vendors. The frame is removed if the
     * user navigates/swipes away from the page, and is recreated if the user
     * navigates back to the page.
     *
     * @param {!Element} element
     */
    maybeInitIframeTransport(element) {
      if (!this.options_['iframe'] || this.iframeTransport_) {
        return;
      }
  
      // In the case of FIE rendering, we should be using the parent doc win.
      const topWin = getTopWindow(toWin(element.ownerDocument.defaultView));
      const type = element.getAttribute('type');
      // In inabox there is no amp-ad element.
      const ampAdResourceId = this.isInabox_
        ? '1'
        : user().assertString(
            getAmpAdResourceId(element, topWin),
            'No friendly amp-ad ancestor element was found ' +
              'for amp-analytics tag with iframe transport.'
          );
  
      this.iframeTransport_ = new IframeTransport(
        topWin,
        type,
        this.options_,
        ampAdResourceId
      );
    }
  
    /**
     * Deletes iframe transport.
     */
    deleteIframeTransport() {
      if (this.iframeTransport_) {
        this.iframeTransport_.detach();
        this.iframeTransport_ = null;
      }
    }
  
    /**
     * Sends a ping request using an iframe, that is removed 5 seconds after
     * it is loaded.
     * This is not available as a standard transport, but rather used for
     * specific, allowlisted requests.
     * Note that this is unrelated to the iframeTransport
     *
     * @param {string} url
     * @param {!BatchSegmentDef} segment
     */
    sendRequestUsingIframe(url, segment) {
      const request = defaultSerializer(url, [segment]);
      if (!request) {
        user().error(TAG_, 'Request not sent. Contents empty.');
        return;
      }
  
      assertHttpsUrl(request, 'amp-analytics request');
      userAssert(
        parseUrlDeprecated(request).origin !=
          parseUrlDeprecated(this.win_.location.href).origin,
        'Origin of iframe request must not be equal to the document origin.' +
          ' See https://github.com/ampproject/' +
          'amphtml/blob/main/docs/spec/amp-iframe-origin-policy.md for details.'
      );
  
      /** @const {!Element} */
      const iframe = this.win_.document.createElement('iframe');
      toggle(iframe, false);
      iframe.onload = iframe.onerror = () => {
        Services.timerFor(this.win_).delay(() => {
          removeElement(iframe);
        }, 5000);
      };
  
      iframe.setAttribute('amp-analytics', '');
      iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin');
      iframe.src = request;
      this.win_.document.body.appendChild(iframe);
    }
  
    /**
     * @return {!TransportSerializerDef}
     */
    getSerializer_() {
      return /** @type {!TransportSerializerDef} */ (
        TransportSerializers['default']
      );
    }
  
    /**
     * @param {!Window} win
     * @param {!RequestDef} request
     * @param {boolean} suppressWarnings
     * @param {string|undefined} referrerPolicy
     */
    static sendRequestUsingImage(win, request, suppressWarnings, referrerPolicy) {
      if (!win) {
        return;
      }
      const image = createPixel(win, request.url, referrerPolicy);
      loadPromise(image)
        .then(() => {
          dev().fine(TAG_, 'Sent image request', request.url);
        })
        .catch(() => {
          if (!suppressWarnings) {
            user().warn(
              TAG_,
              'Response unparseable or failed to send image request',
              request.url
            );
          }
        });
    }
  
    /**
     * @param {!Window} win
     * @param {!RequestDef} request
     * @return {boolean} True if this browser supports navigator.sendBeacon.
     */
    static sendRequestUsingBeacon(win, request) {
      const sendBeacon = WindowInterface.getSendBeacon(win);
      if (!sendBeacon) {
        return false;
      }
      const result = sendBeacon(request.url, request.payload || '');
      if (result) {
        dev().fine(TAG_, 'Sent beacon request', request);
      }
      return result;
    }
  
    /**
     * @param {!Window} win
     * @param {!RequestDef} request
     * @return {boolean} True if this browser supports cross-domain XHR.
     */
    static sendRequestUsingXhr(win, request) {
      const XMLHttpRequest = WindowInterface.getXMLHttpRequest(win);
      if (!XMLHttpRequest) {
        return false;
      }
      const xhr = new XMLHttpRequest();
      if (!('withCredentials' in xhr)) {
        return false; // Looks like XHR level 1 - CORS is not supported.
      }
      xhr.open('POST', request.url, true);
      xhr.withCredentials = true;
  
      // Prevent pre-flight HEAD request.
      xhr.setRequestHeader('Content-Type', 'text/plain');
  
      xhr.onreadystatechange = () => {
        if (xhr.readyState == 4) {
          dev().fine(TAG_, 'Sent XHR request', request.url);
        }
      };
  
      xhr.send(request.payload || '');
      return true;
    }
  
    /**
     * @param {!AmpDoc} ampdoc
     * @param {!RequestDef} request
     * @return {!Promise}
     */
    static forwardRequestToAmpScript(ampdoc, request) {
      return Services.scriptForDocOrNull(ampdoc).then((ampScriptService) => {
        userAssert(ampScriptService, 'AMP-SCRIPT is not installed');
        ampScriptService.fetch(request.url, JSON.parse(request.payload));
      });
    }
  }
  
  /**
   * A helper method that wraps a function and cache its return value.
   *
   * @param {!Function} func the function to cache
   * @return {!Function}
   */
  function cacheFuncResult(func) {
    const cachedValue = {};
    return (arg) => {
      const key = String(arg);
      if (cachedValue[key] === undefined) {
        cachedValue[key] = func(arg);
      }
      return cachedValue[key];
    };
  }
  