<commit_before>// Copyright 2011 The Closure Library 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.

/**
 * @fileoverview Offered as an alternative to XhrIo as a way for making requests
 * via XMLHttpRequest.  Instead of mirroring the XHR interface and exposing
 * events, deferreds are used as a way to pass a "promise" of the response to
 * interested parties.
 *
 */

goog.provide('goog.labs.net.xhr');
goog.provide('goog.labs.net.xhr.Error');
goog.provide('goog.labs.net.xhr.HttpError');
goog.provide('goog.labs.net.xhr.TimeoutError');

goog.require('goog.async.Deferred');
goog.require('goog.debug.Error');
goog.require('goog.json');
goog.require('goog.net.HttpStatus');
goog.require('goog.net.XmlHttp');
goog.require('goog.string');
goog.require('goog.uri.utils');

goog.scope(function() {
  var _ = goog.labs.net.xhr;
  var Deferred = goog.async.Deferred;
  var HttpStatus = goog.net.HttpStatus;

  /**
   * Configuration options for an XMLHttpRequest.
   * - headers: map of header key/value pairs.
   * - timeoutMs: number of milliseconds after which the request will be timed
   *      out by the client. Default is to allow the browser to handle timeouts.
   * - withCredentials: whether user credentials are to be included in a
   *      cross-origin request.  See:
   *      http://dev.w3.org/2006/webapi/XMLHttpRequest-2/#the-withcredentials-attribute
   * - mimeType: allows the caller to override the content-type and charset for
   *      the request, which is useful when requesting binary data.  See:
   *      http://dev.w3.org/2006/webapi/XMLHttpRequest-2/#dom-xmlhttprequest-overridemimetype
   * - xssiPrefix: Prefix used for protecting against XSSI attacks, which should
   *      be removed before parsing the response as JSON.
   *
   * @typedef {{
   *   headers: (Object.<string>|undefined),
   *   timeoutMs: (number|undefined),
   *   withCredentials: (boolean|undefined),
   *   mimeType: (string|undefined),
   *   xssiPrefix: (string|undefined)
   * }}
   */
  _.Options;

  /**
   * Defines the types that are allowed as post data.
   * @typedef {(ArrayBuffer|Blob|Document|FormData|null|string|undefined)}
   */
  _.PostData;

  /**
   * The Content-Type HTTP header name.
   * @type {string}
   */
  _.CONTENT_TYPE_HEADER = 'Content-Type';

  /**
   * The Content-Type HTTP header value for a url-encoded form.
   * @type {string}
   */
  _.FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded;charset=utf-8';

  /**
   * Sends a get request, returning a deferred which will be called back with
   * the response text once the request completes.
   *
   * @param {string} url The URL to request.
   * @param {_.Options=} opt_options Configuration options for the request.
   * @return {!Deferred} A deferred object that will be called back
   *     with the response text once the request finishes.
   */
  _.get = function(url, opt_options) {
    return _.send('GET', url, null, opt_options).
        addCallback(_.getResponseText_);
  };

  /**
   * Sends a post request, returning a deferred which will be called back with
   * the response text once the request completes.
   *
   * @param {string} url The URL to request.
   * @param {_.PostData} data The body of the post request.
   * @param {_.Options=} opt_options Configuration options for the request.
   * @return {!Deferred} A deferred object that will be called back
   *     with the response text once the request finishes.
   */
  _.post = function(url, data, opt_options) {
    return _.send('POST', url, data, opt_options).
        addCallback(_.getResponseText_);
  };

  /**
   * Sends a get request, returning a deferred which will be called back with
   * the parsed response text once the request completes.
   *
   * @param {string} url The URL to request.
   * @param {_.Options=} opt_options Configuration options for the request.
   * @return {!Deferred} A deferred object that will be called back
   *     with the response JSON once the request finishes.
   */
  _.getJson = function(url, opt_options) {
    var d = _.send('GET', url, null, opt_options);
    _.addJsonParsingCallbacks_(d, opt_options);
    return d;
  };

  /**
   * Sends a post request, returning a deferred which will be called back with
   * the parsed response text once the request completes.
   *
   * @param {string} url The URL to request.
   * @param {_.PostData} data The body of the post request.
   * @param {_.Options=} opt_options Configuration options for the request.
   * @return {!Deferred} A deferred object that will be called back
   *     with the response JSON once the request finishes.
   */
  _.postJson = function(url, data, opt_options) {
    var d = _.send('POST', url, data, opt_options);
    _.addJsonParsingCallbacks_(d, opt_options);
    return d;
  };

  /**
   * Sends a request using XMLHttpRequest and returns a deferred.
   *
   * @param {string} method The HTTP method for the request.
   * @param {string} url The URL to request.
   * @param {_.PostData} data The body of the post request.
   * @param {_.Options=} opt_options Configuration options for the request.
   * @return {!Deferred} A deferred object that will be called back
   *     with the XHR object when the request finishes.
   */
  _.send = function(method, url, data, opt_options) {

    // When the deferred is cancelled, we abort the XHR.  We want to make sure
    // the readystatechange event still fires, so it can do the timeout
    // cleanup, however we don't want the callback or errback to be called
    // again.  Thus the slight ugliness here.  If deferreds were pushed into
    // makeRequest, this could become a lot cleaner but we want an option for
    // people not to include goog.async.Deferred.

    function cancel() {
      cancelled = true;
      xhr.abort();
      xhr.onreadystatechange = goog.nullFunction;
      // TODO(user): We could return an AbortedError if we wanted to.
    }

    function callback(data) {
      if (!cancelled) {
        d.callback(data);
      }
    }

    function errback(err) {
      if (!cancelled) {
        d.errback(err);
      }
    }

    var cancelled = false;
    var d = new Deferred(cancel);
    var xhr = _.makeRequest(method, url, data, opt_options, callback, errback);

    return d;
  };

  /**
   * Creates a new XMLHttpRequest and initiates a request.
   *
   * @param {string} method The HTTP method for the request.
   * @param {string} url The URL to request.
   * @param {_.PostData} data The body of the post request, unless the content
   *    type is explicitly set in the Options, then it will default to form
   *    urlencoded.
   * @param {_.Options=} opt_options Configuration options for the request.
   * @param {function(XMLHttpRequest)=} opt_callback Optional callback to call
   *     when the request completes.
   * @param {function(Error)=} opt_errback Optional callback to call
   *     when there is an error.
   * @return {!XMLHttpRequest} The new XMLHttpRequest.
   */
  _.makeRequest = function(
      method, url, data, opt_options, opt_callback, opt_errback) {
    var options = opt_options || {};
    var callback = opt_callback || goog.nullFunction;
    var errback = opt_errback || goog.nullFunction;
    var timer;

    var xhr = /** @type {!XMLHttpRequest} */ (goog.net.XmlHttp());
    try {
      xhr.open(method, url, true);
    } catch (e) {
      // XMLHttpRequest.open may throw when 'open' is called, for example, IE7
      // throws "Access Denied" for cross-origin requests.
      errback(new _.Error('Error opening XHR: ' + e.message, url, xhr));
      return xhr;
    }

    // So sad that IE doesn't support onload and onerror.
    xhr.onreadystatechange = function() {
      if (xhr.readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
        window.clearTimeout(timer);
        if (HttpStatus.isSuccess(xhr.status) ||
            xhr.status === 0 && !_.isEffectiveSchemeHttp_(url)) {
          callback(xhr);
        } else {
          errback(new _.HttpError(xhr.status, url, xhr));
        }
      }
    };

    // Set the headers.
    var contentTypeIsSet = false;
    if (options.headers) {
      for (var key in options.headers) {
        xhr.setRequestHeader(key, options.headers[key]);
      }
      contentTypeIsSet = _.CONTENT_TYPE_HEADER in options.headers;
    }

    // If a content type hasn't been set, default to form-urlencoded/UTF8 for
    // POSTs.  This is because some proxies have been known to reject posts
    // without a content-type.
    if (method == 'POST' && !contentTypeIsSet) {
      xhr.setRequestHeader(_.CONTENT_TYPE_HEADER, _.FORM_CONTENT_TYPE);
    }

    // Set whether to pass cookies on cross-domain requests (if applicable).
    // @see http://dev.w3.org/2006/webapi/XMLHttpRequest-2/#the-withcredentials-attribute
    if (options.withCredentials) {
      xhr.withCredentials = options.withCredentials;
    }

    // Allow the request to override the mime type, useful for getting binary
    // data from the server.  e.g. 'text/plain; charset=x-user-defined'.
    // @see http://dev.w3.org/2006/webapi/XMLHttpRequest-2/#dom-xmlhttprequest-overridemimetype
    if (options.mimeType) {
      xhr.overrideMimeType(options.mimeType);
    }

    // Handle timeouts, if requested.
    if (options.timeoutMs > 0) {
      timer = window.setTimeout(function() {
        // Clear event listener before aborting so the errback will not be
        // called twice.
        xhr.onreadystatechange = goog.nullFunction;
        xhr.abort();
        errback(new _.TimeoutError(url, xhr));
      }, options.timeoutMs);
    }

    // Trigger the send.
    try {
      xhr.send(data);
    } catch (e) {
      // XMLHttpRequest.send is known to throw on some versions of FF, for example
      // if a cross-origin request is disallowed.
      errback(new _.Error('Error sending XHR: ' + e.message, url, xhr));
    }

    return xhr;
  };

  /**
   * @param {string} url The URL to test.
   * @return {boolean} Whether the effective scheme is HTTP or HTTPs.
   * @private
   */
  _.isEffectiveSchemeHttp_ = function(url) {
    var scheme = goog.uri.utils.getEffectiveScheme(url);
    // NOTE(user): Empty-string is for the case under FF3.5 when the location
    // is not defined inside a web worker.
    return scheme == 'http' || scheme == 'https' || scheme == '';
  };

  /**
   * Returns the response text of an XHR object.  Intended for use as a callback
   * on a deferred.
   *
   * @param {!XMLHttpRequest} xhr The XHR object.
   * @return {string} The response text.
   * @private
   */
  _.getResponseText_ = function(xhr) {
    return xhr.responseText;
  };

  /**
   * Adds the callbacks required for parsing a response as JSON.  The chain will
   * expect an XMLHttpRequest and will result in a JS object.  Parse errors will
   * trigger the errback chain.
   *
   * @param {!Deferred} d The deferred to add callbacks to.
   * @param {_.Options|undefined} options The options object.
   * @private
   */
  _.addJsonParsingCallbacks_ = function(d, options) {
    d.addCallback(_.getResponseText_);
    if (options && options.xssiPrefix) {
      d.addCallback(goog.partial(_.stripXssiPrefix_, options.xssiPrefix));
    }
    d.addCallback(goog.json.parse);
  };

  /**
   * Strips the XSSI prefix from the input string.
   *
   * @param {string} prefix The XSSI prefix.
   * @param {string} string The string to strip the prefix from.
   * @return {string} The input string without the prefix.
   * @private
   */
  _.stripXssiPrefix_ = function(prefix, string) {
    if (goog.string.startsWith(string, prefix)) {
      string = string.substring(prefix.length);
    }
    return string;
  };

  /**
   * Generic error that may occur during a request.
   *
   * @param {string} message The error message.
   * @param {string} url The URL that was being requested.
   * @param {!XMLHttpRequest} xhr The XMLHttpRequest that failed.
   * @extends {goog.debug.Error}
   * @constructor
   */
  _.Error = function(message, url, xhr) {
    goog.base(this, message + ', url=' + url);

    /**
     * The URL that was requested.
     * @type {string}
     */
    this.url = url;

    /**
     * The XMLHttpRequest corresponding with the failed request.
     * @type {!XMLHttpRequest}
     */
    this.xhr = xhr;
  };
  goog.inherits(_.Error, goog.debug.Error);

  /**
   * Class for HTTP errors.
   *
   * @param {number} status The HTTP status code of the response.
   * @param {string} url The URL that was being requested.
   * @param {!XMLHttpRequest} xhr The XMLHttpRequest that failed.
   * @extends {_.Error}
   * @constructor
   */
  _.HttpError = function(status, url, xhr) {
    goog.base(this, 'Request Failed, status=' + status, url, xhr);

    /**
     * The HTTP status code for the error.
     * @type {number}
     */
    this.status = status;
  };
  goog.inherits(_.HttpError, _.Error);

  /**
   * Class for Timeout errors.
   *
   * @param {string} url The URL that timed out.
   * @param {!XMLHttpRequest} xhr The XMLHttpRequest that failed.
   * @extends {_.Error}
   * @constructor
   */
  _.TimeoutError = function(url, xhr) {
    goog.base(this, 'Request timed out', url, xhr);
  };
  goog.inherits(_.TimeoutError, _.Error);

});
<commit_msg><commit_after>// Copyright 2011 The Closure Library 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.

