<commit_before>/**
 * Copyright 2018 The Subscribe with Google 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 {ExperimentFlags} from './experiment-flags';
import {PaymentsAsyncClient} from '../../third_party/gpay/src/payjs_async';
import {Preconnect} from '../utils/preconnect';
import {bytesToString, stringToBytes} from '../utils/bytes';
import {createCancelError} from '../utils/errors';
import {feCached} from './services';
import {isExperimentOn} from './experiments';

const REDIRECT_STORAGE_KEY = 'subscribe.google.com:rk';

/**
 * @typedef {{
 *   forceRedirect: (boolean|undefined),
 * }}
 */
export let PayOptionsDef;

/**
 * @const {!Object<string, string>}
 * @package Visible for testing only.
 */
export const PAY_ORIGIN = {
  'PRODUCTION': 'https://pay.google.com',
  'SANDBOX': 'https://pay.sandbox.google.com',
};

/** @return {string} */
function payUrl() {
  return feCached(PAY_ORIGIN['$payEnvironment$'] + '/gp/p/ui/pay');
}

/**
 */
export class PayClient {
  /**
   * @param {!./deps.DepsDef} deps
   */
  constructor(deps) {
    /** @private @const {!Window} */
    this.win_ = deps.win();

    /** @private @const {!../components/activities.ActivityPorts} */
    this.activityPorts_ = deps.activities();

    /** @private {?function(!Promise<!PaymentData>)} */
    this.responseCallback_ = null;

    /** @private {?PaymentDataRequest} */
    this.request_ = null;

    /** @private {?Promise<!PaymentData>} */
    this.response_ = null;

    /** @private @const {!./analytics-service.AnalyticsService} */
    this.analytics_ = deps.analytics();

    /** @private @const {!RedirectVerifierHelper} */
    this.redirectVerifierHelper_ = new RedirectVerifierHelper(this.win_);

    /** @private @const {?PaymentsAsyncClient} */
    this.client_ = null;

    if (!isExperimentOn(this.win_, ExperimentFlags.PAY_CLIENT_LAZYLOAD)) {
      this.client_ = this.createClient_(
        /** @type {!PaymentOptions} */
        ({
          environment: '$payEnvironment$',
          'i': {
            'redirectKey': this.redirectVerifierHelper_.restoreKey(),
          },
        }),
        this.analytics_.getTransactionId(),
        this.handleResponse_.bind(this)
      );
    } else {
      /** @private @const {!Preconnect} */
      this.preconnect_ = new Preconnect(this.win_.document);
    }

    // Prepare new verifier pair.
    this.redirectVerifierHelper_.prepare();

    /** @private @const {!./client-event-manager.ClientEventManager} */
    this.eventManager_ = deps.eventManager();
  }

  /**
   * @param {!PaymentOptions} options
   * @param {string} googleTransactionId
   * @param {function(!Promise<!PaymentData>)} handler
   * @return {!PaymentsAsyncClient}
   * @private
   */
  createClient_(options, googleTransactionId, handler) {
    // Assign Google Transaction ID to PaymentsAsyncClient.googleTransactionId_
    // so it can be passed to gpay_async.js and stored in payment clearcut log.
    PaymentsAsyncClient.googleTransactionId_ = googleTransactionId;
    return new PaymentsAsyncClient(
      options,
      handler,
      /* useIframe */ false,
      this.activityPorts_.getOriginalWebActivityPorts()
    );
  }

  /**
   * @param {!../utils/preconnect.Preconnect} pre
   */
  preconnect(pre) {
    pre.prefetch(payUrl());
    pre.prefetch(
      'https://payments.google.com/payments/v4/js/integrator.js?ss=md'
    );
    pre.prefetch('https://clients2.google.com/gr/gr_full_2.0.6.js');
  }

  /**
   * @return {string}
   */
  getType() {
    // TODO(alin04): remove once all references removed.
    return 'PAYJS';
  }

  /**
   * @param {!PaymentDataRequest} paymentRequest
   * @param {!PayOptionsDef=} options
   */
  start(paymentRequest, options = {}) {
    this.request_ = paymentRequest;

    if (
      isExperimentOn(this.win_, ExperimentFlags.PAY_CLIENT_LAZYLOAD) &&
      !this.client_
    ) {
      this.preconnect(this.preconnect_);
      this.client_ = this.createClient_(
        /** @type {!PaymentOptions} */
        ({
          environment: '$payEnvironment$',
          'i': {
            'redirectKey': this.redirectVerifierHelper_.restoreKey(),
          },
        }),
        this.analytics_.getTransactionId(),
        this.handleResponse_.bind(this)
      );
    }
    if (options.forceRedirect) {
      paymentRequest = Object.assign(paymentRequest, {
        'forceRedirect': options.forceRedirect || false,
      });
    }
    setInternalParam(
      paymentRequest,
      'disableNative',
      // The page cannot be iframed at this time. May be relaxed later
      // for AMP and similar contexts.
      this.win_ != this.top_()
    );
    let resolver = null;
    const promise = new Promise((resolve) => (resolver = resolve));
    // Notice that the callback for verifier may execute asynchronously.
    this.redirectVerifierHelper_.useVerifier((verifier) => {
      if (verifier) {
        setInternalParam(paymentRequest, 'redirectVerifier', verifier);
      }
      if (options.forceRedirect) {
        const client = this.client_;
        this.eventManager_.getReadyPromise().then(() => {
          this.analytics_.getLoggingPromise().then(() => {
            client.loadPaymentData(paymentRequest);
            resolver(true);
          });
        });
      } else {
        this.client_.loadPaymentData(paymentRequest);
        resolver(true);
      }
    });
    return promise;
  }

  /**
   * @param {function(!Promise<!PaymentData>)} callback
   */
  onResponse(callback) {
    this.responseCallback_ = callback;
    const response = this.response_;
    if (response) {
      Promise.resolve().then(() => {
        if (response) {
          callback(this.convertResponse_(response, this.request_));
        }
      });
    }
  }

  /**
   * @param {!Promise<!PaymentData>} responsePromise
   * @private
   */
  handleResponse_(responsePromise) {
    this.response_ = responsePromise;
    if (this.responseCallback_) {
      this.responseCallback_(
        this.convertResponse_(this.response_, this.request_)
      );
    }
  }

  /**
   * @param {!Promise<!PaymentData>} response
   * @param {?PaymentDataRequest} request
   * @return {!Promise<!PaymentData>}
   * @private
   */
  convertResponse_(response, request) {
    return response
      .then(
        // Temporary client side solution to remember the
        // input params. TODO: Remove this once server-side
        // input preservation is done and is part of the response.
        (res) => {
          if (request) {
            res['paymentRequest'] = request;
          }
          return res;
        }
      )
      .catch((reason) => {
        if (typeof reason == 'object' && reason['statusCode'] == 'CANCELED') {
          const error = createCancelError(this.win_);
          if (request) {
            error['productType'] = /** @type {!PaymentDataRequest} */ (request)[
              'i'
            ]['productType'];
          } else {
            error['productType'] = null;
          }
          return Promise.reject(error);
        }
        return Promise.reject(reason);
      });
  }

  /**
   * @return {!Window}
   * @private
   */
  top_() {
    // Only exists for testing since it's not possible to override `window.top`.
    return this.win_.top;
  }
}

/**
 * @typedef {{
 *   key: string,
 *   verifier: string,
 * }}
 */
let RedirectVerifierPairDef;

/**
 * This helper generates key/verifier pair for the redirect mode. When the
 * redirect mode is used, the encrypted payload is returned via nivigation URL.
 * This payload need to be decrypted and to avoid session fixation attacks, a
 * verifier has to be used. This redirect verifier is not the only session
 * verifier in use: we also use GAIA. However, we have to fallback to this
 * verifier when GAIA is not available.
 *
 * @package Visible for testing only.
 */
export class RedirectVerifierHelper {
  /**
   * @param {!Window} win
   */
  constructor(win) {
    /** @private @const {!Window} */
    this.win_ = win;

    /** @private {boolean} */
    this.pairCreated_ = false;

    /** @private {?RedirectVerifierPairDef} */
    this.pair_ = null;

    /** @private {?Promise<?RedirectVerifierPairDef>} */
    this.pairPromise_ = null;
  }

  /**
   * To avoid popup blockers, the key/verifier pair is created as soon as
   * possible.
   * @return {?Promise}
   */
  prepare() {
    return this.getOrCreatePair_(() => {});
  }

  /**
   * Calls the provided callback with the generated redirect verifier. This
   * API is sync/async, which is a big anti-pattern. However, it's necessary
   * to reduce the risk of popup blockers. If the verifier is already available
   * (see `prepare` method), the callback will be called immediately and thus
   * in the same event loop as the user action.
   *
   * The return verifier could be `null`. This could mean either that its
   * generation failed, or if the platform doesn't support necessary APIs, such
   * as Web Crypto. The redirect can still proceed and try to fallback on GAIA
   * as a redirect verifier. The set of platforms where GAIA is not available
   * and the redirect verifier cannot be created is negligible.
   *
   * The key corresponding to the returned verifier is stored in the session
   * storage and can be later restored using `restoreKey` method.
   *
   * @param {function(?string)} callback
   */
  useVerifier(callback) {
    this.getOrCreatePair_((pair) => {
      if (pair) {
        try {
          this.win_.localStorage.setItem(REDIRECT_STORAGE_KEY, pair.key);
        } catch (e) {
          // If storage has failed, there's no point in using the verifer.
          // However, there are other ways to recover the redirect, so it's
          // not necessarily a fatal condition.
          pair = null;
        }
      }
      callback((pair && pair.verifier) || null);
    });
  }

  /**
   * Restores the redirect key from the session storage. The key may be null.
   * @return {?string}
   */
  restoreKey() {
    try {
      return (
        (this.win_.localStorage &&
          this.win_.localStorage.getItem(REDIRECT_STORAGE_KEY)) ||
        null
      );
    } catch (e) {
      return null;
    }
  }

  /**
   * @param {function(?RedirectVerifierPairDef)} callback
   * @return {?Promise}
   * @private
   */
  getOrCreatePair_(callback) {
    this.createPair_();
    if (this.pairCreated_) {
      // Already created.
      callback(this.pair_);
    } else if (this.pairPromise_) {
      // Otherwise wait for it to be created.
      this.pairPromise_.then((pair) => callback(pair));
    }
    return this.pairPromise_;
  }

  /**
   * @private
   */
  createPair_() {
    // Either already created or already started.
    if (this.pairCreated_ || this.pairPromise_) {
      return;
    }

    // Check that the platform can fully support verification. That means
    // that it's expected to implement the following APIs:
    // a. Local storage (localStorage);
    // b. WebCrypto (crypto.subtle);
    // c. Crypto random (crypto.getRandomValues);
    // d. SHA284 (crypto.subtle.digest).
    let supportsLocalStorage;
    try {
      supportsLocalStorage = !!this.win_.localStorage;
    } catch (e) {
      // Note: This can happen when cookies are disabled.
      supportsLocalStorage = false;
    }
    const crypto = this.win_.crypto;
    if (
      supportsLocalStorage &&
      crypto &&
      crypto.getRandomValues &&
      crypto.subtle &&
      crypto.subtle.digest
    ) {
      this.pairPromise_ = new Promise((resolve, reject) => {
        // 1. Use crypto random to create a 128-bit (16 byte) redirect key.
        const keyBytes = new Uint8Array(16);
        crypto.getRandomValues(keyBytes);

        // 2. Encode key as base64.
        const key = btoa(bytesToString(keyBytes));

        // 3. Create a hash.
        crypto.subtle.digest({name: 'SHA-384'}, stringToBytes(key)).then(
          (buffer) => {
            const verifier = btoa(
              bytesToString(
                new Uint8Array(/** @type {!ArrayBuffer} */ (buffer))
              )
            );
            resolve({key, verifier});
          },
          (reason) => {
            reject(reason);
          }
        );
      })
        .catch(() => {
          // Ignore failures. A failure to create a redirect verifier is often
          // recoverable.
          return null;
        })
        .then((pair) => {
          this.pairCreated_ = true;
          this.pair_ = pair;
          return pair;
        });
    } else {
      // Not supported.
      this.pairCreated_ = true;
      this.pair_ = null;
    }
  }
}

/**
 * @param {!PaymentDataRequest} paymentRequest
 * @param {string} param
 * @param {*} value
 */
