core.js 9.63 KB
/*jshint -W100*/

'use strict';

var assign        = require('object-assign');
var typeOf        = require('type-of');
var each          = require('foreach');
var cheerioExtend = require('./cheerio-extend');
var util          = require('./cheerio/util.js');
var encoding      = require('./encoding');
var client        = require('./client');
var pkg           = require('../package.json');
var browsers      = require('./browsers.json');

/**
 * cheerio-httpcli内部からプロパティを直接更新する際の黒魔術
 */
var propertyUpdater = {
  // 内部的にプロパティを直接更新する際の照合キー
  Key: Math.random().toString(36).substr(2),

  // プロパティ更新時の値を黒魔術で包み込む
  wrap: function (value) {
    return [ this.key, value ];
  },

  // プロパティ更新時の値を黒魔術から取り出す
  unwrap: function (value) {
    if ((value instanceof Array) &&
      value.length === 2 && value[0] === this.key) {
      return value[1];
    }

    util.colorMessage('DEPRECATED', 'direct property update will be refused in the future. use set(key, value)');
    //throw new Error(direct property update is not permitted. use set(key, value)');
    return value;
  }
};

// リクエストヘッダを作り直す
var rebuildHeaders = function (value) {
  var tmp = {};
  var nullKeys = [];
  // リクエストヘッダは小文字に統一する & 値にnullが入っているキーは削除
  each(value, function (val, key) {
    if (value[key] === null) {
      nullKeys.push(key.toLowerCase());
    }
    tmp[key.toLowerCase()] = val;
  });
  each(nullKeys, function (key) {
    delete tmp[key];
  });
  return tmp;
};

// 通常プロパティ作成(property変数内で管理)
var defineNormalProperties = function (cli) {
  /*eslint-disable key-spacing*/
  var property = {
    // リクエストヘッダ
    headers           : { types: [ 'object' ], value: null },
    // タイムアウトまでの時間
    timeout           : { types: [ 'number' ], value: null },
    // gzip転送する/しない
    gzip              : { types: [ 'boolean' ], value: null },
    // Refererを自動設定する/しない
    referer           : { types: [ 'boolean' ], value: null },
    // <meta[http-equiv=refresh]>を検知してリダイレクトする/しない
    followMetaRefresh : { types: [ 'boolean' ], value: null },
    // 受信を許可する最大のサイズ
    maxDataSize       : { types: [ 'number', 'null' ], value: null },
    // XML自動判別を使用しない
    forceHtml         : { types: [ 'boolean' ], value: null },
    // requestモジュールに渡すagentOptions
    agentOptions      : { types: [ 'object' ], value: null },
    // デバッグオプション
    debug             : { types: [ 'boolean' ], value: null }
  };
  /*eslint-enable key-spacing*/

  // プロパティ登録(直接更新時にはDEPRECATEDメッセージを表示)
  Object.keys(property).forEach(function (prop) {
    Object.defineProperty(cli, prop, {
      enumerable: true,
      get: function () {
        // TODO 現在は直接更新も可としているのでコメントアウトしておく
        //if (typeOf(property[prop].value) === 'object') {
        //  // オブジェクトの場合は複製を返す
        //  var copy = {};
        //  each(property[prop].value, function (val, key) {
        //    copy[key] = val;
        //  });
        //  return copy;
        //}
        return property[prop].value;
      },
      set: function (value) {
        value = propertyUpdater.unwrap(value);

        // 型チェック
        var types = property[prop].types;
        var vtype = typeOf(value);
        if (types.indexOf(vtype) === -1 || (vtype === 'number' && value < 0)) {
          util.colorMessage(
            'WARNING',
            'invalid value: ' + String(value) + '. '
            + 'property "' + prop + '" can accept only ' + types.join(' or ')
          );
          return;
        }

        // headersのキーを全部小文字に変換 & 値がnullのキーは削除
        if (prop === 'headers') {
          value = rebuildHeaders(value);
        }

        property[prop].value = value;
      }
    });
  });
};

// 特殊プロパティ作成(動的に値を用意する)
var defineSpecialProperties = function (cli) {
  // browserプロパティ
  Object.defineProperty(cli, 'browser', {
    enumerable: true,
    get: function () {
      // User-Agentとブラウザテンプレが一致するものを探す
      var ua = this.headers['user-agent'];
      if (! ua) {
        return null;
      }
      var browser = Object.keys(browsers).filter(function (name) {
        return (browsers[name] === ua);
      });
      if (browser.length > 0) {
        return browser[0];
      }
      return 'custom';
    },
    set: function (type) {
      type = propertyUpdater.unwrap(type);

      if (type in browsers) {
        this.set('headers', {
          'User-Agent': browsers[type]
        });
        return;
      }
      util.colorMessage('WARNING', 'unknown browser: ' + type);
    }
  });

  // iconvプロパティ
  Object.defineProperty(cli, 'iconv', {
    enumerable: true,
    get: function () {
      return encoding.getIconvType();
    },
    set: function (icmod) {
      icmod = propertyUpdater.unwrap(icmod);

      if (! encoding.iconvLoad(icmod)) {
        throw new Error('Cannot find module "' + icmod + '"');
      }
    }
  });

  // バージョン情報プロパティ
  Object.defineProperty(cli, 'version', {
    enumerable: true,
    get: function () {
      return pkg.version;
    }
  });
};

