{"size": 5460, "ext": "js", "max_stars_repo_path": "src/api.js", "max_stars_repo_name": "uncovertruth/margaux", "max_stars_repo_stars_event_min_datetime": "2017-03-28T08:26:52.000Z", "max_stars_repo_stars_event_max_datetime": "2017-12-22T03:11:25.000Z", "max_issues_repo_path": "src/api.js", "max_issues_repo_name": "uncovertruth/margaux", "max_issues_repo_issues_event_min_datetime": "2016-12-19T03:44:17.000Z", "max_issues_repo_issues_event_max_datetime": "2018-11-12T07:42:18.000Z", "max_forks_repo_path": "src/api.js", "max_forks_repo_name": "uncovertruth/margaux", "max_forks_repo_forks_event_min_datetime": "2017-01-18T08:39:56.000Z", "max_forks_repo_forks_event_max_datetime": "2017-01-18T08:39:56.000Z"}
{"max_stars_count": 6, "max_issues_count": 169, "max_forks_count": 1, "avg_line_length": 30.0, "max_line_length": 77, "alphanum_fraction": 0.7106227106}
/* @flow */
'use strict'

import _ from 'lodash'
import errors from 'common-errors'
import path from 'path'
import validator from 'validator'
import { promisify } from 'util'
import uuid from 'uuid'

import * as margaux from './lib/margaux'
import inliner from './lib/inliner'
import * as libPath from './lib/path'
import {
  DEFAULT_HEIGHT,
  DEFAULT_WIDTH,
  DEFAULT_WAIT_TIME,
  USER_AGENT,
  REMOTE_DEBUGGING_PORTS,
  CLOSE_TAB_TIMEOUT
} from './const'

const create = promisify(margaux.create)
const evaluate = promisify(margaux.evaluate)
const setDeviceMetricsOver = promisify(margaux.setDeviceMetricsOver)
const setUserAgentOverride = promisify(margaux.setUserAgentOverride)
const setHeaders = promisify(margaux.setHeaders)
const navigate = promisify(margaux.navigate)
const extractViewport = promisify(margaux.extractViewport)
const forceCharset = promisify(margaux.forceCharset)
const convertLinkToAbsolutely = promisify(margaux.convertLinkToAbsolutely)
const removeScripts = promisify(margaux.removeScripts)
const emptyIframes = promisify(margaux.emptyIframes)
const getOuterHTML = promisify(margaux.getOuterHTML)
const close = promisify(margaux.close)
const inline = promisify(inliner)
const saveFile = promisify(libPath.saveFile)
const wait = delay => {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, delay)
  })
}

function Api () {}

Api.prototype.parseParameters = function (params: Object) {
  return {
    width: parseInt(params.width, 10) || DEFAULT_WIDTH,
    height: parseInt(params.height, 10) || DEFAULT_HEIGHT,
    waitTime: params.waitTime || DEFAULT_WAIT_TIME,
    userAgent: params.userAgent || USER_AGENT,
    acceptLanguage: params.acceptLanguage || 'ja', // 'ハイフン嫌なのでラクダで'
    saveDir: params.saveDir || 'x' // project_id を想定
  }
}

type TakeWebSnapshotOptions = {
  saveDir?: string
}

Api.prototype.takeWebSnapshot = function (
  url: string,
  params: TakeWebSnapshotOptions,
  storeBaseDir: string,
  cb: (err: ?Error, url?: string, viewport?: string) => void
) {
  function generateKey () {
    return uuid.v4().replace(/-/g, '')
  }

  if (!validator.isURL(url, { require_tld: false })) {
    return cb(new errors.ArgumentError('url'))
  }

  const opts = this.parseParameters(params)

  let uniquePath = libPath.generateStorePath(opts.saveDir, generateKey())
  let storePath = path.join(storeBaseDir, uniquePath)

  if (libPath.isFileExists(storePath)) {
    // すでに存在していたらもう一度だけ生成する。UUIDがプロジェクトレベルで2回続けてかぶるのは困る
    uniquePath = libPath.generateStorePath(opts.saveDir, generateKey())
    storePath = path.join(storeBaseDir, uniquePath)
    if (libPath.isFileExists(storePath)) {
      return cb(errors.AlreadyInUseError(storePath))
    }
  }
  // ディレクトリトラバーサル対策。プロジェクト横断は防げない（特に問題にはならないはず）
  if (storePath.indexOf(storeBaseDir) !== 0) {
    return cb(errors.NotPermittedError(storePath))
  }

  const host = 'localhost'
  const index = _.random(0, REMOTE_DEBUGGING_PORTS.length - 1)
  const port = REMOTE_DEBUGGING_PORTS[index]
  const waitTime = opts.waitTime || 1000
  ;(async function () {
    // create new chrome tab
    const chrome = await create(host, port)
    const expression = `setTimeout(window.close, ${CLOSE_TAB_TIMEOUT})`
    await evaluate(chrome, expression)

    // set device and user angent
    await setDeviceMetricsOver(chrome, {
      width: opts.width,
      height: opts.height
    })
    await setUserAgentOverride(chrome, { userAgent: opts.userAgent })
    const extraHeaders = {}
    if (opts.acceptLanguage) {
      extraHeaders['Accept-Language'] = opts.acceptLanguage
    }
    if (Object.keys(extraHeaders).length > 0) {
      await setHeaders(chrome, extraHeaders)
    }

    // start rendering and wait it finish.
    await navigate(chrome, url)
    await wait(waitTime)

    const viewport = await extractViewport(chrome)

    // resolve img@src and link@href
    await convertLinkToAbsolutely(chrome, { baseURI: url, selector: 'img' })
    await convertLinkToAbsolutely(chrome, { baseURI: url, selector: 'link' })

    // get html and close chrome tab
    await removeScripts(chrome)
    await emptyIframes(chrome)
    await forceCharset(chrome)
    const html = await getOuterHTML(chrome)
    await close(chrome)

    // inlining html and save it.
    const inlinedHtml = await inline(html, { baseUrl: url })
    await saveFile(storePath, inlinedHtml)

    cb(null, uniquePath, viewport)
  })()
}

Api.prototype.ping = function (
  mountCheckFile: string,
  mountCheckContent: string,
  chromeCheckURL: string,
  callback: (...args: any[]) => void
) {
  // mountが外れていると嫌なのでチェック
  if (!libPath.isFileExists(mountCheckFile)) {
    return callback(errors.NotFoundError(mountCheckFile + ' is not found.'))
  }
  libPath.readFile(mountCheckFile, (err, text) => {
    if (err) {
      err.status = 500
      return callback(errors.io.FileLoadError(err))
    }
    if (mountCheckContent.trim() !== text.trim()) {
      return callback(
        errors.io.FileLoadError(
          `get:${text} expected: ${mountCheckContent} of ${mountCheckFile}`
        )
      )
    }
  })

  // chromeに接続できてるのもチェック
  const host = 'localhost'
  const index = _.random(0, REMOTE_DEBUGGING_PORTS.length - 1)
  const port = REMOTE_DEBUGGING_PORTS[index]
  ;(async function () {
    const chrome = await create(host, port)
    await navigate(chrome, chromeCheckURL)
    await wait(1000)

    await getOuterHTML(chrome)
    await close(chrome)
    callback()
  })()
}

export default new Api()