function setInternalParam(paymentRequest, param, value) {
  paymentRequest['i'] = Object.assign(paymentRequest['i'] || {}, {
    [param]: value,
  });
}
<commit_msg>Remove whitespaces from pay-client.js<commit_after>/**
 * Copyright 2018 The Subscribe with Google 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 {ExperimentFlags} from './experiment-flags';
import {PaymentsAsyncClient} from '../../third_party/gpay/src/payjs_async';
import {Preconnect} from '../utils/preconnect';
import {bytesToString, stringToBytes} from '../utils/bytes';
import {createCancelError} from '../utils/errors';
import {feCached} from './services';
import {isExperimentOn} from './experiments';

const REDIRECT_STORAGE_KEY = 'subscribe.google.com:rk';

/**
 * @typedef {{
 *   forceRedirect: (boolean|undefined),
 * }}
 */
export let PayOptionsDef;

/**
 * @const {!Object<string, string>}
 * @package Visible for testing only.
 */
export const PAY_ORIGIN = {
  'PRODUCTION': 'https://pay.google.com',
  'SANDBOX': 'https://pay.sandbox.google.com',
};

/** @return {string} */
function payUrl() {
  return feCached(PAY_ORIGIN['$payEnvironment$'] + '/gp/p/ui/pay');
}

/**
 */
export class PayClient {
  /**
   * @param {!./deps.DepsDef} deps
   */
  constructor(deps) {
    /** @private @const {!Window} */
    this.win_ = deps.win();

    /** @private @const {!../components/activities.ActivityPorts} */
    this.activityPorts_ = deps.activities();

    /** @private {?function(!Promise<!PaymentData>)} */
    this.responseCallback_ = null;

    /** @private {?PaymentDataRequest} */
    this.request_ = null;

    /** @private {?Promise<!PaymentData>} */
    this.response_ = null;

    /** @private @const {!./analytics-service.AnalyticsService} */
    this.analytics_ = deps.analytics();

    /** @private @const {!RedirectVerifierHelper} */
    this.redirectVerifierHelper_ = new RedirectVerifierHelper(this.win_);

    /** @private @const {?PaymentsAsyncClient} */
    this.client_ = null;

    if (!isExperimentOn(this.win_, ExperimentFlags.PAY_CLIENT_LAZYLOAD)) {
      this.client_ = this.createClient_(
        /** @type {!PaymentOptions} */
        ({
          environment: '$payEnvironment$',
          'i': {
            'redirectKey': this.redirectVerifierHelper_.restoreKey(),
          },
        }),
        this.analytics_.getTransactionId(),
        this.handleResponse_.bind(this)
      );
    } else {
      /** @private @const {!Preconnect} */
      this.preconnect_ = new Preconnect(this.win_.document);
    }

    // Prepare new verifier pair.
    this.redirectVerifierHelper_.prepare();

    /** @private @const {!./client-event-manager.ClientEventManager} */
    this.eventManager_ = deps.eventManager();
  }

  /**
   * @param {!PaymentOptions} options
   * @param {string} googleTransactionId
   * @param {function(!Promise<!PaymentData>)} handler
   * @return {!PaymentsAsyncClient}
   * @private
   */
  createClient_(options, googleTransactionId, handler) {
    // Assign Google Transaction ID to PaymentsAsyncClient.googleTransactionId_
    // so it can be passed to gpay_async.js and stored in payment clearcut log.
    PaymentsAsyncClient.googleTransactionId_ = googleTransactionId;
    return new PaymentsAsyncClient(
      options,
      handler,
      /* useIframe */ false,
      this.activityPorts_.getOriginalWebActivityPorts()
    );
  }

  /**
   * @param {!../utils/preconnect.Preconnect} pre
   */
  preconnect(pre) {
    pre.prefetch(payUrl());
    pre.prefetch(
      'https://payments.google.com/payments/v4/js/integrator.js?ss=md'
    );
    pre.prefetch('https://clients2.google.com/gr/gr_full_2.0.6.js');
  }

  /**
   * @return {string}
   */
  getType() {
    // TODO(alin04): remove once all references removed.
    return 'PAYJS';
  }

  /**
   * @param {!PaymentDataRequest} paymentRequest
   * @param {!PayOptionsDef=} options
   */
  start(paymentRequest, options = {}) {
    this.request_ = paymentRequest;

    if (
      isExperimentOn(this.win_, ExperimentFlags.PAY_CLIENT_LAZYLOAD) &&
      !this.client_
    ) {
      this.preconnect(this.preconnect_);
      this.client_ = this.createClient_(
        /** @type {!PaymentOptions} */
        ({
          environment: '$payEnvironment$',
          'i': {
            'redirectKey': this.redirectVerifierHelper_.restoreKey(),
          },
        }),
        this.analytics_.getTransactionId(),
        this.handleResponse_.bind(this)
      );
    }
    if (options.forceRedirect) {
      paymentRequest = Object.assign(paymentRequest, {
        'forceRedirect': options.forceRedirect || false,
      });
    }
    setInternalParam(
      paymentRequest,
      'disableNative',
      // The page cannot be iframed at this time. May be relaxed later
      // for AMP and similar contexts.
      this.win_ != this.top_()
    );
    let resolver = null;
    const promise = new Promise((resolve) => (resolver = resolve));
    // Notice that the callback for verifier may execute asynchronously.
    this.redirectVerifierHelper_.useVerifier((verifier) => {
      if (verifier) {
        setInternalParam(paymentRequest, 'redirectVerifier', verifier);
      }
      if (options.forceRedirect) {
        const client = this.client_;
        this.eventManager_.getReadyPromise().then(() => {
          this.analytics_.getLoggingPromise().then(() => {
            client.loadPaymentData(paymentRequest);
            resolver(true);
          });
        });
      } else {
        this.client_.loadPaymentData(paymentRequest);
        resolver(true);
      }
    });
    return promise;
  }

  /**
   * @param {function(!Promise<!PaymentData>)} callback
   */
  onResponse(callback) {
    this.responseCallback_ = callback;
    const response = this.response_;
    if (response) {
      Promise.resolve().then(() => {
        if (response) {
          callback(this.convertResponse_(response, this.request_));
        }
      });
    }
  }

  /**
   * @param {!Promise<!PaymentData>} responsePromise
   * @private
   */
  handleResponse_(responsePromise) {
    this.response_ = responsePromise;
    if (this.responseCallback_) {
      this.responseCallback_(
        this.convertResponse_(this.response_, this.request_)
      );
    }
  }

  /**
   * @param {!Promise<!PaymentData>} response
   * @param {?PaymentDataRequest} request
   * @return {!Promise<!PaymentData>}
   * @private
   */
  convertResponse_(response, request) {
    return response
      .then(
        // Temporary client side solution to remember the
        // input params. TODO: Remove this once server-side
        // input preservation is done and is part of the response.
        (res) => {
          if (request) {
            res['paymentRequest'] = request;
          }
          return res;
        }
      )
      .catch((reason) => {
        if (typeof reason == 'object' && reason['statusCode'] == 'CANCELED') {
          const error = createCancelError(this.win_);
          if (request) {
            error['productType'] = /** @type {!PaymentDataRequest} */ (request)[
              'i'
            ]['productType'];
          } else {
            error['productType'] = null;
          }
          return Promise.reject(error);
        }
        return Promise.reject(reason);
      });
  }

  /**
   * @return {!Window}
   * @private
   */
  top_() {
    // Only exists for testing since it's not possible to override `window.top`.
    return this.win_.top;
  }
}

/**
 * @typedef {{
 *   key: string,
 *   verifier: string,
 * }}
 */
let RedirectVerifierPairDef;

/**
 * This helper generates key/verifier pair for the redirect mode. When the
 * redirect mode is used, the encrypted payload is returned via nivigation URL.
 * This payload need to be decrypted and to avoid session fixation attacks, a
 * verifier has to be used. This redirect verifier is not the only session
 * verifier in use: we also use GAIA. However, we have to fallback to this
 * verifier when GAIA is not available.
 *
 * @package Visible for testing only.
 */
export class RedirectVerifierHelper {
  /**
   * @param {!Window} win
   */
  constructor(win) {
    /** @private @const {!Window} */
    this.win_ = win;

    /** @private {boolean} */
    this.pairCreated_ = false;

    /** @private {?RedirectVerifierPairDef} */
    this.pair_ = null;

    /** @private {?Promise<?RedirectVerifierPairDef>} */
    this.pairPromise_ = null;
  }

  /**
   * To avoid popup blockers, the key/verifier pair is created as soon as
   * possible.
   * @return {?Promise}
   */
  prepare() {
    return this.getOrCreatePair_(() => {});
  }

  /**
   * Calls the provided callback with the generated redirect verifier. This
   * API is sync/async, which is a big anti-pattern. However, it's necessary
   * to reduce the risk of popup blockers. If the verifier is already available
   * (see `prepare` method), the callback will be called immediately and thus
   * in the same event loop as the user action.
   *
   * The return verifier could be `null`. This could mean either that its
   * generation failed, or if the platform doesn't support necessary APIs, such
   * as Web Crypto. The redirect can still proceed and try to fallback on GAIA
   * as a redirect verifier. The set of platforms where GAIA is not available
   * and the redirect verifier cannot be created is negligible.
   *
   * The key corresponding to the returned verifier is stored in the session
   * storage and can be later restored using `restoreKey` method.
   *
   * @param {function(?string)} callback
   */
  useVerifier(callback) {
    this.getOrCreatePair_((pair) => {
      if (pair) {
        try {
          this.win_.localStorage.setItem(REDIRECT_STORAGE_KEY, pair.key);
        } catch (e) {
          // If storage has failed, there's no point in using the verifer.
          // However, there are other ways to recover the redirect, so it's
          // not necessarily a fatal condition.
          pair = null;
        }
      }
      callback((pair && pair.verifier) || null);
    });
  }

  /**
   * Restores the redirect key from the session storage. The key may be null.
   * @return {?string}
   */
  restoreKey() {
    try {
      return (
        (this.win_.localStorage &&
          this.win_.localStorage.getItem(REDIRECT_STORAGE_KEY)) ||
        null
      );
    } catch (e) {
      return null;
    }
  }

  /**
   * @param {function(?RedirectVerifierPairDef)} callback
   * @return {?Promise}
   * @private
   */
  getOrCreatePair_(callback) {
    this.createPair_();
    if (this.pairCreated_) {
      // Already created.
      callback(this.pair_);
    } else if (this.pairPromise_) {
      // Otherwise wait for it to be created.
      this.pairPromise_.then((pair) => callback(pair));
    }
    return this.pairPromise_;
  }

  /**
   * @private
   */
  createPair_() {
    // Either already created or already started.
    if (this.pairCreated_ || this.pairPromise_) {
      return;
    }

    // Check that the platform can fully support verification. That means
    // that it's expected to implement the following APIs:
    // a. Local storage (localStorage);
    // b. WebCrypto (crypto.subtle);
    // c. Crypto random (crypto.getRandomValues);
    // d. SHA284 (crypto.subtle.digest).
    let supportsLocalStorage;
    try {
      supportsLocalStorage = !!this.win_.localStorage;
    } catch (e) {
      // Note: This can happen when cookies are disabled.
      supportsLocalStorage = false;
    }
    const crypto = this.win_.crypto;
    if (
      supportsLocalStorage &&
      crypto &&
      crypto.getRandomValues &&
      crypto.subtle &&
      crypto.subtle.digest
    ) {
      this.pairPromise_ = new Promise((resolve, reject) => {
        // 1. Use crypto random to create a 128-bit (16 byte) redirect key.
        const keyBytes = new Uint8Array(16);
        crypto.getRandomValues(keyBytes);

        // 2. Encode key as base64.
        const key = btoa(bytesToString(keyBytes));

        // 3. Create a hash.
        crypto.subtle.digest({name: 'SHA-384'}, stringToBytes(key)).then(
          (buffer) => {
            const verifier = btoa(
              bytesToString(
                new Uint8Array(/** @type {!ArrayBuffer} */ (buffer))
              )
            );
            resolve({key, verifier});
          },
          (reason) => {
            reject(reason);
          }
        );
      })
        .catch(() => {
          // Ignore failures. A failure to create a redirect verifier is often
          // recoverable.
          return null;
        })
        .then((pair) => {
          this.pairCreated_ = true;
          this.pair_ = pair;
          return pair;
        });
    } else {
      // Not supported.
      this.pairCreated_ = true;
      this.pair_ = null;
    }
  }
}

/**
 * @param {!PaymentDataRequest} paymentRequest
 * @param {string} param
 * @param {*} value
 */
