all files / lib/html-parsers/ index.js

33.33% Statements 8/24
14.49% Branches 10/69
65% Functions 13/20
33.33% Lines 8/24
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212                                                                                                                                                                                                                                                                                                                                                                                                26×     26×     26×                
'use strict'
 
import config from 'config'
import http from 'http'
import https from 'https'
import querystring from 'querystring'
import zlib from 'zlib'
import { notFound200, codeKo200 } from './false-error-codes'
 
/**
 * Helper to send an HTTP request to Myfox services, and parse response stream.
 * You will use this to analyze Myfox HTML response from an HTTP query,
 * and to avoid parsing general events like errors, authentication problems, etc...
 *
 * @param {string} method The HTTP method, in upper case (GET, POST, PUT, PATCH, DELETE, ...)
 * @param {string} path The URL path part (without query string and host parts), like '/home/1234'
 * @param {object} streamParser a stream parser to pipe on the distant response stream
 * @param {function} callback The callback function to call after parsing (will take (err, data) as arguments)
 * @param {object} queryParams An object to serialize into the query string
 * @param {object} payload An object to serialize as the request payload
 * @param {object} headers An object to push into the request headers
 * @param {function} cookieJar The function that will get/set the data to keep between each query (mainly the Cookie)
 */
export function httpsRequest (method, path, streamParser, callback, queryParams, payload, headers, cookieJar) {
  // POST / PUT / PATCH cases
  if (payload) {
    payload = querystring.stringify(payload) // encoded as querystring for login needs.
  }
  const postHeaders = (payload && (method === 'POST' || method === 'PUT' || method === 'PATCH')) ? {
    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
    'Content-Length': payload.length
  } : {}
 
  // Query parameters to path
  if (queryParams && queryParams.constructor === Object && Object.keys(queryParams).length > 0) {
    queryParams = querystring.stringify(queryParams)
    path = path + '?' + queryParams
  }
 
  try {
    let cookie = (cookieJar !== null && cookieJar !== undefined && cookieJar().cookie !== undefined) ? {'cookie': [cookieJar().cookie]} : {}
    let requestData = {
      hostname: config.get('myfox-wrapper-api.html.myfox.hostname'),
      port: config.get('myfox-wrapper-api.html.myfox.port'),
      path: path,
      headers: Object.assign(postHeaders, config.get('myfox-wrapper-api.html.myfox.headers'), cookie, headers),
      method: method
    }
    // console.log('Sending request to Myfox:', requestData)
 
    let req = ((config.get('myfox-wrapper-api.html.myfox.protocol') === 'http') ? http : https).request(requestData, (res) => {
      // console.log('Receiving response:', res.statusCode, res.headers)
      // If HTTP error case, no need to parse the body
      if (res.statusCode >= 400) {
        const error = new Error('Error status code returned by Myfox.')
        error.status = res.statusCode
        return callback(error)
      }
 
      // If false HTTP codes, fix them
      if (res.statusCode === 302 && res.headers.location === config.get('myfox-wrapper-api.html.myfox.redirectForbidden')) {
        const error = new Error('Myfox redirected to / because of forbidden access.')
        error.status = 403
        return callback(error)
      }
 
      // get Set-Cookie response header value to use it later
      let setCookie = res.headers['set-cookie']
      if (setCookie !== null && cookieJar !== null && cookieJar !== undefined) {
        cookieJar({'cookie': setCookie[0].replace(/ ?expires=[^;]*;/, '').replace(/ ?path=\//, '').trim()})
      }
 
      // From here we need body data. Sometimes it can be zipped!
      let unzippedRes
      let encoding = res.headers['content-encoding']
      if (encoding === 'gzip') {
        unzippedRes = res.pipe(zlib.createGunzip())
      } else if (encoding === 'deflate') {
        unzippedRes = res.pipe(zlib.createInflate())
      } else {
        unzippedRes = res
      }
 
      // From here we start to analyze the content of the data. Keep it in a buffer anyway.
      unzippedRes.setEncoding('utf8')
      let buffer = ''
      unzippedRes.on('data', (chunk) => {
        buffer += chunk
      })
 
      let notFound200Instance = notFound200()
      unzippedRes.pipe(notFound200Instance) // Change 'Page not found' code 200 by 404
      notFound200Instance.on('error', callback)
 
      let codeKo200Instance = codeKo200()
      unzippedRes.pipe(codeKo200Instance) // Change code KO with 200 by 400 and fetch the message
      codeKo200Instance.on('error', (err) => {
        if (err.status === 400) {
          callback(err, null)
        } // else it's not JSON data!
      })
 
      if (streamParser !== null && streamParser !== undefined) {
        // There is a streamParser. 'end' event is plugged on streamParser, not unzippedRes.
        unzippedRes.pipe(streamParser)
        streamParser.on('end', () => {
          callback(null, streamParser)
        })
      } else {
        // There is no streamParser, so all bufferized data will be returned at the 'end' event.
        unzippedRes.on('end', () => {
          callback(null, buffer)
        })
      }
    })
 
    if (streamParser !== null && streamParser !== undefined) {
      streamParser.on('error', callback)
    }
    req.on('error', callback)
 
    if (payload && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
      req.write(payload)
    }
    req.end()
  } catch (err) {
    console.error('requestData() encounter exception parsing Myfox response.', err)
    if (err.status === null || err.status === undefined) {
      err.status = 500
    }
    return callback(err)
  }
}
 
/**
 * Returns a function to generate a stream parser for a given HTML element, passed to the given callback.
 *
 * @param  {function}  callback  The callback function to give to the inner text stream parser.
 * @return {function}  A function generating a stream parser to retrieve inner text.
 */
export function trumpetInnerText (callback) {
  return function (element) {
    let buffer = ''
    const stream = element.createReadStream()
    stream.setEncoding('utf8')
    stream.on('data', (chunk) => {
      buffer += chunk
    })
    stream.on('end', () => {
      callback(buffer.trim())
    })
    stream.on('error', (err) => {
      throw new Error(err)
    })
  }
}
 
/**
 * Returns a function to call back with all CSS classes found on the given element.
 *
 * @param  {function}  callback  The callback function to give to the CSS class extractor.
 * @return {function}  A function that will call callback with the array of CSS classes found on the element.
 */
export function trumpetClasses (callback) {
  return function (element) {
    element.getAttribute('class', (classes) => {
      callback(classes.split(' '))
    })
  }
}
 
/**
 * Returns a function to call back with the attribute value found on the given element.
 *
 * @param  {string}    attributeName  The attribute name to retrieve.
 * @param  {function}  callback       The callback function to give to the attribute extractor.
 * @return {function}  A function that will call callback with the value of the attribute found on the element. If no attribute is found, then the callback is not called!
 */
export function trumpetAttr (attributeName, callback) {
  return function (element) {
    element.getAttribute(attributeName, (attributeValue) => {
      callback(attributeValue)
    })
  }
}
 
/**
 * Returns a function to affect an object attribute with the 'value' attribute of the found element.
 *
 * @param  {Object}    affectInto     The object to affect the found value.
 * @param  {string}    affectName     The name of the attribute in the affectInto object.
 * @param  {boolean}   skipDisabled   True to skip affectation if an attribute 'disabled="disabled"' is found.
 * @return {function}  A function that will affect an object attribute with the 'value' attribute of the found element.
 */
export function trumpetAffectValue (affectInto, affectName, skipDisabled = true) {
  return function (element) {
    element.getAttributes((attributes) => {
      Iif (!attributes['value'] || attributes['value'] === '') {
        return
      }
      Iif (skipDisabled && attributes['disabled'] && attributes['disabled'] === 'disabled') {
        return
      }
      if (affectName) {
        affectInto[affectName] = attributes['value']
      } else {
        affectInto[attributes['name']] = attributes['value']
      }
    })
  }
}