/**
 * @fileoverview Offered as an alternative to XhrIo as a way for making requests
 * via XMLHttpRequest.  Instead of mirroring the XHR interface and exposing
 * events, deferreds are used as a way to pass a "promise" of the response to
 * interested parties.
 *
 */

goog.provide('goog.labs.net.xhr');
goog.provide('goog.labs.net.xhr.Error');
goog.provide('goog.labs.net.xhr.HttpError');
goog.provide('goog.labs.net.xhr.TimeoutError');

goog.require('goog.async.Deferred');
goog.require('goog.debug.Error');
goog.require('goog.json');
goog.require('goog.net.HttpStatus');
goog.require('goog.net.XmlHttp');
goog.require('goog.string');
goog.require('goog.uri.utils');

goog.scope(function() {
  var _ = goog.labs.net.xhr;
  var Deferred = goog.async.Deferred;
  var HttpStatus = goog.net.HttpStatus;

  /**
   * Configuration options for an XMLHttpRequest.
   * - headers: map of header key/value pairs.
   * - timeoutMs: number of milliseconds after which the request will be timed
   *      out by the client. Default is to allow the browser to handle timeouts.
   * - withCredentials: whether user credentials are to be included in a
   *      cross-origin request.  See:
   *      http://dev.w3.org/2006/webapi/XMLHttpRequest-2/#the-withcredentials-attribute
   * - mimeType: allows the caller to override the content-type and charset for
   *      the request, which is useful when requesting binary data.  See:
   *      http://dev.w3.org/2006/webapi/XMLHttpRequest-2/#dom-xmlhttprequest-overridemimetype
   * - xssiPrefix: Prefix used for protecting against XSSI attacks, which should
   *      be removed before parsing the response as JSON.
   *
   * @typedef {{
   *   headers: (Object.<string>|undefined),
   *   timeoutMs: (number|undefined),
   *   withCredentials: (boolean|undefined),
   *   mimeType: (string|undefined),
   *   xssiPrefix: (string|undefined)
   * }}
   */
  _.Options;

  /**
   * Defines the types that are allowed as post data.
   * @typedef {(ArrayBuffer|Blob|Document|FormData|null|string|undefined)}
   */
  _.PostData;

  /**
   * The Content-Type HTTP header name.
   * @type {string}
   */
  _.CONTENT_TYPE_HEADER = 'Content-Type';

  /**
   * The Content-Type HTTP header value for a url-encoded form.
   * @type {string}
   */
  _.FORM_CONTENT_TYPE = 'application/x-www-form-urlencoded;charset=utf-8';

  /**
   * Sends a get request, returning a deferred which will be called back with
   * the response text once the request completes.
   *
   * @param {string} url The URL to request.
   * @param {_.Options=} opt_options Configuration options for the request.
   * @return {!Deferred} A deferred object that will be called back
   *     with the response text once the request finishes.
   */
  _.get = function(url, opt_options) {
    return _.send('GET', url, null, opt_options).
        addCallback(_.getResponseText_);
  };

  /**
   * Sends a post request, returning a deferred which will be called back with
   * the response text once the request completes.
   *
   * @param {string} url The URL to request.
   * @param {_.PostData} data The body of the post request.
   * @param {_.Options=} opt_options Configuration options for the request.
   * @return {!Deferred} A deferred object that will be called back
   *     with the response text once the request finishes.
   */
  _.post = function(url, data, opt_options) {
    return _.send('POST', url, data, opt_options).
        addCallback(_.getResponseText_);
  };

  /**
   * Sends a get request, returning a deferred which will be called back with
   * the parsed response text once the request completes.
   *
   * @param {string} url The URL to request.
   * @param {_.Options=} opt_options Configuration options for the request.
   * @return {!Deferred} A deferred object that will be called back
   *     with the response JSON once the request finishes.
   */
  _.getJson = function(url, opt_options) {
    var d = _.send('GET', url, null, opt_options);
    _.addJsonParsingCallbacks_(d, opt_options);
    return d;
  };

  /**
   * Sends a post request, returning a deferred which will be called back with
   * the parsed response text once the request completes.
   *
   * @param {string} url The URL to request.
   * @param {_.PostData} data The body of the post request.
   * @param {_.Options=} opt_options Configuration options for the request.
   * @return {!Deferred} A deferred object that will be called back
   *     with the response JSON once the request finishes.
   */
  _.postJson = function(url, data, opt_options) {
    var d = _.send('POST', url, data, opt_options);
    _.addJsonParsingCallbacks_(d, opt_options);
    return d;
  };

  /**
   * Sends a request using XMLHttpRequest and returns a deferred.
   *
   * @param {string} method The HTTP method for the request.
   * @param {string} url The URL to request.
   * @param {_.PostData} data The body of the post request.
   * @param {_.Options=} opt_options Configuration options for the request.
   * @return {!Deferred} A deferred object that will be called back
   *     with the XHR object when the request finishes.
   */
  _.send = function(method, url, data, opt_options) {

    // When the deferred is cancelled, we abort the XHR.  We want to make sure
    // the readystatechange event still fires, so it can do the timeout
    // cleanup, however we don't want the callback or errback to be called
    // again.  Thus the slight ugliness here.  If deferreds were pushed into
    // makeRequest, this could become a lot cleaner but we want an option for
    // people not to include goog.async.Deferred.

    function cancel() {
      cancelled = true;
      xhr.abort();
      xhr.onreadystatechange = goog.nullFunction;
      // TODO(user): We could return an AbortedError if we wanted to.
    }

    function callback(data) {
      if (!cancelled) {
        d.callback(data);
      }
    }

    function errback(err) {
      if (!cancelled) {
        d.errback(err);
      }
    }

    var cancelled = false;
    var d = new Deferred(cancel);
    var xhr = _.makeRequest(method, url, data, opt_options, callback, errback);

    return d;
  };

  /**
   * Creates a new XMLHttpRequest and initiates a request.
   *
   * @param {string} method The HTTP method for the request.
   * @param {string} url The URL to request.
   * @param {_.PostData} data The body of the post request, unless the content
   *    type is explicitly set in the Options, then it will default to form
   *    urlencoded.
   * @param {_.Options=} opt_options Configuration options for the request.
   * @param {function(XMLHttpRequest)=} opt_callback Optional callback to call
   *     when the request completes.
   * @param {function(Error)=} opt_errback Optional callback to call
   *     when there is an error.
   * @return {!XMLHttpRequest} The new XMLHttpRequest.
   */
  _.makeRequest = function(
      method, url, data, opt_options, opt_callback, opt_errback) {
    var options = opt_options || {};
    var callback = opt_callback || goog.nullFunction;
    var errback = opt_errback || goog.nullFunction;
    var timer;

    var xhr = /** @type {!XMLHttpRequest} */ (goog.net.XmlHttp());
    try {
      xhr.open(method, url, true);
    } catch (e) {
      // XMLHttpRequest.open may throw when 'open' is called, for example, IE7
      // throws "Access Denied" for cross-origin requests.
      errback(new _.Error('Error opening XHR: ' + e.message, url, xhr));
      return xhr;
    }

    // So sad that IE doesn't support onload and onerror.
    xhr.onreadystatechange = function() {
      if (xhr.readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
        window.clearTimeout(timer);
        if (HttpStatus.isSuccess(xhr.status) ||
            xhr.status === 0 && !_.isEffectiveSchemeHttp_(url)) {
          callback(xhr);
        } else {
          errback(new _.HttpError(xhr.status, url, xhr));
        }
      }
    };

    // Set the headers.
    var contentTypeIsSet = false;
    if (options.headers) {
      for (var key in options.headers) {
        xhr.setRequestHeader(key, options.headers[key]);
      }
      contentTypeIsSet = _.CONTENT_TYPE_HEADER in options.headers;
    }

    // If a content type hasn't been set, default to form-urlencoded/UTF8 for
    // POSTs.  This is because some proxies have been known to reject posts
    // without a content-type.
    if (method == 'POST' && !contentTypeIsSet) {
      xhr.setRequestHeader(_.CONTENT_TYPE_HEADER, _.FORM_CONTENT_TYPE);
    }

    // Set whether to pass cookies on cross-domain requests (if applicable).
    // @see http://dev.w3.org/2006/webapi/XMLHttpRequest-2/#the-withcredentials-attribute
    if (options.withCredentials) {
      xhr.withCredentials = options.withCredentials;
    }

    // Allow the request to override the mime type, useful for getting binary
    // data from the server.  e.g. 'text/plain; charset=x-user-defined'.
    // @see http://dev.w3.org/2006/webapi/XMLHttpRequest-2/#dom-xmlhttprequest-overridemimetype
    if (options.mimeType) {
      xhr.overrideMimeType(options.mimeType);
    }

    // Handle timeouts, if requested.
    if (options.timeoutMs > 0) {
      timer = window.setTimeout(function() {
        // Clear event listener before aborting so the errback will not be
        // called twice.
        xhr.onreadystatechange = goog.nullFunction;
        xhr.abort();
        errback(new _.TimeoutError(url, xhr));
      }, options.timeoutMs);
    }

    // Trigger the send.
    try {
      xhr.send(data);
    } catch (e) {
      // XMLHttpRequest.send is known to throw on some versions of FF, for example
      // if a cross-origin request is disallowed.
      errback(new _.Error('Error sending XHR: ' + e.message, url, xhr));
    }

    return xhr;
  };

  /**
   * @param {string} url The URL to test.
   * @return {boolean} Whether the effective scheme is HTTP or HTTPs.
   * @private
   */
  _.isEffectiveSchemeHttp_ = function(url) {
    var scheme = goog.uri.utils.getEffectiveScheme(url);
    // NOTE(user): Empty-string is for the case under FF3.5 when the location
    // is not defined inside a web worker.
    return scheme == 'http' || scheme == 'https' || scheme == '';
  };

  /**
   * Returns the response text of an XHR object.  Intended for use as a callback
   * on a deferred.
   *
   * @param {!XMLHttpRequest} xhr The XHR object.
   * @return {string} The response text.
   * @private
   */
  _.getResponseText_ = function(xhr) {
    return xhr.responseText;
  };

  /**
   * Adds the callbacks required for parsing a response as JSON.  The chain will
   * expect an XMLHttpRequest and will result in a JS object.  Parse errors will
   * trigger the errback chain.
   *
   * @param {!Deferred} d The deferred to add callbacks to.
   * @param {_.Options|undefined} options The options object.
   * @private
   */
  _.addJsonParsingCallbacks_ = function(d, options) {
    d.addCallback(_.getResponseText_);
    if (options && options.xssiPrefix) {
      d.addCallback(goog.partial(_.stripXssiPrefix_, options.xssiPrefix));
    }
    d.addCallback(goog.json.parse);
  };

  /**
   * Strips the XSSI prefix from the input string.
   *
   * @param {string} prefix The XSSI prefix.
   * @param {string} string The string to strip the prefix from.
   * @return {string} The input string without the prefix.
   * @private
   */
  _.stripXssiPrefix_ = function(prefix, string) {
    if (goog.string.startsWith(string, prefix)) {
      string = string.substring(prefix.length);
    }
    return string;
  };

  /**
   * Generic error that may occur during a request.
   *
   * @param {string} message The error message.
   * @param {string} url The URL that was being requested.
   * @param {!XMLHttpRequest} xhr The XMLHttpRequest that failed.
   * @extends {goog.debug.Error}
   * @constructor
   */
  _.Error = function(message, url, xhr) {
    goog.base(this, message + ', url=' + url);

    /**
     * The URL that was requested.
     * @type {string}
     */
    this.url = url;

    /**
     * The XMLHttpRequest corresponding with the failed request.
     * @type {!XMLHttpRequest}
     */
    this.xhr = xhr;
  };
  goog.inherits(_.Error, goog.debug.Error);

  /** @override */
  _.Error.prototype.name = 'XhrError';

  /**
   * Class for HTTP errors.
   *
   * @param {number} status The HTTP status code of the response.
   * @param {string} url The URL that was being requested.
   * @param {!XMLHttpRequest} xhr The XMLHttpRequest that failed.
   * @extends {_.Error}
   * @constructor
   */
  _.HttpError = function(status, url, xhr) {
    goog.base(this, 'Request Failed, status=' + status, url, xhr);

    /**
     * The HTTP status code for the error.
     * @type {number}
     */
    this.status = status;
  };
  goog.inherits(_.HttpError, _.Error);

  /** @override */
  _.HttpError.prototype.name = 'XhrHttpError';

  /**
   * Class for Timeout errors.
   *
   * @param {string} url The URL that timed out.
   * @param {!XMLHttpRequest} xhr The XMLHttpRequest that failed.
   * @extends {_.Error}
   * @constructor
   */
  _.TimeoutError = function(url, xhr) {
    goog.base(this, 'Request timed out', url, xhr);
  };
  goog.inherits(_.TimeoutError, _.Error);

  /** @override */
  _.TimeoutError.prototype.name = 'XhrTimeoutError';

});
<|endoftext|>