function setInternalParam(paymentRequest, param, value) {
  paymentRequest['i'] = Object.assign(paymentRequest['i'] || {}, {
    [param]: value,
  });
}
<|endoftext|>


<commit_before>dojo.require("esri.map");
dojo.require("esri.layers.FeatureLayer");
dojo.require("esri.dijit.Geocoder");
dojo.require("dijit.layout.BorderContainer");
dojo.require("dijit.layout.ContentPane");
dojo.require("dijit.TooltipDialog");
var map, dialog, selSym, hcs = [{"chartType":"column", "renderTo": "rainfall",
	"labels": {"title": "Monthly rainfall"},
	"flds": ["rainJan","rainFeb","rainMar","rainApr","rainMay","rainJun","rainJul",
		"rainAug","rainSep","rainOct","rainNov","rainDec"],
	"yAxis": {"title": {"text": "Average rainfall (mm)"}},
	"ttip": {"sfx": " mm/month"}
},{"chartType":"line", "renderTo": "minTemp",
	"labels": {"title": "Minimum temperature"},
	"flds": ["minJan","minFeb","minMar","minApr","minMay","minJun","minJul",
		"minAug","minSep","minOct","minNov","minDec"],
	"yAxis": {"title": {"text": "Average min temperature (C)"}},
	"ttip": {"sfx": " deg Celsius"}
},{"chartType":"line", "renderTo": "maxTemp",
	"labels": {"title": "Maximum temperature"},
	"flds": ["maxJan","maxFeb","maxMar","maxApr","maxMay","maxJun","maxJul",
		"maxAug","maxSep","maxOct","maxNov","maxDec"],
	"yAxis": {"title": {"text": "Average max temperature (C)"}},
	"ttip": {"sfx": " deg Celsius"}}];
var base = $("#source a")[0].dataset.baseurl; //base URL is a dataset attribute
function init() {
	map = new esri.Map("map", {center: [133.9, -25.8],zoom: 5,basemap: "gray",
		maxZoom: 10});
	selSym = new esri.symbol.SimpleMarkerSymbol().setStyle(
		esri.symbol.SimpleMarkerSymbol.STYLE_CIRCLE).setColor(new dojo.Color([255,255,0,1]));
	var ws = new esri.layers.FeatureLayer(
		"http://54.252.134.128:6080/arcgis/rest/services/climate/WeatherStations/MapServer/0",{
	    mode: esri.layers.FeatureLayer.MODE_ONDEMAND,outFields: ["*"]});
	dojo.connect(map, "onLoad", function() {map.addLayer(ws);});
	dojo.connect(map, "onExtentChange", function() {dijit.popup.close();});
	dojo.connect(ws, "onClick", function(evt) {buildCharts(evt.graphic);});
	dojo.connect(ws, "onMouseOver", function(evt) {map.setMapCursor("pointer");
		dialog.setContent(evt.graphic.attributes["Site_name"]);
		dijit.popup.open({popup: dialog, x:evt.pageX,y:evt.pageY});});
	dojo.connect(ws, "onMouseOut", function(evt) {map.setMapCursor("default");});
	dojo.connect(ws,"onLoad", function() {
	    var query = new esri.tasks.Query(); //prime the charts with a random station
	    query.objectIds = [Math.floor(Math.random()*1417)]; //there are 1418 stations
	    query.outFields = ["*"];
	    ws.queryFeatures(query, function(featureSet) {buildCharts(featureSet.features[0]);});
	    dialog = new dijit.TooltipDialog({id: "tooltipDialog"});
	    dialog.startup();
	});
	var geocoder = new esri.dijit.Geocoder({ map: map,
		arcgisGeocoder: {placeholder: "Find a place", sourceCountry: "AUS"}}, "search");
    geocoder.startup();
}
function buildCharts(graphic) { //build charts from this feature's values
	var name = graphic.attributes["Site_name"];
	if(name.length > 27) {name = name.substring(name, 24) + "...";}
	$("#name").html(name);
	$("#source a").attr("href", base + graphic.attributes["Site"]);
	map.graphics.clear();
	var highlight = new esri.Graphic(graphic.geometry, selSym);
	map.graphics.add(highlight);
	for (var i = 0; i < hcs.length; i++) {
		hc = hcs[i];
	    var attr = graphic.attributes;
	    var series = new Array();
	    var flds = hc.flds;
	    var field = flds[0];
	    var data = new Array();
	    data[0] = attr[field];
	    var numFields = flds.length;
	    for (var x = 1; x < numFields; x++) {data[x] = attr[flds[x]];}
	    series[0] = new Object();
	    series[0].data = data;
	    var chart = new Highcharts.Chart({
	        chart: {renderTo: hc.renderTo,defaultSeriesType: hc.chartType},
	        legend: {enabled: false},title: {text: hc.labels.title},
	        xAxis: [{title: {text: "Month"},
	        	categories: ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
	        }],
	        yAxis: hc.yAxis,
	        tooltip: {formatter: function () {
	        	return this.point.category+": "+this.point.y+hc.ttip.sfx;}},
	        series: series
	    });}
}
function toggleWelcomeDialog() {$("#wd").is(':visible') ? $("#wd").hide() : $("#wd").show();}
dojo.addOnLoad(init);<commit_msg>minor tweaks<commit_after>dojo.require("esri.map");
dojo.require("esri.layers.FeatureLayer");
dojo.require("esri.dijit.Geocoder");
dojo.require("dijit.layout.BorderContainer");
dojo.require("dijit.layout.ContentPane");
dojo.require("dijit.TooltipDialog");
var map, dialog, selSym, hcs = [{"chartType":"column", "renderTo": "rainfall",
	"labels": {"title": "Average monthly rainfall"},
	"flds": ["rainJan","rainFeb","rainMar","rainApr","rainMay","rainJun","rainJul",
		"rainAug","rainSep","rainOct","rainNov","rainDec"],
	"yAxis": {"title": {"text": "millimetres"}},
	"ttip": {"sfx": " mm/month"}
},{"chartType":"line", "renderTo": "minTemp",
	"labels": {"title": "Average minimum temperature"},
	"flds": ["minJan","minFeb","minMar","minApr","minMay","minJun","minJul",
		"minAug","minSep","minOct","minNov","minDec"],
	"yAxis": {"title": {"text": "deg Celsius"}},
	"ttip": {"sfx": " deg Celsius"}
},{"chartType":"line", "renderTo": "maxTemp",
	"labels": {"title": "Average maximum temperature"},
	"flds": ["maxJan","maxFeb","maxMar","maxApr","maxMay","maxJun","maxJul",
		"maxAug","maxSep","maxOct","maxNov","maxDec"],
	"yAxis": {"title": {"text": "deg Celsius"}},
	"ttip": {"sfx": " deg Celsius"}}];
var base = $("#source a")[0].dataset.baseurl; //base URL is a dataset attribute
function init() {
	map = new esri.Map("map", {center: [133.9, -25.8],zoom: 5,basemap: "gray",
		maxZoom: 10});
	selSym = new esri.symbol.SimpleMarkerSymbol().setStyle(
		esri.symbol.SimpleMarkerSymbol.STYLE_CIRCLE).setColor(new dojo.Color([255,255,0,1]));
	var ws = new esri.layers.FeatureLayer(
		"http://54.252.134.128:6080/arcgis/rest/services/climate/WeatherStations/MapServer/0",{
	    mode: esri.layers.FeatureLayer.MODE_ONDEMAND,outFields: ["*"], opacity: 0.75});
	dojo.connect(map, "onLoad", function() {map.addLayer(ws);});
	dojo.connect(map, "onExtentChange", function() {dijit.popup.close();});
	dojo.connect(ws, "onClick", function(evt) {buildCharts(evt.graphic);});
	dojo.connect(ws, "onMouseOver", function(evt) {map.setMapCursor("pointer");
		dialog.setContent(evt.graphic.attributes["Site_name"]);
		dijit.popup.open({popup: dialog, x:evt.pageX,y:evt.pageY});});
	dojo.connect(ws, "onMouseOut", function(evt) {map.setMapCursor("default");});
	dojo.connect(ws,"onLoad", function() {
	    var query = new esri.tasks.Query(); //prime the charts with a random station
	    query.objectIds = [Math.floor(Math.random()*1417)]; //there are 1418 stations
	    query.outFields = ["*"];
	    ws.queryFeatures(query, function(featureSet) {buildCharts(featureSet.features[0]);});
	    dialog = new dijit.TooltipDialog({id: "tooltipDialog"});
	    dialog.startup();
	});
	var geocoder = new esri.dijit.Geocoder({ map: map,
		arcgisGeocoder: {placeholder: "Find a place", sourceCountry: "AUS"}}, "search");
    geocoder.startup();
}
function buildCharts(graphic) { //build charts from this feature's values
	var name = graphic.attributes["Site_name"];
	if(name.length > 25) {name = name.substring(name, 23) + "...";}
	$("#name").html(name);
	$("#source a").attr("href", base + graphic.attributes["Site"]);
	map.graphics.clear();
	var highlight = new esri.Graphic(graphic.geometry, selSym);
	map.graphics.add(highlight);
	for (var i = 0; i < hcs.length; i++) {
		hc = hcs[i];
	    var attr = graphic.attributes;
	    var series = new Array();
	    var flds = hc.flds;
	    var field = flds[0];
	    var data = new Array();
	    data[0] = attr[field];
	    var numFields = flds.length;
	    for (var x = 1; x < numFields; x++) {data[x] = attr[flds[x]];}
	    series[0] = new Object();
	    series[0].data = data;
	    var chart = new Highcharts.Chart({
	        chart: {renderTo: hc.renderTo,defaultSeriesType: hc.chartType,
	        	marginBottom: 50}, legend: {enabled: false},title: {text: hc.labels.title},
	        xAxis: [{
	        	categories: ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
	        }],credits: {enabled: false},
	        yAxis: hc.yAxis,
	        tooltip: {formatter: function () {
	        	return this.point.category+": "+this.point.y+hc.ttip.sfx;}},
	        series: series
	    });}
}
function toggleWelcomeDialog() {$("#wd").is(':visible') ? $("#wd").hide() : $("#wd").show();}
dojo.addOnLoad(init);<|endoftext|>


<commit_before>/**
 * @const
 */
var ALPHANUM = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';

/**
 * Hexadecimal digits.
 * @const
 */
var HEX_DIGITS = '0123456789abcdef';

/**
 * Digits.
 * @const
 */
var DIGITS = '0123456789';

/**
 * Generates random int within the range [min, max]
 * @param min the minimum value for the generated number
 * @param max the maximum value for the generated number
 * @returns random int number
 */
function randomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

/**
 * Get random element from array or string.
 * @param {Array|string} arr source
 * @returns array element or string character
 */
function randomElement(arr) {
    return arr[randomInt(0, arr.length -1)];
}

/**
 * Generate random alphanumeric string.
 * @param {number} length expected string length
 * @returns {string} random string of specified length
 */
function randomAlphanumStr(length) {
    var result = '';

    for (var i = 0; i < length; i += 1) {
        result += randomElement(ALPHANUM);
    }

    return result;
}

/**
 * Returns a random hex digit.
 * @returns {*}
 */
function randomHexDigit() {
    return randomElement(HEX_DIGITS);
}

/**
 * Returns a random string of hex digits with length 'len'.
 * @param len the length.
 */
function randomHexString(len) {
    var ret = '';
    while (len--) {
        ret += this.randomHexDigit();
    }
    return ret;
}

/**
 * Returns a random string of digits with length 'len'.
 * @param len the length.
 */
function randomDigitString(len) {
    var ret = '';
    while (len--) {
        ret += this.randomElement(DIGITS);
    }
    return ret;
}
<commit_msg>Adds number validation to the random number generation.<commit_after>/**
 * @const
 */
var ALPHANUM = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';

/**
 * Hexadecimal digits.
 * @const
 */
var HEX_DIGITS = '0123456789abcdef';

/**
 * Digits.
 * @const
 */
var DIGITS = '0123456789';

/**
 * Generates random int within the range [min, max]
 * @param min the minimum value for the generated number
 * @param max the maximum value for the generated number
 * @returns random int number
 */
function randomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

/**
 * Get random element from array or string.
 * @param {Array|string} arr source
 * @returns array element or string character
 */
function randomElement(arr) {
    return arr[randomInt(0, arr.length -1)];
}

/**
 * Generate random alphanumeric string.
 * @param {number} length expected string length
 * @returns {string} random string of specified length
 */
