text: const helper = require('think-helper');
  const assert = require('assert');
  const Cookies = require('cookies');
  const {URL} = require('url');
  
  const PARAM = Symbol('context-param');
  const POST = Symbol('context-post');
  const FILE = Symbol('context-file');
  const COOKIE_STORE = Symbol('cookie-store');
  const path = require('path');
  const fs = require('fs');
  const onFinished = require('on-finished');
  const destroy = require('destroy');
  
  /**
   * extend context
   */
  module.exports = {
    /**
     * get userAgent header
     */
    get userAgent() {
      return this.header['user-agent'];
    },
    /**
     * is get request
     */
    get isGet() {
      return this.method === 'GET';
    },
    /**
     * is post request
     */
    get isPost() {
      return this.method === 'POST';
    },
    /**
     * is command line invoke
     */
    get isCli() {
      return this.method === 'CLI';
    },
    /**
     * get referer header
     */
    referer(onlyHost) {
      return this.referrer(onlyHost);
    },
    /**
     * get referer header
     */
    referrer(onlyHost) {
      const referrer = this.header['referer'];
      if (!referrer || !onlyHost) return referrer;
      try {
        return new URL(referrer).hostname;
      } catch (e) {}
    },
    /**
     * is method
     */
    isMethod(method) {
      return this.method === method.toUpperCase();
    },
    /**
     * is ajax request
     */
    isAjax(method) {
      if (method && !this.isMethod(method)) return false;
      return this.header['x-requested-with'] === 'XMLHttpRequest';
    },
    /**
     * is jsonp request
     */
    isJsonp(callbackField = this.config('jsonpCallbackField')) {
      return !!this.param(callbackField);
    },
    /**
     * send jsonp data
     */
    jsonp(data, callbackField = this.config('jsonpCallbackField')) {
      let field = this.param(callbackField);
      // remove unsafe chars
      field = (field || '').replace(/[^\w.]/g, '');
      if (field) {
        data = `${field}(${JSON.stringify(data)})`;
      }
      this.type = this.config('jsonpContentType');
      this.body = data;
      return false;
    },
    /**
     * send json data
     */
    json(data) {
      this.type = this.config('jsonContentType');
      this.body = data;
      return false;
    },
    /**
     * send success data
     */
    success(data = '', message = '') {
      const obj = {
        [this.config('errnoField')]: 0,
        [this.config('errmsgField')]: message,
        data
      };
      this.type = this.config('jsonContentType');
      this.body = obj;
      return false;
    },
    /**
     * send fail data
     */
    fail(errno, errmsg = '', data = '') {
      let obj;
      if (helper.isObject(errno)) {
        obj = errno;
      } else {
        if (/^[A-Z_]+$/.test(errno)) {
          const messages = think.app.validators.messages || {};
          const msg = messages[errno];
          if (think.isArray(msg)) {
            [errno, errmsg] = msg;
          }
        }
        if (!think.isNumber(errno)) {
          [data, errmsg, errno] = [errmsg, errno, this.config('defaultErrno')];
        }
        obj = {
          [this.config('errnoField')]: errno,
          [this.config('errmsgField')]: errmsg
        };
        if (data) {
          obj.data = data;
        }
      }
      this.type = this.config('jsonContentType');
      this.body = obj;
      return false;
    },
    /**
     * set expires header
     */
    expires(time) {
      time = helper.ms(time);
      const date = new Date(Date.now() + time);
      this.set('Cache-Control', `max-age=${time}`);
      this.set('Expires', date.toUTCString());
    },
    /**
     * get or set configs
     * @param {String} name
     * @param {Mixed} value
     * @param {String} m
     */
    config(name, value, m = this.module) {
      return think.config(name, value, m);
    },
    /**
     * get or set query data
     * `query` or `get` is already used in koa
     * @param {String} name
     * @param {Mixed} value
     */
    param(name, value) {
      if (!this[PARAM]) {
        this[PARAM] = Object.assign({}, this.request._query || this.request.query);
        this.app.emit('filterParam', this[PARAM]);
      }
      if (!name) return this[PARAM];
      if (helper.isObject(name)) {
        this[PARAM] = Object.assign(this[PARAM], name);
        return this;
      }
      if (value === undefined) {
        // this.param('a,b')
        if (helper.isString(name) && name.indexOf(',') > -1) {
          name = name.split(/\s*,\s*/);
        }
        if (helper.isArray(name)) {
          const value = {};
          name.forEach(item => {
            const val = this[PARAM][item];
            if (val !== undefined) {
              value[item] = val;
            }
          });
          return value;
        }
        return this[PARAM][name];
      }
      this[PARAM][name] = value;
      return this;
    },
    /**
     * get or set post data
     * @param {String} name
     * @param {Mixed} value
     */
    post(name, value) {
      if (!this[POST]) {
        const json = this.request.body && this.request.body.post;
        this[POST] = think.isArray(json) ? Array.from(json) : Object.assign({}, json);
        this.app.emit('filterParam', this[POST]);
      }
      if (!name) return this[POST];
      if (helper.isObject(name)) {
        this[POST] = Object.assign(this[POST], name);
        return this;
      }
      if (value === undefined) {
        // this.param('a,b')
        if (helper.isString(name) && name.indexOf(',') > -1) {
          name = name.split(/\s*,\s*/);
        }
        if (helper.isArray(name)) {
          const value = {};
          name.forEach(item => {
            const val = this[POST][item];
            if (val !== undefined) {
              value[item] = val;
            }
          });
          return value;
        }
        return this[POST][name];
      }
      this[POST][name] = value;
      return this;
    },
    /**
     * get or set file data
     * @param {String} name
     * @param {Mixed} value
     */
    file(name, value) {
      if (!this[FILE]) {
        this[FILE] = Object.assign({}, this.request.body && this.request.body.file);
      }
      if (!name) return this[FILE];
      if (helper.isObject(name)) {
        this[FILE] = Object.assign(this[FILE], name);
        return this;
      }
      if (value === undefined) {
        return this[FILE][name];
      }
      this[FILE][name] = value;
      return this;
    },
    /**
     * get or set cookie
     * @param {String} name
     * @param {String} value
     * @param {Object} options
     */
    cookie(name, value, options = {}) {
      assert(name && helper.isString(name), 'cookie.name must be a string');
      options = Object.assign({}, this.config('cookie'), options);
      const instance = new Cookies(this.req, this.res, {
        keys: options.keys,
        secure: this.request.secure
      });
  
      if (!this[COOKIE_STORE]) this[COOKIE_STORE] = {};
  
      // get cookie
      if (value === undefined) {
        if (this[COOKIE_STORE][name] !== undefined) return this[COOKIE_STORE][name];
        return instance.get(name, options);
      }
      // remove cookie
      if (value === null) {
        delete this[COOKIE_STORE][name];
        // If the value is omitted, an outbound header with an expired date is used to delete the cookie.
        // https://github.com/pillarjs/cookies#cookiesset-name--value---options--
        return instance.set(name, undefined, options);
      }
      assert(helper.isString(value), 'cookie value must be a string');
      // http://browsercookielimits.squawky.net/
      if (value.length >= 4094) {
        this.app.emit('cookieLimit', {name, value, ctx: this});
      }
      this[COOKIE_STORE][name] = value;
      // set cookie
      return instance.set(name, value, options);
    },
    /**
     * get service
     * @param {String} name
     * @param {String} m
     */
    service(...args) {
      return think.service(...args);
    },
    /**
     * download
     * @param {String} filepath
     * @param {String} filename
     */
    download(filepath, filename = path.basename(filepath)) {
      assert(filepath, 'filepath can not be empty');
      const contentType = this.response.get('Content-Type');
      if (!contentType) {
        this.type = path.extname(filename);
      }
      const contentDisposition = this.response.get('Content-Disposition');
      if (!contentDisposition) {
        this.attachment(filename);
      }
      const stream = fs.createReadStream(filepath);
      this.body = stream;
      onFinished(this.res, () => {
        destroy(stream);
      });
      return false;
    }
  };
  