<commit_before>import React from 'react';
import CollectionDetails from './CollectionDetails';
import { shallow } from 'enzyme';

console.error = () => {};

jest.mock('../../../utilities/log', () => {
    return {
        add: function() {
            //
        },
        eventTypes: {}
    }
});

jest.mock('../../../utilities/notifications', () => {
    return {
        add: jest.fn(() => {
            //
        })
    }
});

const defaultProps = {
    collectionID: 'test-collection-12345',
    activePageID: 'economy-grossdomesticproduct-bulletins-gdp-2014',
    name: 'Test collection',
    onClose: () => {},
    onPageClick: () => {},
    onEditPageClick: () => {},
    isLoadingDetails: null,
    inProgress: [],
    complete: [],
    reviewed: []
};

const alternativePageProps = {
    inProgress: [
            {
                uri: "/",
                type: "homepage",
                description: {
                    title: "Home"
                },
                lastEdit: {
                    email: "foobar@email.com",
                    date: "2017-12-14T11:36:03.402Z"
                }

            },
            {
                uri: "/economy/inflationsandprices/consumerinflation/bulletins/consumerpriceinflation/july2017",
                type: "bulletin",
                description: {
                    title: "Consumer Price Inflation",
                    edition: "July 2017"
                },
                lastEdit: {
                    email: "foobar@email.com",
                    date: "2017-12-14T11:36:03.402Z"
                }
            }
        ],
        complete: [
            {
                uri: "/businessindustryandtrade",
                type: "taxonomy_landing_page",
                description: {
                    title: "Business industry and trade"
                },
                lastEdit: {
                    email: "foobar@email.com",
                    date: "2017-12-14T11:36:03.402Z"
                }
            }
        ],
        reviewed: null
}

describe("Collection details page edit/delete buttons only show for an active page", () => {
    const props = {
        ...defaultProps,
        ...alternativePageProps
    };
    const component = shallow(
        <CollectionDetails {...props} />
    );

    it("render the correct number of in progress pages", () => {
        const pages = component.find('.list__item--expandable');
        expect(pages.length).toBe(3);
    });

    it("render the correct number of awaiting review pages", () => {
        const pages = component.find('.list__item--expandable');
        expect(pages.length).toBe(3);
    });

    it("render the correct number of reviewed pages", () => {
        const pages = component.find('.list__item--expandable');
        expect(pages.length).toBe(3);
    });

    it("buttons will hide for inactive pages", () => {
        const activePages = component.find('.list__item--expandable.active');
        const pages = component.find('.list__item--expandable');
        expect(pages.length).toBe(3);
        expect(activePages.length).toBe(0);
    });

    it("buttons will show for active pages", () => {
        component.setProps({
            activePageURI: "/economy/inflationsandprices/consumerinflation/bulletins/consumerpriceinflation/july2017"
        });
        const activePages = component.find('.list__item--expandable.active');
        expect(activePages.length).toBe(1);
        expect(activePages.key()).toBe("/economy/inflationsandprices/consumerinflation/bulletins/consumerpriceinflation/july2017");
    });
});

describe("When the collections details are loading", () => {
    const props = {
        ...defaultProps,
        isLoadingDetails: true
    };
    const component = shallow(
        <CollectionDetails {...props} />
    );

    it("show a loading icon", () => {
        expect(component.find('.loader').length).toBe(1);
    });

    it("doesn't try to render any pages", () => {
        expect(component.find('.page').length).toBe(0);
    });
});

describe("Number of pages in a state are rendered correctly", () => {
    const props = {
        ...defaultProps,
        ...alternativePageProps,
        reviewed: []
    };
    const component = shallow(
        <CollectionDetails {...props} />
    );

    it("for pages in progress", () => {
        expect(component.instance().statePageCount('inProgress')).toBe("2");
        expect(component.instance().renderPluralisedPageText('inProgress')).toBe("pages");
    });

    it("for pages awaiting review", () => {
        expect(component.instance().statePageCount('complete')).toBe("1");
        expect(component.instance().renderPluralisedPageText('complete')).toBe("page");
    });

    it("for pages that have been reviewed", () => {
        expect(component.instance().statePageCount('reviewed')).toBe("0");
        expect(component.instance().renderPluralisedPageText('reviewed')).toBe("pages");
    });
});

describe("Invalid props doesn't break the component", () => {
    const props = {
        ...defaultProps,
        inProgress: null,
        complete: null,
        reviewed: null
    }
    const component = shallow(
        <CollectionDetails {...props} />
    )

    it("'null' properties for page state arrays", () => {
        expect(component.find('h3').length).toBe(3);
    });

    it("missing 'activePageID'", () => {
        component.setProps({...alternativePageProps});
        expect(component.find('.list__item--expandable').length).toBe(3);
    });
});

describe("'Last edit' information for a page in a collection", () => {
    const component = shallow(
        <CollectionDetails {...defaultProps} />
    );

    it("Handles no events being stored for page", () => {
        const brokenEventsProps = {
            ...defaultProps,
            inProgress: [
                ...defaultProps.inProgress,
                {
                    uri: "/economy/page",
                    type: "product_page",
                    description: {
                        title: "A page"
                    }
                }
            ]
        }
        component.setProps({...brokenEventsProps});

        // reset props for futures tests
        component.setProps({...defaultProps});
    });

    it("Excludes the date if the data isn't available", () => {
        const event = {
            email: "foobar@email.com",
            date: ""
        };
        expect(component.instance().renderLastEditText(event)).toBe("Last edit: foobar@email.com (date not available)");

        delete event.date;
        expect(component.instance().renderLastEditText(event)).toBe("Last edit: foobar@email.com (date not available)");
    });

    it("Excludes the date if it isn't a valid date", () => {
        const event = {
            email: "foobar@email.com",
            date: "not a valid date"
        };
        expect(component.instance().renderLastEditText(event)).toBe("Last edit: foobar@email.com (date not available)");
    });

    it("Excludes the email if the data isn't available", () => {
        const event = {
            email: "",
            date: "2017-12-14T11:36:03.402Z"
        };
        expect(component.instance().renderLastEditText(event)).toBe("Last edit: email not available (Thu 14 Dec 2017 - 11:36:03)");

        delete event.email;
        expect(component.instance().renderLastEditText(event)).toBe("Last edit: email not available (Thu 14 Dec 2017 - 11:36:03)");
    });

    it("Renders an error message if neither the email or date are available", () => {
        const event = {};
        expect(component.instance().renderLastEditText(event)).toBe("Error getting 'last edit' details");
        expect(component.instance().renderLastEditText()).toBe("Error getting 'last edit' details");
    })

    it("Renders an error message if it isn't a valid date and no email is available", () => {
        const event = {
            email: "",
            date: "not a valid date"
        };
        expect(component.instance().renderLastEditText(event)).toBe("Error rendering 'last edit' details");
    });

});