function randomAlphanumStr(length) {
    var result = '';

    for (var i = 0; i < length; i += 1) {
        result += randomElement(ALPHANUM);
    }

    return result;
}

/**
 * Returns a random hex digit.
 * @returns {*}
 */
function randomHexDigit() {
    return randomElement(HEX_DIGITS);
}

/**
 * Returns a random string of hex digits with length 'len'.
 * @param len the length.
 */
function randomHexString(len) {
    var ret = '';
    while (len--) {
        ret += this.randomHexDigit();
    }
    return ret;
}

/**
 * Returns a random string of digits with length 'len'.
 * The string will be with random numbers of count 'len' - 2, and the last
 * two digits will be a check sum using the "ISO 7064 Mod 97,10" algorithm.
 * In order to verify it the formula is:
 * (num_to_check % 97) == 1
 * @param len the length.
 */
function randomDigitString(len) {
    var ret = '';
    var randomLen = len - 2;
    while (randomLen--) {
        ret += this.randomElement(DIGITS);
    }
    var num = parseInt(ret);
    var verifyNumber = (98 - (num * 100) % 97) % 97;
    return num.toString()
        + (verifyNumber < 10 ? '0' : '') // adds leading zero if single digit
        + verifyNumber.toString();
}
<|endoftext|>