// WIP マルチインスタンス化に向けて構造変更中
var CheerioHttpCli = (function () {
  /**
   * コンストラクタ
   */
  function CheerioHttpCli() {
    defineNormalProperties(this);
    defineSpecialProperties(this);
  }

  /**
   * プロパティや内部情報の初期化
   */
  CheerioHttpCli.prototype.reset = function () {
    // リクエストヘッダ
    this.set('headers', {}, true);
    // タイムアウトまでの時間(効いているかどうか不明)
    this.set('timeout', 30000);
    // gzip転送する/しない
    this.set('gzip', true);
    // Refererを自動設定する/しない
    this.set('referer', true);
    // <meta[http-equiv=refresh]>を検知してリダイレクトする/しない
    this.set('followMetaRefresh', false);
    // 受信を許可する最大のサイズ
    this.set('maxDataSize', null);
    // XML自動判別を使用しない
    this.set('forceHtml', false);
    // requestモジュールに渡すagentOptions
    this.set('agentOptions', {}, true);
    // デバッグオプション
    this.set('debug', false);

    client.reset();
    return this;
  };

  /**
   * プロパティを操作
   *
   * @param name  操作するプロパティ名
   * @param value 挿入する値
   * @param nomerge trueのときマージを行わない
   */
  CheerioHttpCli.prototype.set = function (name, value, nomerge) {
    // 特殊プロパティ
    if ([ 'browser', 'iconv' ].indexOf(name) !== -1) {
      this[name] = propertyUpdater.wrap(value);
      return this;
    }

    // プロパティが存在するかチェック
    if (! Object.keys(this).some((function (prop) {
      return (prop === name && typeOf(this[prop]) !== 'function');
    }).bind(this))) {
      throw new Error('no such property "' + name + '"');
    }

    // オブジェクトへの代入ならマージする(黒魔術使用)
    if (! nomerge && typeOf(this[name]) === 'object' && typeOf(value) === 'object') {
      this[name] = propertyUpdater.wrap(assign(this[name], value));
    } else {
      this[name] = propertyUpdater.wrap(value);
    }
    return this;
  };

  /**
   * [DEPRECATED] 使用するiconvモジュールを指定
   *
   * @param icmod iconvモジュール名(iconv|iconv-jp|iconv-lite)
   */
  CheerioHttpCli.prototype.setIconvEngine = function (icmod) {
    util.colorMessage('DEPRECATED', 'setIconvEngine() will be removed in the future. use set("iconv", value)');
    this.set('iconv', icmod);
  };

  /**
   * [DEPRECATED] ブラウザごとのUser-Agentをワンタッチ設定
   *
   * @param browser ブラウザ種類(see browsers.json)
   * @return 設定できた/できなかった
   */
  CheerioHttpCli.prototype.setBrowser = function (type) {
    util.colorMessage('DEPRECATED', 'setBrowser() will be removed in the future. use set("browser", value)');
    this.set('browser', type);
  };

  /**
   * GETによる非同期httpリクエストを実行
   *
   * @param url      リクエスト先のURL
   * @param param    リクエストパラメータ
   * @param encode   取得先のHTMLのエンコーディング(default: 自動判定)
   * @param callback リクエスト完了時のコールバック関数(err, cheerio, response, body)
   */
  CheerioHttpCli.prototype.fetch = function (url, param, encode, callback) {
    return client.run('GET', url, param, encode, callback);
  };

  /**
   * GETによる同期httpリクエストを実行
   *
   * @param url      リクエスト先のURL
   * @param param    リクエストパラメータ
   * @param encode   取得先のHTMLのエンコーディング(default: 自動判定)
   * @param callback リクエスト完了時のコールバック関数(err, cheerio, response, body)
   */
  CheerioHttpCli.prototype.fetchSync = function (url, param, encode) {
    return client.run('GET', url, param, encode, 'sync');
  };

  return CheerioHttpCli;
})();

var mainClient = new CheerioHttpCli();
// clientオブジェクト内で使用する外部オブジェクトを登録
mainClient.reset();
client.core = mainClient;
client.encoding = encoding;
client.cheerio = cheerioExtend(encoding, client);

module.exports = mainClient;