describe("Approve collection button", () => {
    const component = shallow(
        <CollectionDetails {...defaultProps}/>
    );

    it("doesn't show when canBeApproved is false", () => {
        component.setProps({canBeApproved: false});
        expect(component.find('#approve-collection').exists()).toEqual(false);
    });

    it("shows when canBeApproved is true", () => {
        component.setProps({canBeApproved: true});
        expect(component.find('#approve-collection').exists()).toEqual(true);
    });

    it("doesn't show when the status is set to neutral", () => {
        component.setProps({
            canBeApproved: true,
            status: {neutral: true}
        });
        expect(component.find('#approve-collection').exists()).toEqual(false);
    });

    it("doesn't show when the status is set to warning", () => {
        component.setProps({
            canBeApproved: true,
            status: {warning: true}
        });
        expect(component.find('#approve-collection').exists()).toEqual(false);
    });
});

describe("Delete collection button", () => {
    const component = shallow(
        <CollectionDetails {...defaultProps}/>
    );

    it("doesn't show when canBeDeleted is false", () => {
        component.setProps({canBeDeleted: false});
        expect(component.find('#delete-collection').exists()).toEqual(false);
    });

    it("shows when canBeDeleted is true", () => {
        component.setProps({canBeDeleted: true});
        expect(component.find('#delete-collection').exists()).toEqual(true);
    });

    it("doesn't show when the status is set to neutral", () => {
        component.setProps({
            canBeDeleted: true,
            status: {neutral: true}
        });
        expect(component.find('#delete-collection').exists()).toEqual(false);
    });

    it("doesn't show when the status is set to warning", () => {
        component.setProps({
            canBeDeleted: true,
            status: {warning: true}
        });
        expect(component.find('#delete-collection').exists()).toEqual(false);
    });
});

<commit_msg>Import module instead of default as default contains connect wrapper<commit_after>import React from 'react';
import { CollectionDetails } from './CollectionDetails';
import { shallow } from 'enzyme';

console.error = () => {};

jest.mock('../../../utilities/log', () => {
    return {
        add: function() {
            //
        },
        eventTypes: {}
    }
});

jest.mock('../../../utilities/notifications', () => {
    return {
        add: jest.fn(() => {
            //
        })
    }
});

const defaultProps = {
    collectionID: 'test-collection-12345',
    activePageID: 'economy-grossdomesticproduct-bulletins-gdp-2014',
    name: 'Test collection',
    onClose: () => {},
    onPageClick: () => {},
    onEditPageClick: () => {},
    isLoadingDetails: null,
    inProgress: [],
    complete: [],
    reviewed: []
};

const alternativePageProps = {
    inProgress: [
            {
                uri: "/",
                type: "homepage",
                description: {
                    title: "Home"
                },
                lastEdit: {
                    email: "foobar@email.com",
                    date: "2017-12-14T11:36:03.402Z"
                }

            },
            {
                uri: "/economy/inflationsandprices/consumerinflation/bulletins/consumerpriceinflation/july2017",
                type: "bulletin",
                description: {
                    title: "Consumer Price Inflation",
                    edition: "July 2017"
                },
                lastEdit: {
                    email: "foobar@email.com",
                    date: "2017-12-14T11:36:03.402Z"
                }
            }
        ],
        complete: [
            {
                uri: "/businessindustryandtrade",
                type: "taxonomy_landing_page",
                description: {
                    title: "Business industry and trade"
                },
                lastEdit: {
                    email: "foobar@email.com",
                    date: "2017-12-14T11:36:03.402Z"
                }
            }
        ],
        reviewed: null
}

describe("Collection details page edit/delete buttons only show for an active page", () => {
    const props = {
        ...defaultProps,
        ...alternativePageProps
    };
    const component = shallow(
        <CollectionDetails {...props} />
    );

    it("render the correct number of in progress pages", () => {
        const pages = component.find('.list__item--expandable');
        expect(pages.length).toBe(3);
    });

    it("render the correct number of awaiting review pages", () => {
        const pages = component.find('.list__item--expandable');
        expect(pages.length).toBe(3);
    });

    it("render the correct number of reviewed pages", () => {
        const pages = component.find('.list__item--expandable');
        expect(pages.length).toBe(3);
    });

    it("buttons will hide for inactive pages", () => {
        const activePages = component.find('.list__item--expandable.active');
        const pages = component.find('.list__item--expandable');
        expect(pages.length).toBe(3);
        expect(activePages.length).toBe(0);
    });

    it("buttons will show for active pages", () => {
        component.setProps({
            activePageURI: "/economy/inflationsandprices/consumerinflation/bulletins/consumerpriceinflation/july2017"
        });
        const activePages = component.find('.list__item--expandable.active');
        expect(activePages.length).toBe(1);
        expect(activePages.key()).toBe("/economy/inflationsandprices/consumerinflation/bulletins/consumerpriceinflation/july2017");
    });
});

describe("When the collections details are loading", () => {
    const props = {
        ...defaultProps,
        isLoadingDetails: true
    };
    const component = shallow(
        <CollectionDetails {...props} />
    );

    it("show a loading icon", () => {
        expect(component.find('.loader').length).toBe(1);
    });

    it("doesn't try to render any pages", () => {
        expect(component.find('.page').length).toBe(0);
    });
});

describe("Number of pages in a state are rendered correctly", () => {
    const props = {
        ...defaultProps,
        ...alternativePageProps,
        reviewed: []
    };
    const component = shallow(
        <CollectionDetails {...props} />
    );

    it("for pages in progress", () => {
        expect(component.instance().statePageCount('inProgress')).toBe("2");
        expect(component.instance().renderPluralisedPageText('inProgress')).toBe("pages");
    });

    it("for pages awaiting review", () => {
        expect(component.instance().statePageCount('complete')).toBe("1");
        expect(component.instance().renderPluralisedPageText('complete')).toBe("page");
    });

    it("for pages that have been reviewed", () => {
        expect(component.instance().statePageCount('reviewed')).toBe("0");
        expect(component.instance().renderPluralisedPageText('reviewed')).toBe("pages");
    });
});

describe("Invalid props doesn't break the component", () => {
    const props = {
        ...defaultProps,
        inProgress: null,
        complete: null,
        reviewed: null
    }
    const component = shallow(
        <CollectionDetails {...props} />
    )

    it("'null' properties for page state arrays", () => {
        expect(component.find('h3').length).toBe(3);
    });

    it("missing 'activePageID'", () => {
        component.setProps({...alternativePageProps});
        expect(component.find('.list__item--expandable').length).toBe(3);
    });
});

describe("'Last edit' information for a page in a collection", () => {
    const component = shallow(
        <CollectionDetails {...defaultProps} />
    );

    it("Handles no events being stored for page", () => {
        const brokenEventsProps = {
            ...defaultProps,
            inProgress: [
                ...defaultProps.inProgress,
                {
                    uri: "/economy/page",
                    type: "product_page",
                    description: {
                        title: "A page"
                    }
                }
            ]
        }
        component.setProps({...brokenEventsProps});

        // reset props for futures tests
        component.setProps({...defaultProps});
    });

    it("Excludes the date if the data isn't available", () => {
        const event = {
            email: "foobar@email.com",
            date: ""
        };
        expect(component.instance().renderLastEditText(event)).toBe("Last edit: foobar@email.com (date not available)");

        delete event.date;
        expect(component.instance().renderLastEditText(event)).toBe("Last edit: foobar@email.com (date not available)");
    });

    it("Excludes the date if it isn't a valid date", () => {
        const event = {
            email: "foobar@email.com",
            date: "not a valid date"
        };
        expect(component.instance().renderLastEditText(event)).toBe("Last edit: foobar@email.com (date not available)");
    });

    it("Excludes the email if the data isn't available", () => {
        const event = {
            email: "",
            date: "2017-12-14T11:36:03.402Z"
        };
        expect(component.instance().renderLastEditText(event)).toBe("Last edit: email not available (Thu 14 Dec 2017 - 11:36:03)");

        delete event.email;
        expect(component.instance().renderLastEditText(event)).toBe("Last edit: email not available (Thu 14 Dec 2017 - 11:36:03)");
    });

    it("Renders an error message if neither the email or date are available", () => {
        const event = {};
        expect(component.instance().renderLastEditText(event)).toBe("Error getting 'last edit' details");
        expect(component.instance().renderLastEditText()).toBe("Error getting 'last edit' details");
    })

    it("Renders an error message if it isn't a valid date and no email is available", () => {
        const event = {
            email: "",
            date: "not a valid date"
        };
        expect(component.instance().renderLastEditText(event)).toBe("Error rendering 'last edit' details");
    });

});

describe("Approve collection button", () => {
    const component = shallow(
        <CollectionDetails {...defaultProps}/>
    );

    it("doesn't show when canBeApproved is false", () => {
        component.setProps({canBeApproved: false});
        expect(component.find('#approve-collection').exists()).toEqual(false);
    });

    it("shows when canBeApproved is true", () => {
        component.setProps({canBeApproved: true});
        expect(component.find('#approve-collection').exists()).toEqual(true);
    });

    it("doesn't show when the status is set to neutral", () => {
        component.setProps({
            canBeApproved: true,
            status: {neutral: true}
        });
        expect(component.find('#approve-collection').exists()).toEqual(false);
    });

    it("doesn't show when the status is set to warning", () => {
        component.setProps({
            canBeApproved: true,
            status: {warning: true}
        });
        expect(component.find('#approve-collection').exists()).toEqual(false);
    });
});

describe("Delete collection button", () => {
    const component = shallow(
        <CollectionDetails {...defaultProps}/>
    );

    it("doesn't show when canBeDeleted is false", () => {
        component.setProps({canBeDeleted: false});
        expect(component.find('#delete-collection').exists()).toEqual(false);
    });

    it("shows when canBeDeleted is true", () => {
        component.setProps({canBeDeleted: true});
        expect(component.find('#delete-collection').exists()).toEqual(true);
    });

    it("doesn't show when the status is set to neutral", () => {
        component.setProps({
            canBeDeleted: true,
            status: {neutral: true}
        });
        expect(component.find('#delete-collection').exists()).toEqual(false);
    });

    it("doesn't show when the status is set to warning", () => {
        component.setProps({
            canBeDeleted: true,
            status: {warning: true}
        });
        expect(component.find('#delete-collection').exists()).toEqual(false);
    });
});