<commit_before>var map, dialog, selSym, hc, charts = [{"source": "pt", "chartType":"column", "renderTo": "rainfall",
	"labels": {"title": "Average monthly rainfall"}, "fields": [{"flds": ["rainJan","rainFeb","rainMar",
	"rainApr","rainMay","rainJun","rainJul","rainAug","rainSep","rainOct","rainNov","rainDec"]}],
	"yAxis": {"title": {"text": "millimetres"}}, "xAxis": [{"categories": ["Jan","Feb","Mar","Apr",
	"May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]}],"ttip": {"sfx": " mm/month"}},{"source": "pt",
	"chartType":"line", "renderTo": "tempRange","labels": {"title": "Average temperature range"},
	"fields": [{"flds": ["minJan","minFeb","minMar","minApr","minMay","minJun","minJul","minAug",
	"minSep","minOct","minNov","minDec"]},{"flds": ["maxJan","maxFeb","maxMar","maxApr","maxMay",
	"maxJun","maxJul","maxAug","maxSep","maxOct","maxNov","maxDec"]}],"yAxis": {"title":
	{"text": "deg Celsius"}},"xAxis": [{"categories": ["Jan","Feb","Mar","Apr","May","Jun","Jul",
	"Aug","Sep","Oct","Nov","Dec"]}],"ttip": {"sfx": " deg C"}},{"source": "query","chartType":"line",
	"renderTo": "maxTemp","ttip": {"sfx": " deg C"},"labels": {"title": "Average maximum temperature"},
	"yAxis": {"title": {"text": "deg C"}}}];
var base = "http://www.bom.gov.au/jsp/ncc/cdio/weatherData/av?p_nccObsCode=136&p_display_type=dailyDataFile&p_startYear=&p_c=&p_stn_num=";
require(["dojo/ready", "esri/map", "esri/tasks/query", "esri/dijit/Geocoder", "esri/layers/FeatureLayer",
	"dijit/layout/BorderContainer", "dijit/layout/ContentPane", "dijit/TooltipDialog","dijit/Popup"], function() {
	map = new esri.Map("map", {center: [133.9, -25.8],zoom: 5,basemap: "gray",maxZoom: 10});
	selSym = new esri.symbol.SimpleMarkerSymbol().setStyle(
		esri.symbol.SimpleMarkerSymbol.STYLE_CIRCLE).setColor(new dojo.Color([255,255,0,1]));
	var ws = new esri.layers.FeatureLayer(
		"http://services.azuron.com/arcgis/rest/services/weather/WeatherStations/MapServer/0",{
	    mode: esri.layers.FeatureLayer.MODE_ONDEMAND,outFields: ["*"], opacity: 0.75});
	dojo.connect(map,"onLoad",function(){map.addLayer(ws);});
	dojo.connect(ws,"onClick",function(evt){buildCharts(evt.graphic);});
	dojo.connect(ws,"onMouseOver",function(evt){map.setMapCursor("pointer");
		dialog.setContent(evt.graphic.attributes["Site_name"]);
		dijit.popup.open({popup:dialog,x:evt.pageX,y:evt.pageY});});
	dojo.connect(ws,"onMouseOut",function(evt){map.setMapCursor("default");dijit.popup.close();});
	dojo.connect(ws,"onLoad",function(){
	    var query = new esri.tasks.Query(); //prime the charts with a random station
	    query.objectIds = [Math.floor(Math.random()*1417)]; //there are 1418 stations
	    query.outFields = ["*"];
	    ws.queryFeatures(query,function(featureSet){buildCharts(featureSet.features[0]);});
	    dialog = new dijit.TooltipDialog({id:"tooltipDialog"});
	    dialog.startup();
	});
	var geocoder = new esri.dijit.Geocoder({map:map,autoComplete:true,
		arcgisGeocoder:{placeholder:"Find a place",sourceCountry:"AUS"}},"search");
    geocoder.startup();
});
function buildCharts(graphic) { //build charts from this station's values
	var name = graphic.attributes["Site_name"];
	if(name.length>25){name=name.substring(name,23) + "...";}
	$("#name").html(name);
	$("#source a").attr("href",base + graphic.attributes["Site"]);
	map.graphics.clear();
	var highlight = new esri.Graphic(graphic.geometry, selSym);
	map.graphics.add(highlight);
	$(charts).each(function(idx, hc){
		hc.series = new Array();
		if (hc.source == "pt") {
			var attr = graphic.attributes;
			for (var y = 0; y < hc.fields.length; y++) {
				var flds = hc.fields[y].flds;
			    var field = flds[0];
			    var data = new Array();
			    data[0] = attr[field];
			    for (var x = 1; x < flds.length; x++) {data[x] = attr[flds[x]];}
			    hc.series[y] = new Object();
			    hc.series[y].data = data;
			}
			drawChart(hc);
		} else {
			var qt = new esri.tasks.QueryTask("http://services.azuron.com/arcgis/rest/services/weather/WeatherStations/MapServer/1");
			var query = new esri.tasks.Query();
			query.outFields = ["*"];
			query.where = "Site = " + graphic.attributes["Site"];
			qt.execute(query, function(results) {
				var data = [], cats = [];
				for (var j = 0; j < results.features.length; j++) {
					data.push(results.features[j].attributes.maxAvg);
					cats.push(results.features[j].attributes.year.toString());
				}
				hc.xAxis = {"categories": cats, labels: {step: Math.round(cats.length/5)}};
				hc.series[0] = new Object();
				hc.series[0].data = data;
				drawChart(hc);
			});
		}
	});
}
function drawChart(hc) {
	var chart = new Highcharts.Chart({
	    chart: {renderTo: hc.renderTo,defaultSeriesType: hc.chartType,
	    	marginBottom: 50}, legend: {enabled: false},title: {text: hc.labels.title},
	    xAxis:hc.xAxis,credits: {enabled: false}, yAxis: hc.yAxis, colors:['#4572A7','#AA4643'],
	    tooltip: {formatter: function () {return this.point.category+": "+this.point.y+hc.ttip.sfx;}},
	    series: hc.series});}
function toggleWelcomeDialog() {$("#wd").is(':visible') ? $("#wd").hide() : $("#wd").show();}
$(document).on("click", "#information", function() {toggleWelcomeDialog()});<commit_msg>removed quotation marks and spaces to save chars<commit_after>var map,dialog,selSym,hc,qt,tempQuery,charts=[{source:"pt",chartType:"column",renderTo:"rainfall",
	labels:{title:"Average monthly rainfall"},plotOptions:{series:{pointWidth:25}},fields:
	[{flds:["rainJan","rainFeb","rainMar","rainApr","rainMay","rainJun","rainJul","rainAug",
	"rainSep","rainOct","rainNov","rainDec"]}],yAxis:{title:{text:"millimetres"}},xAxis:
	[{categories:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]}],
	ttip:{sfx:" mm/month"}},{plotOptions:{marker:{enabled:false}},source:"pt",chartType:"line",
	renderTo:"tempRange",labels:{title:"Average temperature range"},fields:[{flds:
	["minJan","minFeb","minMar","minApr","minMay","minJun","minJul","minAug","minSep","minOct",
	"minNov","minDec"]},{flds:["maxJan","maxFeb","maxMar","maxApr","maxMay","maxJun","maxJul",
	"maxAug","maxSep","maxOct","maxNov","maxDec"]}],yAxis:{title:{text:"deg Celsius"}},
	xAxis:[{categories:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]}],
	ttip:{sfx:" deg C"}},{source:"query",chartType:"spline",renderTo:"maxTemp",ttip:
	{sfx:" deg C"},labels:{title:"Average maximum temperature"},plotOptions:{spline:
	{marker:{enabled:false,states:{hover:{enabled:true,symbol:'circle',radius:5}}}}},yAxis:{title:{text:"deg C"}}}];
var base = "http://www.bom.gov.au/jsp/ncc/cdio/weatherData/av?p_nccObsCode=136&p_display_type=dailyDataFile&p_startYear=&p_c=&p_stn_num=";
require(["dojo/ready","esri/map","esri/tasks/query","esri/dijit/Geocoder","esri/layers/FeatureLayer",
	"dijit/layout/BorderContainer","dijit/layout/ContentPane","dijit/TooltipDialog","dijit/Popup"],function() {
	map = new esri.Map("map",{center: [133.9,-25.8],zoom: 5,basemap: "gray",maxZoom: 10});
	selSym = new esri.symbol.SimpleMarkerSymbol().setStyle(
		esri.symbol.SimpleMarkerSymbol.STYLE_CIRCLE).setColor(new dojo.Color([255,255,0,1]));
	var ws = new esri.layers.FeatureLayer(
		"http://services.azuron.com/arcgis/rest/services/weather/WeatherStations/MapServer/0",{
	    mode: esri.layers.FeatureLayer.MODE_ONDEMAND,outFields: ["*"],opacity: 0.75});
	dojo.connect(map,"onLoad",function(){map.addLayer(ws);});
	dojo.connect(ws,"onClick",function(evt){buildCharts(evt.graphic);});
	dojo.connect(ws,"onMouseOver",function(evt){map.setMapCursor("pointer");
		dialog.setContent(evt.graphic.attributes["Site_name"]);
		dijit.popup.open({popup:dialog,x:evt.pageX,y:evt.pageY});});
	dojo.connect(ws,"onMouseOut",function(evt){map.setMapCursor("default");dijit.popup.close();});
	dojo.connect(ws,"onLoad",function(){
	    var query = new esri.tasks.Query(); //prime the charts with a random station
	    query.objectIds = [Math.floor(Math.random()*1417)]; //there are 1418 stations
	    query.outFields = ["*"];
	    ws.queryFeatures(query,function(featureSet){buildCharts(featureSet.features[0]);});
	    dialog = new dijit.TooltipDialog({id:"tooltipDialog"});
	    dialog.startup();
	});
	var geocoder = new esri.dijit.Geocoder({map:map,autoComplete:true,
		arcgisGeocoder:{placeholder:"Find a place",sourceCountry:"AUS"}},"search");
    geocoder.startup();
    qt = new esri.tasks.QueryTask("http://services.azuron.com/arcgis/rest/services/weather/WeatherStations/MapServer/1");
    tempQuery = new esri.tasks.Query();
    tempQuery.outFields = ["*"];
});
function buildCharts(graphic) { //build charts from this station's values
	var name = graphic.attributes["Site_name"];
	if(name.length>25){name=name.substring(name,23) + "...";}
	$("#name").html(name);
	$("#source a").attr("href",base + graphic.attributes["Site"]);
	map.graphics.clear();
	var highlight = new esri.Graphic(graphic.geometry,selSym);
	map.graphics.add(highlight);
	$(charts).each(function(idx,hc){
		hc.series = new Array();
		hc.url = base + graphic.attributes["Site"];
		if (hc.source == "pt") {
			var attr = graphic.attributes;
			for (var y = 0; y < hc.fields.length; y++) {
				var flds = hc.fields[y].flds;
			    var field = flds[0];
			    var data = new Array();
			    data[0] = attr[field];
			    for (var x = 1; x < flds.length; x++) {data[x] = attr[flds[x]];}
			    hc.series[y] = new Object();
			    hc.series[y].data = data;
			}
			drawChart(hc);
		} else {
			tempQuery.where = "Site = " + graphic.attributes["Site"];
			qt.execute(tempQuery,function(results) {
				var data = [],cats = [];
				for (var j = 0; j < results.features.length; j++) {
					data.push(results.features[j].attributes.maxAvg);
					cats.push(results.features[j].attributes.year.toString());
				}
				hc.xAxis = {"categories": cats,labels: {step: Math.round(cats.length/5)}};
				hc.series[0] = new Object();
				hc.series[0].data = data;
				drawChart(hc);
			});
		}
	});
}
function drawChart(hc) {
	var chart = new Highcharts.Chart({
		credits: {
		            text: 'Bureau of Meteorology',
		            href: hc.url
        },
	    chart: {renderTo: hc.renderTo,defaultSeriesType: hc.chartType,
	    	marginBottom: 50},legend: {enabled: false},title: {text: hc.labels.title},
	    xAxis:hc.xAxis,yAxis: hc.yAxis,colors:['#4572A7','#AA4643'],
	    tooltip: {formatter: function () {return this.point.category+": "+this.point.y+hc.ttip.sfx;}},
	    plotOptions: hc.plotOptions,series: hc.series});}
function toggleWelcomeDialog() {$("#wd").is(':visible') ? $("#wd").hide() : $("#wd").show();}
$(document).on("click","#information",function() {toggleWelcomeDialog()});<|endoftext|>


<commit_before>/*    Copyright 2022 Firewalla LLC
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

const log = require('../net2/logger.js')(__filename);
const PolicyManager2 = require('../alarm/PolicyManager2');
const pm2 = new PolicyManager2();
const DomainIPTool = require('../control/DomainIPTool');
const domainIpTool = new DomainIPTool();
const Sensor = require('./Sensor.js').Sensor;

const sem = require('../sensor/SensorEventManager').getInstance();
const _ = require('lodash');
const scheduler = require('../util/scheduler');
const { Address4, Address6 } = require('ip-address');
const rclient = require('../util/redis_manager.js').getRedisClient();
const qos = require('../control/QoS');
const LRU = require('lru-cache');
const crypto = require('crypto');
const featureName = "rule_stats";

const KEY_RULE_STATS_INIT_TS = "sys:ruleStats:initTs"
class RuleStatsPlugin extends Sensor {
  run() {
    this.hookFeature(featureName);
    this.policyRulesMap = null;
    this.recordBuffer = [];
    this.cache = new LRU({
      max: 200,
      maxAge: 15 * 1000,
      updateAgeOnGet: false
    });
    sem.on("PolicyEnforcement", async (event) => {
      await this.loadBlockAllowGlobalRules();
    });
    this.on = false;
    void this.process();
  }

  async initFeatureFirstEnabledTimestamp() {
    const currentTs = new Date().getTime() / 1000;
    await rclient.setnxAsync(KEY_RULE_STATS_INIT_TS, currentTs);
  }

  async getFeatureFirstEnabledTimestamp() {
    const initTs = await rclient.getAsync(KEY_RULE_STATS_INIT_TS);
    if (initTs) {
      return parseFloat(initTs);
    } else {
      return 0;
    }
  }

  async globalOn() {
    await super.globalOn();
    await this.initFeatureFirstEnabledTimestamp()
    this.on = true;
  }

  async globalOff() {
    await super.globalOff();
    this.on = false;
    this.cache.reset();
  }

  async process() {
    while (true) {
      try {
        if (this.on) {
          await this.updateRuleStats();
        }
      } catch (e) {
        log.debug(e);
      }
      await scheduler.delay(1000);
    }
  }

  // Load global allow/block policy rules
  async loadBlockAllowGlobalRules() {
    if (!this.on) {
      return;
    }
    log.debug("Load policy rules for stats");
    const newPolicyRulesMap = new Map();
    const policyRules = await pm2.loadActivePoliciesAsync();
    log.debug(`${policyRules.length} rules active`);
    for (const policy of policyRules) {
      const action = policy.action;
      if (!newPolicyRulesMap.has(action)) {
        newPolicyRulesMap.set(action, []);
      }
      // filter on rules
      switch (action) {
        case "allow":
        case "block": {
          if (policy.disabled === "1") {
            continue;
          }
          // skip non global rules because they will already have their rule id logged.
          if (!_.isEmpty(policy.tags) || !_.isEmpty(policy.intfs) || !_.isEmpty(policy.scope) || !_.isEmpty(policy.guids) || policy.localPort || policy.remotePort) {
            continue;
          }

          if (!["dns", "domain", "ip", "net"].includes(policy.type)) {
            continue;
          }

          log.debug(`Add policy rule ${policy.pid} to global ${action} list`);
          newPolicyRulesMap.get(action).push(policy);
        }
      }
    }
    this.policyRulesMap = newPolicyRulesMap;
  }

  accountRule(record) {
    if (!this.on) {
      return;
    }
    // ignore
    if (record.dir === "W") {
      return;
    }

    // ignore normal dnsmasq pass rule
    if (record.type === "dns" && record.ac !== "block" && record.pid === 0 && !record.global) {
      return;
    }

    // limit buf size to avoid cpu and memory overload
    if (this.recordBuffer.length < 2000) {
      this.recordBuffer.push(record);
    }
  }

  async updateRuleStats() {
    const recordBuffer = this.recordBuffer;
    this.recordBuffer = [];

    if (!this.policyRulesMap) {
      await this.loadBlockAllowGlobalRules();
    }

    const ruleStatMap = new Map();

    // Match record to policy id
    for (const record of recordBuffer) {
      log.debug("Determine policy rule id of record:", record);
      let matchedPids;
      if (record.pid) {
        matchedPids = [record.pid];
      } else {
        // use cache to reduce computation and redis operation.
        const hash = crypto.createHash("md5");
        hash.update(String(record.ac));
        hash.update(String(record.type));
        hash.update(String(record.fd));
        hash.update(String(record.sec));
        hash.update(String(record.dn));
        hash.update(String(record.dh));
        hash.update(String(record.qmark));
        const key = hash.digest("hex");
        const v = this.cache.get(key);
        if (v) {
          log.debug("Hit rule stat cache");
          matchedPids = v;
        } else {
          matchedPids = await this.getPolicyIds(record);
          this.cache.set(key, matchedPids);
        }
      }

      for (const pid of matchedPids) {
        log.debug("Matched policy rule: ", pid);
        let stat;
        if (ruleStatMap.has(pid)) {
          stat = ruleStatMap.get(pid);
        } else {
          stat = new RuleStat();
        }
        stat.count++;
        if (record.ts > stat.lastHitTs) {
          stat.lastHitTs = record.ts;
        }
        ruleStatMap.set(pid, stat);
      }
    }

    // update rule status to redis
    for (const [pid, stat] of ruleStatMap) {
      if (! await rclient.existsAsync(`policy:${pid}`)) {
        return;
      }
      const multi = rclient.multi();
      multi.hincrby(`policy:${pid}`, "hitCount", stat.count);
      const lastHitTs = Number(await rclient.hgetAsync(`policy:${pid}`, "lastHitTs") || "0");
      if (stat.lastHitTs > lastHitTs) {
        multi.hset(`policy:${pid}`, "lastHitTs", String(stat.lastHitTs));
      }
      await multi.execAsync();
    }
  }

  async getPolicyIds(record) {
    switch (record.ac) {
      case "allow":
      case "block": {
        log.debug("Match policy id for allow/block record", record);
        let recordIp, recordDomain;
        const action = record.ac;
        let lookupSets = [];

        if (record.sec) {
          lookupSets.push("sec_block_domain_set");
        } else {
          lookupSets.push(`${action}_domain_set`);
          if (record.fd === "in") {
            lookupSets.push(`${action}_ob_domain_set`);
          }
          if (record.fd === "out") {
            lookupSets.push(`${action}_ib_domain_set`);
          }
        }

        if (record.type === "dns") {
          recordDomain = record.dn;
        } else {
          recordIp = record.dh;
        }

        if (!this.policyRulesMap.has(action)) {
          return [];
        }

        for (const policy of this.policyRulesMap.get(action)) {
          if (record.sec && !policy.isSecurityBlockPolicy()) {
            continue;
          }
          if (!record.sec && policy.isSecurityBlockPolicy()) {
            continue;
          }

          const needToMatchDomainIpset = (action === "allow" || !policy.dnsmasq_only);

          const target = policy.target;

          // domain match
          if (recordDomain && this.matchWildcardDomain(recordDomain, target)) {
            return [policy.pid];
          }

          if (recordIp) {
            // exact ip match
            if (recordIp === target) {
              return [policy.pid];
            }
            // ip subnet match
            const addr4 = new Address4(recordIp);
            const targetNet4 = new Address4(target);
            if (addr4.isValid() && targetNet4.isValid() && addr4.isInSubnet(targetNet4)) {
              return [policy.pid];
            }
            const addr6 = new Address6(recordIp);
            const targetNet6 = new Address6(target);
            if (addr6.isValid() && targetNet6.isValid() && addr6.isInSubnet(targetNet6)) {
              return [policy.pid];
            }
          }

          // domain ipset match
          if (needToMatchDomainIpset) {
            for (const lookupSet of lookupSets) {
              log.debug(`Match ${recordIp} to domain ipset ${lookupSet}`);
              const ips = new Set(await domainIpTool.getMappedIPAddresses(target, { blockSet: lookupSet }));
              if (recordIp && ips.has(recordIp)) {
                return [policy.pid];
              }
            }
          }

        }
        return [];

      }
      case "qos": {
        const downloadHandlerId = (record.qmark & qos.QOS_DOWNLOAD_MASK) >> 16;
        const uploadHandlerId = (record.qmark & qos.QOS_UPLOAD_MASK) >> 23;
        const downloadPolicyId = await qos.getPolicyForQosHandler(downloadHandlerId);
        const uploadPolicyId = await qos.getPolicyForQosHandler(uploadHandlerId);
        const result = [];
        if (downloadPolicyId) {
          result.push(downloadPolicyId);
        }
        if (uploadPolicyId) {
          result.push(uploadPolicyId);
        }
        return result;
      }
    }
  }

  matchWildcardDomain(domain, target) {
    if (domain === target) {
      return true;
    }
    const tokens = domain.split(".");
    for (let i = 1; i < tokens.length - 1; i++) {
      if (tokens.slice(i).join(".") === target) {
        return true;
      }
    }
    return false;
  }
}

class RuleStat {
  constructor() {
    this.count = 0;
    this.lastHitTs = 0;
  }
}

module.exports = RuleStatsPlugin;<commit_msg>reset all rule stats on first run<commit_after>/*    Copyright 2022 Firewalla LLC
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

const log = require('../net2/logger.js')(__filename);
const PolicyManager2 = require('../alarm/PolicyManager2');
const pm2 = new PolicyManager2();
const DomainIPTool = require('../control/DomainIPTool');
const domainIpTool = new DomainIPTool();
const Sensor = require('./Sensor.js').Sensor;

const sem = require('../sensor/SensorEventManager').getInstance();
const _ = require('lodash');
const scheduler = require('../util/scheduler');
const { Address4, Address6 } = require('ip-address');
const rclient = require('../util/redis_manager.js').getRedisClient();
const qos = require('../control/QoS');
const LRU = require('lru-cache');
const crypto = require('crypto');
const featureName = "rule_stats";

const KEY_RULE_STATS_INIT_TS = "sys:ruleStats:initTs"
class RuleStatsPlugin extends Sensor {
  run() {
    this.hookFeature(featureName);
    this.policyRulesMap = null;
    this.recordBuffer = [];
    this.cache = new LRU({
      max: 200,
      maxAge: 15 * 1000,
      updateAgeOnGet: false
    });
    sem.on("PolicyEnforcement", async (event) => {
      await this.loadBlockAllowGlobalRules();
    });
    this.on = false;
    void this.process();
  }

  async initRuleStatsFirstTimeOnBox() {
    const result = await rclient.existsAsync(KEY_RULE_STATS_INIT_TS);
    if (result === 0) {
      // this code will only run once on each box to reset rule stats.
      log.info("Clear all hit count data when this feature is first enabled");
      const policies = await pm2.loadActivePoliciesAsync({ includingDisabled: true });
      for (const policy of policies) {
        pm2.resetStats(policy.pid);
      }
      const currentTs = new Date().getTime() / 1000;
      // a flag to indicate that the box has inited rule stats
      await rclient.setAsync(KEY_RULE_STATS_INIT_TS, currentTs);
    }
  }

  async getFeatureFirstEnabledTimestamp() {
    const initTs = await rclient.getAsync(KEY_RULE_STATS_INIT_TS);
    if (initTs) {
      return parseFloat(initTs);
    } else {
      return 0;
    }
  }

  async globalOn() {
    await super.globalOn();
    await this.initRuleStatsFirstTimeOnBox()
    this.on = true;
  }

  async globalOff() {
    await super.globalOff();
    this.on = false;
    this.cache.reset();
  }

  async process() {
    while (true) {
      try {
        if (this.on) {
          await this.updateRuleStats();
        }
      } catch (e) {
        log.debug(e);
      }
      await scheduler.delay(1000);
    }
  }

  // Load global allow/block policy rules
  async loadBlockAllowGlobalRules() {
    if (!this.on) {
      return;
    }
    log.debug("Load policy rules for stats");
    const newPolicyRulesMap = new Map();
    const policyRules = await pm2.loadActivePoliciesAsync();
    log.debug(`${policyRules.length} rules active`);
    for (const policy of policyRules) {
      const action = policy.action;
      if (!newPolicyRulesMap.has(action)) {
        newPolicyRulesMap.set(action, []);
      }
      // filter on rules
      switch (action) {
        case "allow":
        case "block": {
          if (policy.disabled === "1") {
            continue;
          }
          // skip non global rules because they will already have their rule id logged.
          if (!_.isEmpty(policy.tags) || !_.isEmpty(policy.intfs) || !_.isEmpty(policy.scope) || !_.isEmpty(policy.guids) || policy.localPort || policy.remotePort) {
            continue;
          }

          if (!["dns", "domain", "ip", "net"].includes(policy.type)) {
            continue;
          }

          log.debug(`Add policy rule ${policy.pid} to global ${action} list`);
          newPolicyRulesMap.get(action).push(policy);
        }
      }
    }
    this.policyRulesMap = newPolicyRulesMap;
  }

  accountRule(record) {
    if (!this.on) {
      return;
    }
    // ignore
    if (record.dir === "W") {
      return;
    }

    // ignore normal dnsmasq pass rule
    if (record.type === "dns" && record.ac !== "block" && record.pid === 0 && !record.global) {
      return;
    }

    // limit buf size to avoid cpu and memory overload
    if (this.recordBuffer.length < 2000) {
      this.recordBuffer.push(record);
    }
  }

  async updateRuleStats() {
    const recordBuffer = this.recordBuffer;
    this.recordBuffer = [];

    if (!this.policyRulesMap) {
      await this.loadBlockAllowGlobalRules();
    }

    const ruleStatMap = new Map();

    // Match record to policy id
    for (const record of recordBuffer) {
      log.debug("Determine policy rule id of record:", record);
      let matchedPids;
      if (record.pid) {
        matchedPids = [record.pid];
      } else {
        // use cache to reduce computation and redis operation.
        const hash = crypto.createHash("md5");
        hash.update(String(record.ac));
        hash.update(String(record.type));
        hash.update(String(record.fd));
        hash.update(String(record.sec));
        hash.update(String(record.dn));
        hash.update(String(record.dh));
        hash.update(String(record.qmark));
        const key = hash.digest("hex");
        const v = this.cache.get(key);
        if (v) {
          log.debug("Hit rule stat cache");
          matchedPids = v;
        } else {
          matchedPids = await this.getPolicyIds(record);
          this.cache.set(key, matchedPids);
        }
      }

      for (const pid of matchedPids) {
        log.debug("Matched policy rule: ", pid);
        let stat;
        if (ruleStatMap.has(pid)) {
          stat = ruleStatMap.get(pid);
        } else {
          stat = new RuleStat();
        }
        stat.count++;
        if (record.ts > stat.lastHitTs) {
          stat.lastHitTs = record.ts;
        }
        ruleStatMap.set(pid, stat);
      }
    }

    // update rule status to redis
    for (const [pid, stat] of ruleStatMap) {
      if (! await rclient.existsAsync(`policy:${pid}`)) {
        return;
      }
      const multi = rclient.multi();
      multi.hincrby(`policy:${pid}`, "hitCount", stat.count);
      const lastHitTs = Number(await rclient.hgetAsync(`policy:${pid}`, "lastHitTs") || "0");
      if (stat.lastHitTs > lastHitTs) {
        multi.hset(`policy:${pid}`, "lastHitTs", String(stat.lastHitTs));
      }
      await multi.execAsync();
    }
  }

  async getPolicyIds(record) {
    switch (record.ac) {
      case "allow":
      case "block": {
        log.debug("Match policy id for allow/block record", record);
        let recordIp, recordDomain;
        const action = record.ac;
        let lookupSets = [];

        if (record.sec) {
          lookupSets.push("sec_block_domain_set");
        } else {
          lookupSets.push(`${action}_domain_set`);
          if (record.fd === "in") {
            lookupSets.push(`${action}_ob_domain_set`);
          }
          if (record.fd === "out") {
            lookupSets.push(`${action}_ib_domain_set`);
          }
        }

        if (record.type === "dns") {
          recordDomain = record.dn;
        } else {
          recordIp = record.dh;
        }

        if (!this.policyRulesMap.has(action)) {
          return [];
        }

        for (const policy of this.policyRulesMap.get(action)) {
          if (record.sec && !policy.isSecurityBlockPolicy()) {
            continue;
          }
          if (!record.sec && policy.isSecurityBlockPolicy()) {
            continue;
          }

          const needToMatchDomainIpset = (action === "allow" || !policy.dnsmasq_only);

          const target = policy.target;

          // domain match
          if (recordDomain && this.matchWildcardDomain(recordDomain, target)) {
            return [policy.pid];
          }

          if (recordIp) {
            // exact ip match
            if (recordIp === target) {
              return [policy.pid];
            }
            // ip subnet match
            const addr4 = new Address4(recordIp);
            const targetNet4 = new Address4(target);
            if (addr4.isValid() && targetNet4.isValid() && addr4.isInSubnet(targetNet4)) {
              return [policy.pid];
            }
            const addr6 = new Address6(recordIp);
            const targetNet6 = new Address6(target);
            if (addr6.isValid() && targetNet6.isValid() && addr6.isInSubnet(targetNet6)) {
              return [policy.pid];
            }
          }

          // domain ipset match
          if (needToMatchDomainIpset) {
            for (const lookupSet of lookupSets) {
              log.debug(`Match ${recordIp} to domain ipset ${lookupSet}`);
              const ips = new Set(await domainIpTool.getMappedIPAddresses(target, { blockSet: lookupSet }));
              if (recordIp && ips.has(recordIp)) {
                return [policy.pid];
              }
            }
          }

        }
        return [];

      }
      case "qos": {
        const downloadHandlerId = (record.qmark & qos.QOS_DOWNLOAD_MASK) >> 16;
        const uploadHandlerId = (record.qmark & qos.QOS_UPLOAD_MASK) >> 23;
        const downloadPolicyId = await qos.getPolicyForQosHandler(downloadHandlerId);
        const uploadPolicyId = await qos.getPolicyForQosHandler(uploadHandlerId);
        const result = [];
        if (downloadPolicyId) {
          result.push(downloadPolicyId);
        }
        if (uploadPolicyId) {
          result.push(uploadPolicyId);
        }
        return result;
      }
    }
  }

  matchWildcardDomain(domain, target) {
    if (domain === target) {
      return true;
    }
    const tokens = domain.split(".");
    for (let i = 1; i < tokens.length - 1; i++) {
      if (tokens.slice(i).join(".") === target) {
        return true;
      }
    }
    return false;
  }
}

class RuleStat {
  constructor() {
    this.count = 0;
    this.lastHitTs = 0;
  }
}

module.exports = RuleStatsPlugin;<|endoftext|>


<commit_before>'use strict'

const _ = require('lodash')

let options = {name: 'concorda'}

// Hapi Plugin for wiring up Concorda
module.exports = function (server, opts, next) {
  // Set up our seneca plugins
  let seneca = server.seneca
  options = _.extend({}, opts, options)

  seneca.add('role: ' + options.name + ', cmd: closeSession', function(msg, done){
    done()
  })

  seneca
    .use('mesh',{auto:true, pin:'role:' + options.name + ', info: *'})

  next()
}

// Hapi plugin metadata
module.exports.attributes = {
  name: options.name
}
<commit_msg>change pattern for info: logout seneca action<commit_after>'use strict'

const _ = require('lodash')

let options = {name: 'concorda'}

// Hapi Plugin for wiring up Concorda
module.exports = function (server, opts, next) {
  // Set up our seneca plugins
  let seneca = server.seneca
  options = _.extend({}, opts, options)

  seneca.add('role: ' + options.name + ', info: logout', function(msg, done){
    done()
  })

  seneca
    .use('mesh',{auto:true, pin:'role:' + options.name + ', info: *'})

  next()
}

// Hapi plugin metadata
module.exports.attributes = {
  name: options.name
}
<|endoftext|>


<commit_before>"use strict";

var passport          = require('passport');
var session           = require('express-session');
var pg                = require('pg');
var pgSession         = require('connect-pg-simple')(session);
var uuid              = require('node-uuid');
var cookieParser      = require('cookie-parser');
var Strategies        = require('./oauthStrategies');
var User              = require('../models/users');

module.exports = (app,express) => {
  let database_url = process.env.DATABASE_URL || 'postgresql://localhost/fairshare';

  const sessionConfig = {
    genid: () => uuid.v1(),
    store:  new pgSession({
      pg       : pg,
      conString: database_url,
      tableName: 'sessions'
    }),
    secret: 'kitkat',
    resave: true,
    saveUninitialized: true
  };

  app.use(session(sessionConfig));
  app.use(passport.initialize());
  app.use(passport.session());
  app.use(cookieParser('kitkat'));

  passport.serializeUser((user, done) => {
    return done(null, user.id);
  });

  passport.deserializeUser((id, done) => {
    User.getById({id: id})
    .then( userObj => {
      return done(null, userObj[0]);
    })
    .catch( err => {
      console.warn("err at deserialize:", err);
    });
  });

  passport.use(Strategies.facebook_strat);
  passport.use(Strategies.google_strat);
  passport.use(Strategies.paypal_strat);
};<commit_msg>set resave to false in passport<commit_after>"use strict";

var passport          = require('passport');
var session           = require('express-session');
var pg                = require('pg');
var pgSession         = require('connect-pg-simple')(session);
var uuid              = require('node-uuid');
var cookieParser      = require('cookie-parser');
var Strategies        = require('./oauthStrategies');
var User              = require('../models/users');

module.exports = (app,express) => {
  let database_url = process.env.DATABASE_URL || 'postgresql://localhost/fairshare';

  const sessionConfig = {
    genid: () => uuid.v1(),
    store:  new pgSession({
      pg       : pg,
      conString: database_url,
      tableName: 'sessions'
    }),
    secret: 'kitkat',
    resave: false,
    saveUninitialized: false
  };

  app.use(session(sessionConfig));
  app.use(passport.initialize());
  app.use(passport.session());
  app.use(cookieParser('kitkat'));

  passport.serializeUser((user, done) => {
    return done(null, user.id);
  });

  passport.deserializeUser((id, done) => {
    User.getById({id: id})
    .then( userObj => {
      return done(null, userObj[0]);
    })
    .catch( err => {
      console.warn("err at deserialize:", err);
    });
  });

  passport.use(Strategies.facebook_strat);
  passport.use(Strategies.google_strat);
  passport.use(Strategies.paypal_strat);
};
<|endoftext|>


<commit_before>var dbConfig = require(__base + 'secret/config-db.json');
var MariaSql = require('mariasql');
var bluebird = require('bluebird');

var TagDB = {
	getTags() {
		const connection = bluebird.promisifyAll(new MariaSql(dbConfig));
		const query = (
			'SELECT DISTINCT t.tag_text AS tag FROM TAGS t ' +
			'JOIN USER_TAGS ut ON t.tag_id = ut.tag_id ' +
			'WHERE ut.user_id = :userId'
		)
		return connection.queryAsync(query, {userId: 1}, {useArray: true}).then(rows => {
			connection.end()
			return rows.map(row => row[0])
		})
	},

	addTags(messageId, tags) {
		const connection = bluebird.promisifyAll(new MariaSql(dbConfig));

		const promiseTags = tags.map(tag => {
			// insert tag
			// hook tag to message
			// hook tag to user
			const insertTag = "INSERT INTO TAGS (tag_text) VALUES (:tagText)"
			const getLink = "SELECT link_id FROM MESSAGE WHERE message_id = :messageId"
			const insertTagLink = "INSERT INTO LINKS_TAGS (link_id, tag_id) VALUES (:linkId, :tagId)"
			const insertTagUser = "INSERT INTO USER_TAGS (user_id, tag_id) VALUES (:userId, :tagId)"

			return connection.queryAsync(insertTag, {tagText: tag}, {useArray: true}).then(() => {
				const tagId = connection.lastInsertId()

				return connection.queryAsync(getLink, {messageId: messageId}, {useArray: true}).then(rows => {
					if (rows && rows.length) {
						const linkId = rows[0]
						console.log("link rows", rows)
						return {
							tagId: tagId,
							linkId: linkId
						}
					}
					return {}
				})
			}).then(linkAndTag => {
				const link = linkAndTag.linkId
				const tag = linkAndTag.tagId

				console.log(linkAndTag)

				if (link && tag) {
					connection.queryAsync(insertTagLink, {linkId: link, tagId: tag})
					connection.queryAsync(insertTagUser, {userId: 1, tagId: tag})
				}
			})
		})

		return Promise.all(promiseTags).then(() => connection.end())
	}
}

module.exports = function (connection) {
	var tagDB = Object.create(TagDB);
	tagDB._connection = connection;
	return tagDB;
}
<commit_msg>adding tagging<commit_after>var dbConfig = require(__base + 'secret/config-db.json');
var MariaSql = require('mariasql');
var bluebird = require('bluebird');

var TagDB = {
	getTags() {
		const connection = bluebird.promisifyAll(new MariaSql(dbConfig));
		const query = (
			'SELECT DISTINCT t.tag_text AS tag FROM TAGS t ' +
			'JOIN USER_TAGS ut ON t.tag_id = ut.tag_id ' +
			'WHERE ut.user_id = :userId'
		)
		return connection.queryAsync(query, {userId: 1}, {useArray: true}).then(rows => {
			connection.end()
			return rows.map(row => row[0])
		})
	},

	addTags(messageId, tags) {
		const connection = bluebird.promisifyAll(new MariaSql(dbConfig));
		console.log(tags)

		const promiseTags = tags.map(tag => {
			// insert tag
			// hook tag to message
			// hook tag to user
			const insertTag = "INSERT INTO TAGS (tag_text) VALUES (:tagText)"
			const getLink = "SELECT link_id FROM MESSAGE WHERE message_id = :messageId"
			const insertTagLink = "INSERT INTO LINKS_TAGS (link_id, tag_id) VALUES (:linkId, :tagId)"
			const insertTagUser = "INSERT INTO USER_TAGS (user_id, tag_id) VALUES (:userId, :tagId)"

			return connection.queryAsync(insertTag, {tagText: tag}, {useArray: true}).then(() => {
				const tagId = connection.lastInsertId()

				return connection.queryAsync(getLink, {messageId: messageId}, {useArray: true}).then(rows => {
					if (rows && rows.length) {
						const linkId = rows[0]
						console.log("link rows", rows)
						return {
							tagId: tagId,
							linkId: linkId
						}
					}
					return {}
				})
			}).then(linkAndTag => {
				const link = linkAndTag.linkId
				const tag = linkAndTag.tagId

				console.log(linkAndTag)

				if (link && tag) {
					connection.queryAsync(insertTagLink, {linkId: link, tagId: tag})
					connection.queryAsync(insertTagUser, {userId: 1, tagId: tag})
				}
			})
		})

		return Promise.all(promiseTags).then(() => connection.end())
	}
}

module.exports = function (connection) {
	var tagDB = Object.create(TagDB);
	tagDB._connection = connection;
	return tagDB;
}
<|endoftext|>


<commit_before>const extractJSComments = require('./extract-js-comments');

const someJS = `
// single line comment

/*
a multiline comment
*/

var x = 'y'; // single line comment

var y = '// single line comment';

`;

describe('extractJSComments', () => {
  it('should return an object with comment keys and count values', () => {
    const commentCounts = {
      'single line comment': 2,
      'a multiline comment': 1
    };
    expect(extractJSComments(someJS)).toEqual(commentCounts);
  });
});
<commit_msg>test: extract-js-comments error<commit_after>const extractJSComments = require('./extract-js-comments');

const someJS = `
// single line comment

/*
a multiline comment
*/

var x = 'y'; // single line comment

var y = '// single line comment';

`;

const someInvalidJS = `const isChange;`;

describe('extractJSComments', () => {
  it('should return an object with comment keys and count values', () => {
    const commentCounts = {
      'single line comment': 2,
      'a multiline comment': 1
    };
    expect(extractJSComments(someJS)).toEqual(commentCounts);
  });

  it('should throw an informative error if the JS is invalid', () => {
    expect(() => extractJSComments(someInvalidJS)).toThrow(
      `extract-js-comments could not parse the code below, this challenge have invalid syntax:

${someInvalidJS}
`
    );
  });
});
<|endoftext|>


<commit_before>dojo.provide("dijit.ProgressBar");

dojo.require("dojo.fx");
dojo.require("dojo.number");

dojo.require("dijit._Widget");
dojo.require("dijit._Templated");

dojo.declare("dijit.ProgressBar", [dijit._Widget, dijit._Templated], {
	// summary:
	//		A progress indication widget, showing the amount completed
	//		(often the percentage completed) of a task.
	//
	// example:
	// |

	// |

	//
	// description:
	//		Note that the progress bar is updated via (a non-standard)
	//		update() method, rather than via attr() like other widgets.

	// progress: [const] String (Percentage or Number)
	//		Number or percentage indicating amount of task completed.
	// 		With "%": percentage value, 0% <= progress <= 100%, or
	// 		without "%": absolute value, 0 <= progress <= maximum
	// TODO: rename to value for 2.0
	progress: "0",

	// maximum: [const] Float
	//		Max sample number
	maximum: 100,

	// places: [const] Number
	//		Number of places to show in values; 0 by default
	places: 0,

	// indeterminate: [const] Boolean
	// 		If false: show progress value (number or percentage).
	// 		If true: show that a process is underway but that the amount completed is unknown.
	indeterminate: false,

	// label: String?
	//		Label on progress bar.   Defaults to percentage for determinate progress bar and
	//		blank for indeterminate progress bar.
	label:"",

	// name: String
	//		this is the field name (for a form) if set. This needs to be set if you want to use
	//		this widget in a dijit.form.Form widget (such as dijit.Dialog)
	name: '',

	templateString: dojo.cache("dijit", "templates/ProgressBar.html"),

	// _indeterminateHighContrastImagePath: [private] dojo._URL
	//		URL to image to use for indeterminate progress bar when display is in high contrast mode
	_indeterminateHighContrastImagePath:
		dojo.moduleUrl("dijit", "themes/a11y/indeterminate_progress.gif"),

	// public functions
	postCreate: function(){
		this.inherited(arguments);
		this.indeterminateHighContrastImage.setAttribute("src",
			this._indeterminateHighContrastImagePath.toString());
		this.update();
	},

	update: function(/*Object?*/attributes){
		// summary:
		//		Change attributes of ProgressBar, similar to set(hash).
		//
		// attributes:
		//		May provide progress and/or maximum properties on this parameter;
		//		see attribute specs for details.
		//
		// example:
		//	|	myProgressBar.update({'indeterminate': true});
		//	|	myProgressBar.update({'progress': 80});
		//	|	myProgressBar.update({'indeterminate': true, label:"Loading ..." })

		// TODO: deprecate this method and use set() instead

		dojo.mixin(this, attributes || {});
		var tip = this.internalProgress;
		var percent = 1;
		if(this.indeterminate){
			dijit.removeWaiState(tip, "valuenow");
			dijit.removeWaiState(tip, "valuemin");
			dijit.removeWaiState(tip, "valuemax");
		}else{
			if(String(this.progress).indexOf("%") != -1){
				percent = Math.min(parseFloat(this.progress)/100, 1);
				this.progress = percent * this.maximum;
			}else{
				this.progress = Math.min(this.progress, this.maximum);
				percent = this.progress / this.maximum;
			}

			dijit.setWaiState(tip, "describedby", this.label.id);
			dijit.setWaiState(tip, "valuenow", this.progress);
			dijit.setWaiState(tip, "valuemin", 0);
			dijit.setWaiState(tip, "valuemax", this.maximum);
		}
		this.labelNode.innerHTML = this.report(percent);

		dojo.toggleClass(this.domNode, "dijitProgressBarIndeterminate", this.indeterminate);
		tip.style.width = (percent * 100) + "%";
		this.onChange();
	},

	_setValueAttr: function(v){
		if(v == Infinity){
			this.update({indeterminate:true});
		}else{
			this.update({indeterminate:false, progress:v});
		}
	},

	_getValueAttr: function(){
		return this.progress;
	},

	_setLabelAttr: function(label){
		this.label = label;
		this.update();
	},

	_setIndeterminateAttr: function(indeterminate){
		this.indeterminate = indeterminate;
		this.update();
	},

	report: function(/*float*/percent){
		// summary:
		//		Generates message to show inside progress bar (normally indicating amount of task completed).
		//		May be overridden.
		// tags:
		//		extension

		return this.label ? this.label :
				(this.indeterminate ? " " : dojo.number.format(percent, { type: "percent", places: this.places, locale: this.lang }));
	},

	onChange: function(){
		// summary:
		//		Callback fired when progress updates.
		// tags:
		//		progress
	}
});
<commit_msg>Changing doc/tests to indicate that set() is the preferred interface for changing percent, label, indeterminate mode, etc.   Refs #4322 !strict.<commit_after>dojo.provide("dijit.ProgressBar");

dojo.require("dojo.fx");
dojo.require("dojo.number");

dojo.require("dijit._Widget");
dojo.require("dijit._Templated");

dojo.declare("dijit.ProgressBar", [dijit._Widget, dijit._Templated], {
	// summary:
	//		A progress indication widget, showing the amount completed
	//		(often the percentage completed) of a task.
	//
	// example:
	// |

	// |

	// progress: [const] String (Percentage or Number)
	//		Number or percentage indicating amount of task completed.
	// 		With "%": percentage value, 0% <= progress <= 100%, or
	// 		without "%": absolute value, 0 <= progress <= maximum
	// TODO: rename to value for 2.0
	progress: "0",

	// maximum: [const] Float
	//		Max sample number
	maximum: 100,

	// places: [const] Number
	//		Number of places to show in values; 0 by default
	places: 0,

	// indeterminate: [const] Boolean
	// 		If false: show progress value (number or percentage).
	// 		If true: show that a process is underway but that the amount completed is unknown.
	indeterminate: false,

	// label: String?
	//		Label on progress bar.   Defaults to percentage for determinate progress bar and
	//		blank for indeterminate progress bar.
	label:"",

	// name: String
	//		this is the field name (for a form) if set. This needs to be set if you want to use
	//		this widget in a dijit.form.Form widget (such as dijit.Dialog)
	name: '',

	templateString: dojo.cache("dijit", "templates/ProgressBar.html"),

	// _indeterminateHighContrastImagePath: [private] dojo._URL
	//		URL to image to use for indeterminate progress bar when display is in high contrast mode
	_indeterminateHighContrastImagePath:
		dojo.moduleUrl("dijit", "themes/a11y/indeterminate_progress.gif"),

	// public functions
	postCreate: function(){
		this.inherited(arguments);
		this.indeterminateHighContrastImage.setAttribute("src",
			this._indeterminateHighContrastImagePath.toString());
		this.update();
	},

	update: function(/*Object?*/attributes){
		// summary:
		//		Change attributes of ProgressBar, similar to set(hash).  Users should call
		//		set() rather than calling this method directly.
		//
		// attributes:
		//		May provide progress and/or maximum properties on this parameter;
		//		see attribute specs for details.
		//
		// example:
		//	|	myProgressBar.update({'indeterminate': true});
		//	|	myProgressBar.update({'progress': 80});
		//	|	myProgressBar.update({'indeterminate': true, label:"Loading ..." })

		// TODO: deprecate this method and use set() instead

		dojo.mixin(this, attributes || {});
		var tip = this.internalProgress;
		var percent = 1;
		if(this.indeterminate){
			dijit.removeWaiState(tip, "valuenow");
			dijit.removeWaiState(tip, "valuemin");
			dijit.removeWaiState(tip, "valuemax");
		}else{
			if(String(this.progress).indexOf("%") != -1){
				percent = Math.min(parseFloat(this.progress)/100, 1);
				this.progress = percent * this.maximum;
			}else{
				this.progress = Math.min(this.progress, this.maximum);
				percent = this.progress / this.maximum;
			}

			dijit.setWaiState(tip, "describedby", this.label.id);
			dijit.setWaiState(tip, "valuenow", this.progress);
			dijit.setWaiState(tip, "valuemin", 0);
			dijit.setWaiState(tip, "valuemax", this.maximum);
		}
		this.labelNode.innerHTML = this.report(percent);

		dojo.toggleClass(this.domNode, "dijitProgressBarIndeterminate", this.indeterminate);
		tip.style.width = (percent * 100) + "%";
		this.onChange();
	},

	_setValueAttr: function(v){
		if(v == Infinity){
			this.update({indeterminate:true});
		}else{
			this.update({indeterminate:false, progress:v});
		}
	},

	_getValueAttr: function(){
		return this.progress;
	},

	_setLabelAttr: function(label){
		this.label = label;
		this.update();
	},

	_setIndeterminateAttr: function(indeterminate){
		this.indeterminate = indeterminate;
		this.update();
	},

	report: function(/*float*/percent){
		// summary:
		//		Generates message to show inside progress bar (normally indicating amount of task completed).
		//		May be overridden.
		// tags:
		//		extension

		return this.label ? this.label :
				(this.indeterminate ? " " : dojo.number.format(percent, { type: "percent", places: this.places, locale: this.lang }));
	},

	onChange: function(){
		// summary:
		//		Callback fired when progress updates.
		// tags:
		//		extension
	}
});
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import TimeView from './TimeView';
import Day from './Day';

export default class Calendar extends Component {
  constructor(props) {
    super(props);
    this.state = { backdrop: false, backdropTargetHandler: () => {} };
  }

  hideBackdrop = () => {
    this.state.backdropTargetHandler(false);
    this.setState({ backdrop: false, backdropTargetHandler: () => {} });
  };

  showBackdrop = (selectionHandler) => {
    selectionHandler(true);
    this.setState({ backdrop: true, backdropTargetHandler: selectionHandler });
  }

  render() {
    const today = moment();
    const { weekEvents, date, start, end } = this.props;
    return (
      <container>
        <div className={`calendar ${this.state.backdrop ? 'has-backdrop' : ''}`}>
          <TimeView start={start} end={end} />
          {new Array(weekEvents.length).fill().map((_, i) => {
            return (<Day
              key={i}
              eventData={weekEvents[i]}
              name={name}
              start={start}
              end={end}
              date={moment(date).day(0).add(i, 'days')}
              showBackdrop={this.showBackdrop}
              isCurrent={i === today.day()
                && today.week() === date.week()
                && today.year() === date.year()}
            />);
          })}
          <div
            className={`calendar--backdrop ${this.state.backdrop ? 'is-visible' : ''}`}
            onClick={this.hideBackdrop}
          />

      </container>
    );
  }
}

Calendar.propTypes = {
  weekEvents: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.object)).isRequired,
  date: PropTypes.object.isRequired,
  start: PropTypes.number,
  end: PropTypes.number,
};

Calendar.defaultProps = {
  start: 8,
  end: 18,
};
<commit_msg>Let timeline update every two minutes<commit_after>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import TimeView from './TimeView';
import Day from './Day';

export default class Calendar extends Component {
  constructor(props) {
    super(props);
    this.state = { backdrop: false, backdropTargetHandler: () => {}, timeline: 0 };
    setInterval(() => { this.setState({ timeline: moment().seconds() }); }, 2 * 60 * 1000);
  }

  hideBackdrop = () => {
    this.state.backdropTargetHandler(false);
    this.setState({ backdrop: false, backdropTargetHandler: () => {} });
  };

  showBackdrop = (selectionHandler) => {
    selectionHandler(true);
    this.setState({ backdrop: true, backdropTargetHandler: selectionHandler });
  }

  render() {
    const today = moment();
    const { weekEvents, date, start, end } = this.props;
    return (
      <container>
        <div className={`calendar ${this.state.backdrop ? 'has-backdrop' : ''}`}>
          <TimeView start={start} end={end} />
          {new Array(weekEvents.length).fill().map((_, i) => {
            return (<Day
              key={i}
              eventData={weekEvents[i]}
              name={name}
              start={start}
              end={end}
              date={moment(date).day(0).add(i, 'days')}
              showBackdrop={this.showBackdrop}
              isCurrent={i === today.day()
                && today.week() === date.week()
                && today.year() === date.year()}
            />);
          })}
          <div
            className={`calendar--backdrop ${this.state.backdrop ? 'is-visible' : ''}`}
            onClick={this.hideBackdrop}
          />

      </container>
    );
  }
}

Calendar.propTypes = {
  weekEvents: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.object)).isRequired,
  date: PropTypes.object.isRequired,
  start: PropTypes.number,
  end: PropTypes.number,
};

Calendar.defaultProps = {
  start: 8,
  end: 18,
};
<|endoftext|>


<commit_before>/**
 * WalkontableColumnStrategy
 * @param containerSizeFn
 * @param sizeAtIndex
 * @param strategy - all, last, none
 * @constructor
 */
function WalkontableColumnStrategy(instance, containerSizeFn, sizeAtIndex, strategy) {
  var size
    , i = 0;

  WalkontableAbstractStrategy.apply(this, arguments);

  this.containerSizeFn = containerSizeFn;
  this.cellSizesSum = 0;
  this.cellSizes = [];
  this.cellStretch = [];
  this.cellCount = 0;
  this.visibleCellCount = 0;
  this.remainingSize = 0;
  this.strategy = strategy;

  //step 1 - determine cells that fit containerSize and cache their widths
  while (true) {
    size = sizeAtIndex(i);
    if (size === void 0) {
      break; //total columns exceeded
    }
    if (this.cellSizesSum <= this.getContainerSize(this.cellSizesSum + size)) {
      this.visibleCellCount++;
    }
    this.cellSizes.push(size);
    this.cellSizesSum += size;
    this.cellCount++;

    i++;
  }

  var containerSize = this.getContainerSize(this.cellSizesSum);
  this.remainingSize = this.cellSizesSum - containerSize;
  //negative value means the last cell is fully visible and there is some space left for stretching
  //positive value means the last cell is not fully visible
}

WalkontableColumnStrategy.prototype = new WalkontableAbstractStrategy();

WalkontableColumnStrategy.prototype.getSize = function (index) {
  return this.cellSizes[index] + (this.cellStretch[index] || 0);
};

WalkontableColumnStrategy.prototype.stretch = function () {
  //step 2 - apply stretching strategy
  var containerSize
    , i = 0;

  containerSize = this.instance.wtTable.allRowsInViewport() ? this.getContainerSize() : this.getContainerSize(Infinity);

  this.remainingSize = this.cellSizesSum - containerSize;

  this.cellStretch.length = 0; //clear previous stretch

  if (this.strategy === 'all') {
    if (this.remainingSize < 0) {
      var ratio = containerSize / this.cellSizesSum;
      var newSize;

      while (i < this.cellCount - 1) { //"i < this.cellCount - 1" is needed because last cellSize is adjusted after the loop
        newSize = Math.floor(ratio * this.cellSizes[i]);
        this.remainingSize += newSize - this.cellSizes[i];
        this.cellStretch[i] = newSize - this.cellSizes[i];
        i++;
      }
      this.cellStretch[this.cellCount - 1] = -this.remainingSize;
      this.remainingSize = 0;
    }
  }
  else if (this.strategy === 'last') {
    if (this.remainingSize < 0 && containerSize !== Infinity) { //Infinity is with native scroll when the table is wider than the viewport (TODO: test)
      this.cellStretch[this.cellCount - 1] = -this.remainingSize;
      this.remainingSize = 0;
    }
  }
};

WalkontableColumnStrategy.prototype.countVisible = function () {
  return this.visibleCellCount;
};

WalkontableColumnStrategy.prototype.isLastIncomplete = function () {

  var firstRow = this.instance.wtTable.getFirstVisibleRow();
  var lastCol = this.instance.wtTable.getLastVisibleColumn();
  var cell = this.instance.wtTable.getCell(new WalkontableCellCoords(firstRow, lastCol));
  var cellOffset = WalkontableDom.prototype.offset(cell);
  var cellWidth = WalkontableDom.prototype.outerWidth(cell);
  var cellEnd = cellOffset.left + cellWidth;

  var viewportOffsetLeft = this.instance.wtScrollbars.vertical.getScrollPosition();
  var viewportWitdh = this.instance.wtViewport.getViewportWidth();
  var viewportEnd = viewportOffsetLeft + viewportWitdh;

  return viewportEnd >= cellEnd;
};<commit_msg>Fixed columnStrategy.visibleCellCount()<commit_after>/**
 * WalkontableColumnStrategy
 * @param containerSizeFn
 * @param sizeAtIndex
 * @param strategy - all, last, none
 * @constructor
 */
function WalkontableColumnStrategy(instance, containerSizeFn, sizeAtIndex, strategy) {
  var size
    , i = 0;

  WalkontableAbstractStrategy.apply(this, arguments);

  this.containerSizeFn = containerSizeFn;
  this.cellSizesSum = 0;
  this.cellSizes = [];
  this.cellStretch = [];
  this.cellCount = 0;
  this.visibleCellCount = 0;
  this.remainingSize = 0;
  this.strategy = strategy;

  //step 1 - determine cells that fit containerSize and cache their widths
  while (true) {
    size = sizeAtIndex(i);
    if (size === void 0) {
      break; //total columns exceeded
    }
    if (this.cellSizesSum < this.getContainerSize(this.cellSizesSum + size)) {
      this.visibleCellCount++;
    }
    this.cellSizes.push(size);
    this.cellSizesSum += size;
    this.cellCount++;

    i++;
  }

  var containerSize = this.getContainerSize(this.cellSizesSum);
  this.remainingSize = this.cellSizesSum - containerSize;
  //negative value means the last cell is fully visible and there is some space left for stretching
  //positive value means the last cell is not fully visible
}

WalkontableColumnStrategy.prototype = new WalkontableAbstractStrategy();

WalkontableColumnStrategy.prototype.getSize = function (index) {
  return this.cellSizes[index] + (this.cellStretch[index] || 0);
};

WalkontableColumnStrategy.prototype.stretch = function () {
  //step 2 - apply stretching strategy
  var containerSize
    , i = 0;

  containerSize = this.instance.wtTable.allRowsInViewport() ? this.getContainerSize() : this.getContainerSize(Infinity);

  this.remainingSize = this.cellSizesSum - containerSize;

  this.cellStretch.length = 0; //clear previous stretch

  if (this.strategy === 'all') {
    if (this.remainingSize < 0) {
      var ratio = containerSize / this.cellSizesSum;
      var newSize;

      while (i < this.cellCount - 1) { //"i < this.cellCount - 1" is needed because last cellSize is adjusted after the loop
        newSize = Math.floor(ratio * this.cellSizes[i]);
        this.remainingSize += newSize - this.cellSizes[i];
        this.cellStretch[i] = newSize - this.cellSizes[i];
        i++;
      }
      this.cellStretch[this.cellCount - 1] = -this.remainingSize;
      this.remainingSize = 0;
    }
  }
  else if (this.strategy === 'last') {
    if (this.remainingSize < 0 && containerSize !== Infinity) { //Infinity is with native scroll when the table is wider than the viewport (TODO: test)
      this.cellStretch[this.cellCount - 1] = -this.remainingSize;
      this.remainingSize = 0;
    }
  }
};

WalkontableColumnStrategy.prototype.countVisible = function () {
  return this.visibleCellCount;
};

WalkontableColumnStrategy.prototype.isLastIncomplete = function () {

  var firstRow = this.instance.wtTable.getFirstVisibleRow();
  var lastCol = this.instance.wtTable.getLastVisibleColumn();
  var cell = this.instance.wtTable.getCell(new WalkontableCellCoords(firstRow, lastCol));
  var cellOffset = WalkontableDom.prototype.offset(cell);
  var cellWidth = WalkontableDom.prototype.outerWidth(cell);
  var cellEnd = cellOffset.left + cellWidth;

  var viewportOffsetLeft = this.instance.wtScrollbars.vertical.getScrollPosition();
  var viewportWitdh = this.instance.wtViewport.getViewportWidth();
  var viewportEnd = viewportOffsetLeft + viewportWitdh;

  return viewportEnd >= cellEnd;
};<|endoftext|>