<|endoftext|>


<commit_before>define('/style/js/biz/list.js', function(require, exports, module) {
	var container = $('#captureList');
	var body = $('#captureListBody');
	var quickSearch = $('#quickSearch');
	var	MAX_COUNT = 720;
	var ids = [];
	var data = {};
	var index = 0;
	var pending;

	var AMP    = /&/g,
	    LT     = /</g,
	    GT     = />/g,
	    QUOT   = /\"/g,
	    SQUOT  = /\'/g;

	function escapeHtml(s) {
	    return typeof s === 'string' ? s.replace(AMP,'&').replace(LT,'<').replace(GT,'>')
	  	      .replace(QUOT,'"').replace(SQUOT, ''') : s;
	}

	function getList(options) {
		if (pending) {
			return;
		}
		pending = true;

		return $.ajax({
			url: '/cgi-bin/list/get',
			dataType: 'json',
			timeout: 5000,
			type: 'post',
			data: options,
			success: handleData,
			complete: function() {
				setTimeout(load, 1000);
			}
		});
	}

	function load() {
		var id;
		var pendingIds = [];
		for (var i = 0, len = ids.length; i < len; i++) {
			id = ids[i];
			var curData = data[id];
			if (curData && !curData.endTime) {
				pendingIds.push(id);
			}
		}
		var startTime = ids.length < MAX_COUNT ? id : -1;

		function _load() {
			pending = false;
			getList({
				ids: pendingIds.join(),
				startTime: startTime
			});
		}

		if (startTime == -1 && !pendingIds.length) {
			setTimeout(_load, 3000);
		} else {
			_load();
		}
	}

	function handleData(json) {
		ids = ids.concat(json.newIds);
		for (var i = 0, len = ids.length; i < len; i++) {
			var id = ids[i];
			var curData = json.data[id];
			if (curData) {
				data[id] = curData;
			}
		}
		createList();
	}

	function createList() {
		var list = body.find('tr');
		list.filter('.pending').each(function() {
			var self = $(this);
			var curData = data[self.attr('id')];
			curData && updateElement(self, curData);
		});

		var last = list.filter(':last');
		var lastId = last.attr('id');
		lastId = lastId ? $.inArray(lastId, ids) + 1 : 0;
		var bottom = atBottom();
		var exceedCount = list.length - MAX_COUNT + (bottom ? 1 : 0);
		if (exceedCount > 0) {
			removeElements(list.slice(0, exceedCount));
		}

		var html = [];
		for (var len = ids.length; lastId < len; lastId++) {
			html.push(getHtml(data[ids[lastId]]));
		}
		body.append(html.join(''));
		html.length && search();
		bottom && container.scrollTop(1000000);
	}

	function getHtml(data) {
		if (!data) {
			return '';
		}
		var res = data.res;
		var req = data.req;
		var defaultValue = data.reqError || data.resError ? 'error' : '-';

		return '
\
			        ' + ++index + '	\
			        ' + (res.statusCode || defaultValue) + '	\
			        ' + getProtocol(data.url) + '	\
			        ' + req.method + '	\
			        ' + getHostname(data.url) + '	\
			        ' + (res.ip || defaultValue) + '	\
			        ' + escapeHtml(data.url) + '	\
			        ' + (res.headers ? (res.headers['content-type'] || '') : defaultValue) + '	\
			        ' + (data.endTime ? data.endTime - data.startTime : defaultValue) + '	\

';
	}

	function updateElement(elem, data) {
		var res = data.res;
		var defaultValue = data.reqError || data.resError ? 'error' : '-';
		elem.find('.result').text(res.statusCode || defaultValue);
		elem.find('.host-ip').text(res.ip || defaultValue);
		elem.find('.type').text(res.headers ? (res.headers['content-type'] || '') : defaultValue);
		elem.find('.time').text(data.endTime ? data.endTime - data.startTime : defaultValue);
		elem.addClass(getClassname(data));
		if (data.endTime) {
			elem.removeClass('pending');
		}
	}

	function getHostname(url) {
		var index = url.indexOf(':\/\/') + 3;
		return url.substring(index, url.indexOf('\/', index));
	}

	function getProtocol(url) {
		return url.substring(0, url.indexOf(':\/\/')).toUpperCase();
	}

	function getClassname(data) {
		if (data.reqError || data.resError || data.res.statusCode >= 400) {
			return 'danger';
		}
		var headers = data.res.headers;
		switch(getContentType(headers)) {
			case 'JS':
				return 'warning';
			case 'CSS':
				return 'info';
			case 'HTML':
				return 'success';
			case 'IMG':
				return 'active';
		}

		return '';
	}

	function getContentType(contentType) {
		if (contentType && typeof contentType != 'string') {
			contentType = contentType['content-type'] || contentType.contentType;
		}

		if (typeof contentType == 'string') {
			contentType = contentType.toLowerCase();
			if (contentType.indexOf('javascript') != -1) {
		        return 'JS';
		    }

			if (contentType.indexOf('css') != -1) {
		        return 'CSS';
		    }

			if (contentType.indexOf('html') != -1) {
		        return 'HTML';
		    }

			if (contentType.indexOf('json') != -1) {
		        return 'JSON';
		    }

			if (contentType.indexOf('text/') != -1) {
		        return 'TEXT';
		    }

			if (contentType.indexOf('image') != -1) {
		        return 'IMG';
		    }
		}

		return null;
	}

	function removeElements(elems) {
		elems.each(function() {
			var id = this.id;
			delete data[id];
			var index = $.inArray(id, ids);
			if (index != -1) {
				ids.splice(index, 1);
			}
		}).remove();
	}

	function atBottom() {
		var conHeight = container[0].offsetHeight + 2;
		var bodyHeight = body[0].offsetHeight;

		return bodyHeight <= conHeight || (bodyHeight - container.scrollTop() < conHeight);
	}

	function search() {
		var rows = body.find('tr');
		var keywords = $.trim(quickSearch.val());
		if (!keywords) {
			rows.show();
			return;
		}
		keywords = keywords.toLowerCase().split(/\s+/g).slice(0, 5);
		rows.each(function() {
			var row = $(this);
			var text = (row.text() || '').toLowerCase();
			contains(text, keywords) ? row.show() : row.hide();
		});
	}

	function contains(text, keywords) {
		return keywords[0] && text.indexOf(keywords[0]) != -1 || (keywords[1] && text.indexOf(keywords[1]) != -1)
			|| (keywords[2] && text.indexOf(keywords[2]) != -1) || (keywords[3] && text.indexOf(keywords[3]) != -1)
			|| (keywords[4] && text.indexOf(keywords[4]) != -1);
	}

	module.exports = function init() {
		getList();
		$('#clearAll').click(function() {
			body.html('');
			data = {};
			ids = [];
			index = 0;
			quickSearch.val('');
		});

		container.on('scroll', function() {
			if (atBottom()) {
				var list = body.find('tr');
				var exceedCount = list.length - MAX_COUNT;
				if (exceedCount >= 0) {
					removeElements(list.slice(0, exceedCount + 1));
				}
			}
		});

		quickSearch.on('input', search);
	};
});<commit_msg>req error<commit_after>define('/style/js/biz/list.js', function(require, exports, module) {
	var container = $('#captureList');
	var body = $('#captureListBody');
	var quickSearch = $('#quickSearch');
	var	MAX_COUNT = 720;
	var ids = [];
	var data = {};
	var index = 0;
	var pending;

	var AMP    = /&/g,
	    LT     = /</g,
	    GT     = />/g,
	    QUOT   = /\"/g,
	    SQUOT  = /\'/g;

	function escapeHtml(s) {
	    return typeof s === 'string' ? s.replace(AMP,'&').replace(LT,'<').replace(GT,'>')
	  	      .replace(QUOT,'"').replace(SQUOT, ''') : s;
	}

	function getList(options) {
		if (pending) {
			return;
		}
		pending = true;

		return $.ajax({
			url: '/cgi-bin/list/get',
			dataType: 'json',
			timeout: 5000,
			type: 'post',
			data: options,
			success: handleData,
			complete: function() {
				setTimeout(load, 1000);
			}
		});
	}

	function load() {
		var id;
		var pendingIds = [];
		for (var i = 0, len = ids.length; i < len; i++) {
			id = ids[i];
			var curData = data[id];
			if (curData && !curData.endTime) {
				pendingIds.push(id);
			}
		}
		var startTime = ids.length < MAX_COUNT ? id : -1;

		function _load() {
			pending = false;
			getList({
				ids: pendingIds.join(),
				startTime: startTime
			});
		}

		if (startTime == -1 && !pendingIds.length) {
			setTimeout(_load, 3000);
		} else {
			_load();
		}
	}

	function handleData(json) {
		ids = ids.concat(json.newIds);
		for (var i = 0, len = ids.length; i < len; i++) {
			var id = ids[i];
			var curData = json.data[id];
			if (curData) {
				data[id] = curData;
			}
		}
		createList();
	}

	function createList() {
		var list = body.find('tr');
		list.filter('.pending').each(function() {
			var self = $(this);
			var curData = data[self.attr('id')];
			curData && updateElement(self, curData);
		});

		var last = list.filter(':last');
		var lastId = last.attr('id');
		lastId = lastId ? $.inArray(lastId, ids) + 1 : 0;
		var bottom = atBottom();
		var exceedCount = list.length - MAX_COUNT + (bottom ? 1 : 0);
		if (exceedCount > 0) {
			removeElements(list.slice(0, exceedCount));
		}

		var html = [];
		for (var len = ids.length; lastId < len; lastId++) {
			html.push(getHtml(data[ids[lastId]]));
		}
		body.append(html.join(''));
		html.length && search();
		bottom && container.scrollTop(1000000);
	}

	function getHtml(data) {
		if (!data) {
			return '';
		}
		var res = data.res;
		var req = data.req;
		var defaultValue = data.reqError ? 'req error' : (data.resError ? 'res error' : '-');

		return '
\
			        ' + ++index + '	\
			        ' + (res.statusCode || defaultValue) + '	\
			        ' + getProtocol(data.url) + '	\
			        ' + req.method + '	\
			        ' + getHostname(data.url) + '	\
			        ' + (res.ip || defaultValue) + '	\
			        ' + escapeHtml(data.url) + '	\
			        ' + (res.headers ? (res.headers['content-type'] || '') : defaultValue) + '	\
			        ' + (data.endTime ? data.endTime - data.startTime : defaultValue) + '	\

';
	}

	function updateElement(elem, data) {
		var res = data.res;
		var defaultValue = data.reqError ? 'req error' : (data.resError ? 'res error' : '-');
		elem.find('.result').text(res.statusCode || defaultValue);
		elem.find('.host-ip').text(res.ip || defaultValue);
		elem.find('.type').text(res.headers ? (res.headers['content-type'] || '') : defaultValue);
		elem.find('.time').text(data.endTime ? data.endTime - data.startTime : defaultValue);
		elem.addClass(getClassname(data));
		if (data.endTime) {
			elem.removeClass('pending');
		}
	}

	function getHostname(url) {
		var index = url.indexOf(':\/\/') + 3;
		return url.substring(index, url.indexOf('\/', index));
	}

	function getProtocol(url) {
		return url.substring(0, url.indexOf(':\/\/')).toUpperCase();
	}

	function getClassname(data) {
		if (data.reqError || data.resError || data.res.statusCode >= 400) {
			return 'danger';
		}
		var headers = data.res.headers;
		switch(getContentType(headers)) {
			case 'JS':
				return 'warning';
			case 'CSS':
				return 'info';
			case 'HTML':
				return 'success';
			case 'IMG':
				return 'active';
		}

		return '';
	}

	function getContentType(contentType) {
		if (contentType && typeof contentType != 'string') {
			contentType = contentType['content-type'] || contentType.contentType;
		}

		if (typeof contentType == 'string') {
			contentType = contentType.toLowerCase();
			if (contentType.indexOf('javascript') != -1) {
		        return 'JS';
		    }

			if (contentType.indexOf('css') != -1) {
		        return 'CSS';
		    }

			if (contentType.indexOf('html') != -1) {
		        return 'HTML';
		    }

			if (contentType.indexOf('json') != -1) {
		        return 'JSON';
		    }

			if (contentType.indexOf('text/') != -1) {
		        return 'TEXT';
		    }

			if (contentType.indexOf('image') != -1) {
		        return 'IMG';
		    }
		}

		return null;
	}

	function removeElements(elems) {
		elems.each(function() {
			var id = this.id;
			delete data[id];
			var index = $.inArray(id, ids);
			if (index != -1) {
				ids.splice(index, 1);
			}
		}).remove();
	}

	function atBottom() {
		var conHeight = container[0].offsetHeight + 2;
		var bodyHeight = body[0].offsetHeight;

		return bodyHeight <= conHeight || (bodyHeight - container.scrollTop() < conHeight);
	}

	function search() {
		var rows = body.find('tr');
		var keywords = $.trim(quickSearch.val());
		if (!keywords) {
			rows.show();
			return;
		}
		keywords = keywords.toLowerCase().split(/\s+/g).slice(0, 5);
		rows.each(function() {
			var row = $(this);
			var text = (row.text() || '').toLowerCase();
			contains(text, keywords) ? row.show() : row.hide();
		});
	}

	function contains(text, keywords) {
		return keywords[0] && text.indexOf(keywords[0]) != -1 || (keywords[1] && text.indexOf(keywords[1]) != -1)
			|| (keywords[2] && text.indexOf(keywords[2]) != -1) || (keywords[3] && text.indexOf(keywords[3]) != -1)
			|| (keywords[4] && text.indexOf(keywords[4]) != -1);
	}

	module.exports = function init() {
		getList();
		$('#clearAll').click(function() {
			body.html('');
			data = {};
			ids = [];
			index = 0;
			quickSearch.val('');
		});

		container.on('scroll', function() {
			if (atBottom()) {
				var list = body.find('tr');
				var exceedCount = list.length - MAX_COUNT;
				if (exceedCount >= 0) {
					removeElements(list.slice(0, exceedCount + 1));
				}
			}
		});

		quickSearch.on('input', search);
	};
});<|endoftext|>


<commit_before>angular.module('feedbackcontrol', [])
.directive('transform', function(FeedbackSignal) {
    return {
        restrict: 'A'
      , link: function(scope, element, attrs) {
            $(element).bind('click', function(e) {
                var currentUrl = window.location.pathname;
                var me = $(element);
                var data = FeedbackSignal.data;

                if(currentUrl.match(/published|contacts/g)) {

                    if(me.attr('return-policy') == 1) {
                        hide_the_children(me);
                    }

                    if(data.status != "fileas") {
                        if(data.status == "feature") {
                            var state = {
                                activate: {'background-position': '-64px -31px'}
                              , deactivate_sibling: {'background-position': '-32px 0px'}
                              , activation_color: {'background-color': '#FFFFE0'}
                              , state_change_inbox: '{"status": "inbox", "id": ' + data.id + ', "catid": ' + data.catid + ', "origin": "publish"}'
                              , state_change: '{"status": "publish", "id": ' + data.id + ', "catid": ' + data.catid + ', "origin": "publish"}'
                            }
                            published_state(me, '.publish', 'Publish Feedback', state);
                        }

                        if(data.status == "publish") {
                            var state = {
                                activate: {'background-position': '-32px -31px'}
                              , deactivate_sibling: {'background-position': '-64px 0px'}
                              , activation_color: {'background-color': '#FFF'}
                              , state_change_inbox: '{"status": "inbox", "id": ' + data.id + ', "catid": ' + data.catid + ', "origin": "publish"}'
                              , state_change: '{"status": "feature", "id": ' + data.id + ', "catid": ' + data.catid + ', "origin": "publish"}'
                            }
                            published_state(me, '.feature', 'Feature Feedback', state);
                        }
                    } else {
                        hide_the_children(me);
                    }

                } else if(currentUrl.match(/filed/g)) {

                    var data_status = data.status;

                    if(data_status == "publish" || data_status == "feature" || data_status == "delete") {
                        hide_the_children(me);
                    }

                } else {
                    hide_the_children(me);
                }

                e.preventDefault();
            });
        }
    }
})
.directive('undo', function(FeedbackSignal, FeedbackControlService) {
    return {
        restrict: 'A'
      , link: function(scope, element, attrs) {
            $(element).bind("click", function(e) {
                $(".checky-box-container").hide();
                if (FeedbackSignal.data.id instanceof Array) {
                    for(var i=0; i < FeedbackSignal.data.id.length ; i++) {
                        var feedback = $(".dashboard-feedback[feedback=" + FeedbackSignal.data.id[i] + "]");
                        feedback.show();
                        $(feedback).parents('.feedback-group').show();
                    }
                } else {
                    var feedback = $(".dashboard-feedback[feedback=" + FeedbackSignal.data.id + "]");
                    feedback.show();
                    $(feedback).parents('.feedback-group').show();
                }

                FeedbackControlService.expunge();
                e.preventDefault();
            });
        }
    }
})
.directive('close', function(FeedbackControlService) {
    return {
        restrict: 'A'
      , link: function(scope, element, attrs) {
            $(element).bind("click", function(e) {
                $(".checky-box-container").hide();
                FeedbackControlService.expunge();
                e.preventDefault();
            });
        }
    }
})
.directive('feedcheckbox', function() {
    return {
        restrict: 'A'
      , link: function(scope, element, attrs) {
            var checkbox = $(element);

            checkbox.bind('change', function() {
                if(checkbox.is(":checked")) {
                    checkbox.parents('.dashboard-feedback').css({'background-color': '#F7F7F7'});
                } else {
                    checkbox.parents('.dashboard-feedback').css({'background-color': '#FFF'});
                }
            });

        }
    }
})
.directive('statuschange', function(FeedbackService) {
    return {
        restrict: 'A'
      , scope: {
            feedid: "=feedid"
        }
      , link: function(scope, element, attrs) {
            $(element).bind('change', function() {
                FeedbackService.inline_change($(this).val(), scope.feedid, 'status');
            })
        }
    }
})
.directive('prioritychange', function(FeedbackService) {
    return {
        restrict: 'A'
      , scope: {
            feedid: "=feedid"
        }
      , link: function(scope, element, attrs) {
            $(element).bind('change', function() {
                FeedbackService.inline_change($(this).val(), scope.feedid, 'priority');
            })
        }
    }
})
.directive('deleteBlock', function() {
    return {
        restrict: 'A'
      , link: function(scope, element, attrs) {
            $(element).bind('click', function(e) {
                $(this).parents('.custom-att').fadeOut(400, function() {
                     var new_uploaded_images = new Array;
                     var new_attached_link;

                     var div = $(this).parents('.uploaded-images-and-links');
                     var images = div.find('.uploaded_image');
                     var media_attachment = div.find('.attached_link');

                     $(this).remove();

                     var remove_images = {
                        'small_url'    :$(this).find('.small-image-url').val(),
                        'medium_url'   :$(this).find('.medium-image-url').val(),
                        'large_url'    :$(this).find('.large-image-url').val()
                     }

                    if(images.length > 0){
                        var us = images.not(":hidden");
                        for(var i=0; i < us.length; i++) {
                            new_uploaded_images.push({
                                'name': $(us[i]).find('.image-name').val()
                            });
                        }
                    }

                    if(media_attachment.length > 0){
                        new_attached_link = {
                            title           : media_attachment.find('.link-title').val(),
                            description     : media_attachment.find('.link-description').val(),
                            image           : media_attachment.find('.link-image').val(),
                            url             : media_attachment.find('.link-url').val(),
                            video           : media_attachment.find('.link-video').val(),
                        }
                    };

                    var remaining = {
                        uploaded_images : new_uploaded_images
                      , attached_link   : new_attached_link
                    }

                    var attachment_data = {
                        feedbackId      : div.find('.attachment_feedback_id').val()
                      , attachments     : remaining
                      , remove_images   : remove_images
                    }

                    console.log(media_attachment.length);
                    console.log(media_attachment);
                    /*
                    $.ajax({
                        type: "POST"
                      , url: "/inbox/update_feedback_attachment"
                      , dataType: "json"
                      , data: attachment_data
                    });
                    */
                });
                e.preventDefault();
            })
        }
    }
})

function published_state(obj, sibling_id, msg, state) {
    obj.parents('.dashboard-feedback').css(state.activation_color);
    obj.css(state.activate);
    obj.children('.action-tooltip').children('span').html("Return to Inbox");
    obj.siblings(sibling_id).css(state.deactivate_sibling);
    obj.siblings(sibling_id).children('.action-tooltip').children('span').html(msg);
    obj.siblings(sibling_id).attr('return-policy', 0);
    obj.siblings(sibling_id).attr('data-feed', state.state_change);
    obj.attr('return-policy', 1);
    obj.attr('data-feed', state.state_change_inbox);
}

function hide_the_children(obj) {
    obj.parents('.dashboard-feedback').hide();
    var child_count = obj.parents('.feedback-group').children('.dashboard-feedback:visible');
    if(child_count.length == 0) {
        obj.parents('.feedback-group').hide();
    }

    $(".checky-box-container").show();
}
<commit_msg>fancy word for a fancy man<commit_after>angular.module('feedbackcontrol', [])
.directive('transform', function(FeedbackSignal) {
    return {
        restrict: 'A'
      , link: function(scope, element, attrs) {
            $(element).bind('click', function(e) {
                var currentUrl = window.location.pathname;
                var me = $(element);
                var data = FeedbackSignal.data;

                if(currentUrl.match(/published|contacts/g)) {

                    if(me.attr('return-policy') == 1) {
                        hide_the_children(me);
                    }

                    if(data.status != "fileas") {
                        if(data.status == "feature") {
                            var state = {
                                activate: {'background-position': '-64px -31px'}
                              , deactivate_sibling: {'background-position': '-32px 0px'}
                              , activation_color: {'background-color': '#FFFFE0'}
                              , state_change_inbox: '{"status": "inbox", "id": ' + data.id + ', "catid": ' + data.catid + ', "origin": "publish"}'
                              , state_change: '{"status": "publish", "id": ' + data.id + ', "catid": ' + data.catid + ', "origin": "publish"}'
                            }
                            published_state(me, '.publish', 'Publish Feedback', state);
                        }

                        if(data.status == "publish") {
                            var state = {
                                activate: {'background-position': '-32px -31px'}
                              , deactivate_sibling: {'background-position': '-64px 0px'}
                              , activation_color: {'background-color': '#FFF'}
                              , state_change_inbox: '{"status": "inbox", "id": ' + data.id + ', "catid": ' + data.catid + ', "origin": "publish"}'
                              , state_change: '{"status": "feature", "id": ' + data.id + ', "catid": ' + data.catid + ', "origin": "publish"}'
                            }
                            published_state(me, '.feature', 'Feature Feedback', state);
                        }
                    } else {
                        hide_the_children(me);
                    }

                } else if(currentUrl.match(/filed/g)) {

                    var data_status = data.status;

                    if(data_status == "publish" || data_status == "feature" || data_status == "delete") {
                        hide_the_children(me);
                    }

                } else {
                    hide_the_children(me);
                }

                e.preventDefault();
            });
        }
    }
})
.directive('undo', function(FeedbackSignal, FeedbackControlService) {
    return {
        restrict: 'A'
      , link: function(scope, element, attrs) {
            $(element).bind("click", function(e) {
                $(".checky-box-container").hide();
                if (FeedbackSignal.data.id instanceof Array) {
                    for(var i=0; i < FeedbackSignal.data.id.length ; i++) {
                        var feedback = $(".dashboard-feedback[feedback=" + FeedbackSignal.data.id[i] + "]");
                        feedback.show();
                        $(feedback).parents('.feedback-group').show();
                    }
                } else {
                    var feedback = $(".dashboard-feedback[feedback=" + FeedbackSignal.data.id + "]");
                    feedback.show();
                    $(feedback).parents('.feedback-group').show();
                }

                FeedbackControlService.expunge();
                e.preventDefault();
            });
        }
    }
})
.directive('close', function(FeedbackControlService) {
    return {
        restrict: 'A'
      , link: function(scope, element, attrs) {
            $(element).bind("click", function(e) {
                $(".checky-box-container").hide();
                FeedbackControlService.expunge();
                e.preventDefault();
            });
        }
    }
})
.directive('feedcheckbox', function() {
    return {
        restrict: 'A'
      , link: function(scope, element, attrs) {
            var checkbox = $(element);

            checkbox.bind('change', function() {
                if(checkbox.is(":checked")) {
                    checkbox.parents('.dashboard-feedback').css({'background-color': '#F7F7F7'});
                } else {
                    checkbox.parents('.dashboard-feedback').css({'background-color': '#FFF'});
                }
            });

        }
    }
})
.directive('statuschange', function(FeedbackService) {
    return {
        restrict: 'A'
      , scope: {
            feedid: "=feedid"
        }
      , link: function(scope, element, attrs) {
            $(element).bind('change', function() {
                FeedbackService.inline_change($(this).val(), scope.feedid, 'status');
            })
        }
    }
})
.directive('prioritychange', function(FeedbackService) {
    return {
        restrict: 'A'
      , scope: {
            feedid: "=feedid"
        }
      , link: function(scope, element, attrs) {
            $(element).bind('change', function() {
                FeedbackService.inline_change($(this).val(), scope.feedid, 'priority');
            })
        }
    }
})
.directive('deleteBlock', function() {
    return {
        restrict: 'A'
      , link: function(scope, element, attrs) {
            $(element).bind('click', function(e) {
                $(this).parents('.custom-att').fadeOut(400, function() {
                     var new_uploaded_images = new Array;
                     var new_attached_link;

                     var div = $(this).parents('.uploaded-images-and-links');
                     var images = div.find('.uploaded_image');
                     var media_attachment = div.find('.attached_link');

                     //$(this).remove();

                     var remove_images = {
                        'small_url'    :$(this).find('.small-image-url').val(),
                        'medium_url'   :$(this).find('.medium-image-url').val(),
                        'large_url'    :$(this).find('.large-image-url').val()
                     }

                    if(images.length > 0){
                        var us = images.not(":hidden");
                        for(var i=0; i < us.length; i++) {
                            new_uploaded_images.push({
                                'name': $(us[i]).find('.image-name').val()
                            });
                        }
                    }

                    if(media_attachment.length > 0){
                        new_attached_link = {
                            title           : media_attachment.find('.link-title').val(),
                            description     : media_attachment.find('.link-description').val(),
                            image           : media_attachment.find('.link-image').val(),
                            url             : media_attachment.find('.link-url').val(),
                            video           : media_attachment.find('.link-video').val(),
                        }
                    };

                    var remaining = {
                        uploaded_images : new_uploaded_images
                      , attached_link   : new_attached_link
                    }

                    var attachment_data = {
                        feedbackId      : div.find('.attachment_feedback_id').val()
                      , attachments     : remaining
                      , remove_images   : remove_images
                    }

                    console.log(media_attachment.length);
                    console.log(media_attachment);
                    /*
                    $.ajax({
                        type: "POST"
                      , url: "/inbox/update_feedback_attachment"
                      , dataType: "json"
                      , data: attachment_data
                    });
                    */
                });
                e.preventDefault();
            })
        }
    }
})

function published_state(obj, sibling_id, msg, state) {
    obj.parents('.dashboard-feedback').css(state.activation_color);
    obj.css(state.activate);
    obj.children('.action-tooltip').children('span').html("Return to Inbox");
    obj.siblings(sibling_id).css(state.deactivate_sibling);
    obj.siblings(sibling_id).children('.action-tooltip').children('span').html(msg);
    obj.siblings(sibling_id).attr('return-policy', 0);
    obj.siblings(sibling_id).attr('data-feed', state.state_change);
    obj.attr('return-policy', 1);
    obj.attr('data-feed', state.state_change_inbox);
}

function hide_the_children(obj) {
    obj.parents('.dashboard-feedback').hide();
    var child_count = obj.parents('.feedback-group').children('.dashboard-feedback:visible');
    if(child_count.length == 0) {
        obj.parents('.feedback-group').hide();
    }

    $(".checky-box-container").show();
}
<|endoftext|>


<commit_before>var mongoose = require('mongoose')
  , db = require('../lib/database')
  , Tx = require('../models/tx')
  , Address = require('../models/address')
  , AddressTx = require('../models/addresstx')
  , Richlist = require('../models/richlist')
  , Stats = require('../models/stats')
  , settings = require('../lib/settings')
  , fs = require('fs');

var mode = 'update';
var database = 'index';

// displays usage and exits
function usage() {
  console.log('Usage: node scripts/sync.js [database] [mode]');
  console.log('');
  console.log('database: (required)');
  console.log('index [mode] Main index: coin info/stats, transactions & addresses');
  console.log('market       Market data: summaries, orderbooks, trade history & chartdata')
  console.log('');
  console.log('mode: (required for index database only)');
  console.log('update       Updates index from last sync to current block');
  console.log('check        checks index for (and adds) any missing transactions/addresses');
  console.log('reindex      Clears index then resyncs from genesis to current block');
  console.log('');
  console.log('notes:');
  console.log('* \'current block\' is the latest created block when script is executed.');
  console.log('* The market database only supports (& defaults to) reindex mode.');
  console.log('* If check mode finds missing data(ignoring new data since last sync),');
  console.log('  index_timeout in settings.json is set too low.')
  console.log('');
  process.exit(0);
}

// check options
if (process.argv[2] == 'index') {
  if (process.argv.length <3) {
    usage();
  } else {
    switch(process.argv[3])
    {
      case 'update':
        mode = 'update';
        break;
      case 'check':
        mode = 'check';
        break;
      case 'reindex':
        mode = 'reindex';
        break;
      case 'reindex-rich':
        mode = 'reindex-rich';
        break;
      default:
        usage();
    }
  }
} else if (process.argv[2] == 'market'){
  database = 'market';
} else {
  usage();
}

function create_lock(cb) {
  if ( database == 'index' ) {
    var fname = './tmp/' + database + '.pid';
    fs.appendFile(fname, process.pid, function (err) {
      if (err) {
        console.log("Error: unable to create %s", fname);
        process.exit(1);
      } else {
        return cb();
      }
    });
  } else {
    return cb();
  }
}

function remove_lock(cb) {
  if ( database == 'index' ) {
    var fname = './tmp/' + database + '.pid';
    fs.unlink(fname, function (err){
      if(err) {
        console.log("unable to remove lock: %s", fname);
        process.exit(1);
      } else {
        return cb();
      }
    });
  } else {
    return cb();
  }
}

function is_locked(cb) {
  if ( database == 'index' ) {
    var fname = './tmp/' + database + '.pid';
    fs.exists(fname, function (exists){
      if(exists) {
        return cb(true);
      } else {
        return cb(false);
      }
    });
  } else {
    return cb();
  }
}

function exit() {
  remove_lock(function(){
    mongoose.disconnect();
    process.exit(0);
  });
}

var dbString = 'mongodb://' + settings.dbsettings.user;
dbString = dbString + ':' + settings.dbsettings.password;
dbString = dbString + '@' + settings.dbsettings.address;
dbString = dbString + ':' + settings.dbsettings.port;
dbString = dbString + '/' + settings.dbsettings.database;

is_locked(function (exists) {
  if (exists) {
    console.log("Script already running..");
    process.exit(0);
  } else {
    create_lock(function (){
      console.log("script launched with pid: " + process.pid);
      mongoose.connect(dbString, function(err) {
        if (err) {
          console.log('Unable to connect to database: %s', dbString);
          console.log('Aborting');
          exit();
        } else if (database == 'index') {
          db.check_stats(settings.coin, function(exists) {
            if (exists == false) {
              console.log('Run \'npm start\' to create database structures before running this script.');
              exit();
            } else {
              db.update_db(settings.coin, function(stats){
                if (settings.heavy == true) {
                  db.update_heavy(settings.coin, stats.count, 20, function(){

                  });
                }
                if (mode == 'reindex') {
                  Tx.deleteMany({}, function(err) {
                    Address.deleteMany({}, function(err2) {
                      AddressTx.deleteMany({}, function(err3) {
                        Richlist.updateOne({coin: settings.coin}, {
                          received: [],
                          balance: [],
                        }, function(err3) {
                          Stats.updateOne({coin: settings.coin}, {
                            last: 0,
                            count: 0,
                            supply: 0,
                          }, function() {
                            console.log('index cleared (reindex)');
                          });
                          db.update_tx_db(settings.coin, 1, stats.count, settings.update_timeout, function(){
                            db.update_richlist('received', function(){
                              db.update_richlist('balance', function(){
                                db.get_stats(settings.coin, function(nstats){
                                  console.log('reindex complete (block: %s)', nstats.last);
                                  exit();
                                });
                              });
                            });
                          });
                        });
                      });
                    });
                  });
                } else if (mode == 'check') {
                  db.update_tx_db(settings.coin, 1, stats.count, settings.check_timeout, function(){
                    db.get_stats(settings.coin, function(nstats){
                      console.log('check complete (block: %s)', nstats.last);
                      exit();
                    });
                  });
                } else if (mode == 'update') {
                  db.update_tx_db(settings.coin, stats.last, stats.count, settings.update_timeout, function(){
                    db.update_richlist('received', function(){
                      db.update_richlist('balance', function(){
                        db.get_stats(settings.coin, function(nstats){
                          console.log('update complete (block: %s)', nstats.last);
                          exit();
                        });
                      });
                    });
                  });
                } else if (mode == 'reindex-rich') {
                  console.log('update started');
                  db.update_tx_db(settings.coin, stats.last, stats.count, settings.check_timeout, function(){
                    console.log('update finished');
                    db.check_richlist(settings.coin, function(exists){
                      if (exists == true) {
                        console.log('richlist entry found, deleting now..');
                      }
                      db.delete_richlist(settings.coin, function(deleted) {
                        if (deleted == true) {
                          console.log('richlist entry deleted');
                        }
                        db.create_richlist(settings.coin, function() {
                          console.log('richlist created.');
                          db.update_richlist('received', function(){
                            console.log('richlist updated received.');
                            db.update_richlist('balance', function(){
                              console.log('richlist updated balance.');
                              db.get_stats(settings.coin, function(nstats){
                                console.log('update complete (block: %s)', nstats.last);
                                exit();
                              });
                            });
                          });
                        });
                      });
                    });
                  });
                }
              });
            }
          });
        } else {
          //update markets
          var markets = settings.markets.enabled;
          var complete = 0;
          for (var x = 0; x < markets.length; x++) {
            var market = markets[x];
            db.check_market(market, function(mkt, exists) {
              if (exists) {
                db.update_markets_db(mkt, function(err) {
                  if (!err) {
                    console.log('%s market data updated successfully.', mkt);
                    complete++;
                    if (complete == markets.length) {
                      exit();
                    }
                  } else {
                    console.log('%s: %s', mkt, err);
                    complete++;
                    if (complete == markets.length) {
                      exit();
                    }
                  }
                });
              } else {
                console.log('error: entry for %s does not exists in markets db.', mkt);
                complete++;
                if (complete == markets.length) {
                  exit();
                }
              }
            });
          }
        }
      });
    });
  }
});<commit_msg>Sync reindex more verbose<commit_after>var mongoose = require('mongoose')
  , db = require('../lib/database')
  , Tx = require('../models/tx')
  , Address = require('../models/address')
  , AddressTx = require('../models/addresstx')
  , Richlist = require('../models/richlist')
  , Stats = require('../models/stats')
  , settings = require('../lib/settings')
  , fs = require('fs');

var mode = 'update';
var database = 'index';

// displays usage and exits
function usage() {
  console.log('Usage: node scripts/sync.js [database] [mode]');
  console.log('');
  console.log('database: (required)');
  console.log('index [mode] Main index: coin info/stats, transactions & addresses');
  console.log('market       Market data: summaries, orderbooks, trade history & chartdata')
  console.log('');
  console.log('mode: (required for index database only)');
  console.log('update       Updates index from last sync to current block');
  console.log('check        checks index for (and adds) any missing transactions/addresses');
  console.log('reindex      Clears index then resyncs from genesis to current block');
  console.log('');
  console.log('notes:');
  console.log('* \'current block\' is the latest created block when script is executed.');
  console.log('* The market database only supports (& defaults to) reindex mode.');
  console.log('* If check mode finds missing data(ignoring new data since last sync),');
  console.log('  index_timeout in settings.json is set too low.')
  console.log('');
  process.exit(0);
}

// check options
if (process.argv[2] == 'index') {
  if (process.argv.length <3) {
    usage();
  } else {
    switch(process.argv[3])
    {
      case 'update':
        mode = 'update';
        break;
      case 'check':
        mode = 'check';
        break;
      case 'reindex':
        mode = 'reindex';
        break;
      case 'reindex-rich':
        mode = 'reindex-rich';
        break;
      default:
        usage();
    }
  }
} else if (process.argv[2] == 'market'){
  database = 'market';
} else {
  usage();
}

function create_lock(cb) {
  if ( database == 'index' ) {
    var fname = './tmp/' + database + '.pid';
    fs.appendFile(fname, process.pid, function (err) {
      if (err) {
        console.log("Error: unable to create %s", fname);
        process.exit(1);
      } else {
        return cb();
      }
    });
  } else {
    return cb();
  }
}

function remove_lock(cb) {
  if ( database == 'index' ) {
    var fname = './tmp/' + database + '.pid';
    fs.unlink(fname, function (err){
      if(err) {
        console.log("unable to remove lock: %s", fname);
        process.exit(1);
      } else {
        return cb();
      }
    });
  } else {
    return cb();
  }
}

function is_locked(cb) {
  if ( database == 'index' ) {
    var fname = './tmp/' + database + '.pid';
    fs.exists(fname, function (exists){
      if(exists) {
        return cb(true);
      } else {
        return cb(false);
      }
    });
  } else {
    return cb();
  }
}

function exit() {
  remove_lock(function(){
    mongoose.disconnect();
    process.exit(0);
  });
}

var dbString = 'mongodb://' + settings.dbsettings.user;
dbString = dbString + ':' + settings.dbsettings.password;
dbString = dbString + '@' + settings.dbsettings.address;
dbString = dbString + ':' + settings.dbsettings.port;
dbString = dbString + '/' + settings.dbsettings.database;

is_locked(function (exists) {
  if (exists) {
    console.log("Script already running..");
    process.exit(0);
  } else {
    create_lock(function (){
      console.log("script launched with pid: " + process.pid);
      mongoose.connect(dbString, function(err) {
        if (err) {
          console.log('Unable to connect to database: %s', dbString);
          console.log('Aborting');
          exit();
        } else if (database == 'index') {
          db.check_stats(settings.coin, function(exists) {
            if (exists == false) {
              console.log('Run \'npm start\' to create database structures before running this script.');
              exit();
            } else {
              db.update_db(settings.coin, function(stats){
                if (settings.heavy == true) {
                  db.update_heavy(settings.coin, stats.count, 20, function(){

                  });
                }
                if (mode == 'reindex') {
                  Tx.deleteMany({}, function(err) {
                    console.log('TXs cleared.');
                    Address.deleteMany({}, function(err2) {
                      console.log('Addresses cleared.');
                      AddressTx.deleteMany({}, function(err3) {
                        console.log('Address TXs cleared.');
                        Richlist.updateOne({coin: settings.coin}, {
                          received: [],
                          balance: [],
                        }, function(err3) {
                          Stats.updateOne({coin: settings.coin}, {
                            last: 0,
                            count: 0,
                            supply: 0,
                          }, function() {
                            console.log('index cleared (reindex)');
                          });
                          db.update_tx_db(settings.coin, 1, stats.count, settings.update_timeout, function(){
                            db.update_richlist('received', function(){
                              db.update_richlist('balance', function(){
                                db.get_stats(settings.coin, function(nstats){
                                  console.log('reindex complete (block: %s)', nstats.last);
                                  exit();
                                });
                              });
                            });
                          });
                        });
                      });
                    });
                  });
                } else if (mode == 'check') {
                  db.update_tx_db(settings.coin, 1, stats.count, settings.check_timeout, function(){
                    db.get_stats(settings.coin, function(nstats){
                      console.log('check complete (block: %s)', nstats.last);
                      exit();
                    });
                  });
                } else if (mode == 'update') {
                  db.update_tx_db(settings.coin, stats.last, stats.count, settings.update_timeout, function(){
                    db.update_richlist('received', function(){
                      db.update_richlist('balance', function(){
                        db.get_stats(settings.coin, function(nstats){
                          console.log('update complete (block: %s)', nstats.last);
                          exit();
                        });
                      });
                    });
                  });
                } else if (mode == 'reindex-rich') {
                  console.log('update started');
                  db.update_tx_db(settings.coin, stats.last, stats.count, settings.check_timeout, function(){
                    console.log('update finished');
                    db.check_richlist(settings.coin, function(exists){
                      if (exists == true) {
                        console.log('richlist entry found, deleting now..');
                      }
                      db.delete_richlist(settings.coin, function(deleted) {
                        if (deleted == true) {
                          console.log('richlist entry deleted');
                        }
                        db.create_richlist(settings.coin, function() {
                          console.log('richlist created.');
                          db.update_richlist('received', function(){
                            console.log('richlist updated received.');
                            db.update_richlist('balance', function(){
                              console.log('richlist updated balance.');
                              db.get_stats(settings.coin, function(nstats){
                                console.log('update complete (block: %s)', nstats.last);
                                exit();
                              });
                            });
                          });
                        });
                      });
                    });
                  });
                }
              });
            }
          });
        } else {
          //update markets
          var markets = settings.markets.enabled;
          var complete = 0;
          for (var x = 0; x < markets.length; x++) {
            var market = markets[x];
            db.check_market(market, function(mkt, exists) {
              if (exists) {
                db.update_markets_db(mkt, function(err) {
                  if (!err) {
                    console.log('%s market data updated successfully.', mkt);
                    complete++;
                    if (complete == markets.length) {
                      exit();
                    }
                  } else {
                    console.log('%s: %s', mkt, err);
                    complete++;
                    if (complete == markets.length) {
                      exit();
                    }
                  }
                });
              } else {
                console.log('error: entry for %s does not exists in markets db.', mkt);
                complete++;
                if (complete == markets.length) {
                  exit();
                }
              }
            });
          }
        }
      });
    });
  }
});<|endoftext|>