<commit_before>const config = require('../../config.json')
const moment = require('moment-timezone')
const htmlConvert = require('html-to-text')
const defaultConfigs = require('../../util/configCheck.js').defaultConfigs

function dateHasNoTime(date) { // Determine if the time is T00:00:00.000Z
  const timeParts = [date.getUTCHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()]
  for (var x in timeParts) {
    if (timeParts[x] !== 0) return false
  }
  return true
}

function setCurrentTime(momentObj) {
  const now = new Date()
  return momentObj.hours(now.getUTCHours()).minutes(now.getMinutes()).seconds(now.getSeconds()).millisecond(now.getMilliseconds())
}

// To avoid stack call exceeded
function checkObjType (item, results) {
  if (Object.prototype.toString.call(item) === '[object Object]') {
    return function () {
      return findImages(item, results)
    }
  } else if (typeof item === 'string' && item.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/i) && !results.includes(item) && results.length < 9) results.push(item)
}

// Used to find images in any object values of the article
function findImages (obj, results) {
  for (var key in obj) {
    let value = checkObjType(obj[key], results)
    while (typeof value === 'function') {
      value = value()
    }
  }
}

function escapeRegExp (str) {
  return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')
}

function regexReplace (string, regexSearchQuery, replacementData, flags) {
  try {
    let newMatch
    if (replacementData.type === 'regex') {
      const newMatchIndex = replacementData.matchNumber
      const newRegExp = new RegExp(replacementData.content, flags)
      newMatch = newRegExp.exec(string)[newMatchIndex]
    } else if (replacementData.type === 'string') newMatch = replacementData.content

    const oldRegExp = new RegExp(regexSearchQuery, flags)
    const oldMatches = []
    let oldMatch
    while ((oldMatch = oldRegExp.exec(string)) && !oldMatches.includes(oldMatch[0])) {
      oldMatches.push(oldMatch[0])
    }
    for (var x in oldMatches) { // oldMatches is an array of strings
      let exp = new RegExp(escapeRegExp(oldMatches[x]), flags)
      string = string.replace(exp, newMatch)
    }

    return string
  } catch (e) {
    return e
  }
}

module.exports = function Article (rawArticle, guildRss, rssName) {
  const rssList = guildRss.sources

  function evalRegexConfig (text, placeholder) {
    const source = rssList[rssName]
    const customPlaceholders = {}

    if (typeof source.regexOps === 'object' && source.regexOps.disabled !== true && Array.isArray(source.regexOps[placeholder])) { // Eval regex if specified
      if (Array.isArray(source.regexOps.disabled) && source.regexOps.disabled.length > 0) { // .disabled can be an array of disabled placeholders, or just a boolean to disable everything
        for (var y in source.regexOps.disabled) { // Looping through strings of placeholders
          if (source.regexOps.disabled[y] === placeholder) return null // text
        }
      }

      for (var regexOpIndex in source.regexOps[placeholder]) { // Looping through each regexOp for a placeholder
        let regexOp = source.regexOps[placeholder][regexOpIndex]
        if (regexOp.disabled === true || typeof regexOp.name !== 'string') continue

        if (!customPlaceholders[regexOp.name]) customPlaceholders[regexOp.name] = text // Initialize with a value if it doesn't exist

        const clone = Object.assign({}, customPlaceholders)

        let modified = regexReplace(clone[regexOp.name], regexOp.search, regexOp.replacement, regexOp.flags)
        if (typeof modified !== 'string') {
          if (config.feedSettings.showRegexErrs !== false) console.log(`Error found while evaluating regex for feed ${source.link}:\n`, modified)
        } else customPlaceholders[regexOp.name] = modified // newText = modified
      }
    } else return null
    return customPlaceholders
  }

  function cleanRandoms (text, imgSrcs) {
    if (!text) return text

    text = text.replace(/\*/gi, '')
            .replace(/<(strong|b)>(.*?)<\/(strong|b)>/gi, '**$2**') // Bolded markdown
            .replace(/<(em|i)>(.*?)<(\/(em|i))>/gi, '*$2*') // Italicized markdown
            .replace(/<(u)>(.*?)<(\/(u))>/gi, '__$2__') // Underlined markdown

    text = htmlConvert.fromString(text, {
      wordwrap: null,
      ignoreHref: true,
      noLinkBrackets: true,
      format: {
        image: function (node, options) {
          if (Array.isArray(imgSrcs) && imgSrcs.length < 5 && typeof node.attribs.src === 'string' && node.attribs.src) {
            let link = node.attribs.src
            if (!link.startsWith('http') && link.startsWith('//')) link = 'http:' + link
            else if (!link.startsWith('http')) link = 'http://' + link
            imgSrcs.push(link)
          }
          const link = node.attribs.src

          let exist = true
          const globalExistOption = config.feedSettings.imageLinksExistence != null ? config.feedSettings.imageLinksExistence : defaultConfigs.feedSettings.imageLinksExistence.default // Always a boolean via startup checks
          exist = globalExistOption
          const specificExistOption = rssList[rssName].imageLinksExistence
          exist = typeof specificExistOption !== 'boolean' ? exist : specificExistOption
          if (!exist) return ''

          let image = ''
          const globalPreviewOption = config.feedSettings.imagePreviews != null ? config.feedSettings.imagePreviews : defaultConfigs.feedSettings.imagePreviews.default // Always a boolean via startup checks
          image = globalPreviewOption ? link : `<${link}>`
          const specificPreviewOption = rssList[rssName].imagePreviews
          image = typeof specificPreviewOption !== 'boolean' ? image : specificPreviewOption === true ? link : `<${link}>`

          return image
        },
        heading: function (node, fn, options) {
          let h = fn(node.children, options)
          return h
        }
      }
    })

    text = text.replace(/\n\s*\n\s*\n/g, '\n\n') // Replace triple line breaks with double
    return text
  }

  this.rawDescrip = rawArticle.guid && rawArticle.guid.startsWith('yt:video') && rawArticle['media:group'] && rawArticle['media:group']['media:description'] && rawArticle['media:group']['media:description']['#'] ? cleanRandoms(rawArticle['media:group']['media:description']['#']) : cleanRandoms(rawArticle.description) // Account for youtube's description
  this.rawSummary = cleanRandoms(rawArticle.summary)
  this.meta = rawArticle.meta
  this.guid = rawArticle.guid
  this.author = (rawArticle.author) ? cleanRandoms(rawArticle.author) : ''
  this.link = (rawArticle.link) ? rawArticle.link.split(' ')[0].trim() : '' // Sometimes HTML is appended at the end of links for some reason
  if (this.meta.link && this.meta.link.includes('www.reddit.com') && this.link.startsWith('/r/')) this.link = 'https://www.reddit.com' + this.link

  // Title
  const rawTitleImgs = []
  this.title = (!rawArticle.title) ? '' : cleanRandoms(rawArticle.title, rawTitleImgs)
  this.title = this.title.length > 150 ? this.title.slice(0, 150) + ' [...]' : this.title
  this.titleImgs = rawTitleImgs

  // Date
  if ((rawArticle.pubdate && rawArticle.toString() !== 'Invalid Date') || config.feedSettings.dateFallback === true) {
    const guildTimezone = guildRss.timezone
    const timezone = (guildTimezone && moment.tz.zone(guildTimezone)) ? guildTimezone : config.feedSettings.timezone
    const timeFormat = (config.feedSettings.timeFormat) ? config.feedSettings.timeFormat : 'ddd, D MMMM YYYY, h:mm A z'

    const useDateFallback = config.feedSettings.dateFallback === true && (!rawArticle.pubdate || rawArticle.pubdate.toString() === 'Invalid Date')
    const useTimeFallback = config.feedSettings.timeFallback === true && rawArticle.pubdate.toString() !== 'Invalid Date' && dateHasNoTime(rawArticle.pubdate)
    const date = useDateFallback || useTimeFallback ? new Date() : rawArticle.pubdate
    const vanityDateMoment = moment(date)
    if (useDateFallback) vanityDateMoment = setCurrentTime(vanityDateMoment)

    const vanityDate = vanityDateMoment.tz(timezone).format(timeFormat)
    this.date = (vanityDate !== 'Invalid Date') ? vanityDate : ''
  }

  // Description
  let rawArticleDescrip = ''
  const rawDescripImgs = []
  // YouTube doesn't use the regular description field, thus manually setting it as the description
  if (rawArticle.guid && rawArticle.guid.startsWith('yt:video') && rawArticle['media:group'] && rawArticle['media:group']['media:description'] && rawArticle['media:group']['media:description']['#']) rawArticleDescrip = rawArticle['media:group']['media:description']['#']
  else if (rawArticle.description) rawArticleDescrip = cleanRandoms(rawArticle.description, rawDescripImgs)
  rawArticleDescrip = (rawArticleDescrip.length > 800) ? `${rawArticleDescrip.slice(0, 790)} [...]` : rawArticleDescrip

  if (this.meta.link && this.meta.link.includes('reddit')) {
    rawArticleDescrip = rawArticleDescrip.replace('\n[link] [comments]', '') // truncate the useless end of reddit description
  }

  this.description = rawArticleDescrip
  this.descriptionImgs = rawDescripImgs

  // Summary
  let rawArticleSummary = ''
  const rawSummaryImgs = []
  if (rawArticle.summary) rawArticleSummary = cleanRandoms(rawArticle.summary, rawSummaryImgs)
  rawArticleSummary = (rawArticleSummary.length > 800) ? `${rawArticleSummary.slice(0, 790)} [...]` : rawArticleSummary
  this.summary = rawArticleSummary
  this.summaryImgs = rawSummaryImgs

  // List all {imageX} to string
  const imageLinks = []
  findImages(rawArticle, imageLinks)
  this.images = (imageLinks.length === 0) ? undefined : imageLinks

  this.listImages = function () {
    let imageList = ''
    for (var image in this.images) {
      imageList += `[Image${parseInt(image, 10) + 1} URL]: {image${parseInt(image, 10) + 1}}\n${this.images[image]}`
      if (parseInt(image, 10) !== this.images.length - 1) imageList += '\n'
    }
    return imageList
  }

  // List all {placeholder:imageX} to string
  this.listPlaceholderImages = function () {
    const validPlaceholders = ['title', 'description', 'summary']
    const listedImages = []
    let list = ''
    for (var k in validPlaceholders) {
      const placeholderImgs = this[validPlaceholders[k] + 'Imgs']
      for (var l in placeholderImgs) {
        if (listedImages.includes(placeholderImgs[l])) continue
        listedImages.push(placeholderImgs[l])
        const placeholder = validPlaceholders[k].slice(0, 1).toUpperCase() + validPlaceholders[k].substr(1, validPlaceholders[k].length)
        const imgNum = parseInt(l, 10) + 1
        list += `\n[${placeholder} Image${imgNum}]: {${validPlaceholders[k]}:image${imgNum}}\n${placeholderImgs[l]}`
      }
    }

    return list.trim()
  }

  // Categories/Tags
  if (rawArticle.categories) {
    let categoryList = ''
    for (var category in rawArticle.categories) {
      if (typeof rawArticle.categories[category] !== 'string') continue
      categoryList += rawArticle.categories[category].trim()
      if (parseInt(category, 10) !== rawArticle.categories.length - 1) categoryList += '\n'
    }
    this.tags = categoryList
  }

  this.resolvePlaceholderImg = function (input) {
    const arr = input.split(':')
    if (arr.length === 1 || !arr[1].startsWith('image') || arr[1].length !== 7) return

    const validPlaceholders = ['title', 'description', 'summary']
    const placeholder = arr[0].substr(1, arr[0].length)
    const placeholderImgs = this[placeholder + 'Imgs']
    if (!validPlaceholders.includes(placeholder) || !placeholderImgs || placeholderImgs.length < 1) return

    const imgNum = parseInt(arr[1].replace(/[^1-9]/g, ''), 10) - 1
    if (isNaN(imgNum) || imgNum > 4 || imgNum < 0) return

    return placeholderImgs[imgNum]
  }

  // {imageX} and {placeholder:imageX}
  this.convertImgs = function (content) {
    const imgDictionary = {}
    const imgLocs = content.match(/{image.+}/g)
    const phImageLocs = content.match(/({(description|image|title):image[1-5]})/gi)

    if (imgLocs) {
      for (var loc in imgLocs) {
        if (imgLocs[loc].length === 8) { // only single digit image numbers
          let imgNum = parseInt(imgLocs[loc].substr(6, 1), 10)
          if (!isNaN(imgNum) && imgNum !== 0 && this.images && this.images[imgNum - 1]) imgDictionary[imgLocs[loc]] = this.images[imgNum - 1] // key is {imageX}, value is article image URL
          else if (!isNaN(imgNum) || imgNum === 0 || !this.images) imgDictionary[imgLocs[loc]] = ''
        }
      }
      for (var imgKeyword in imgDictionary) content = content.replace(new RegExp(imgKeyword, 'g'), imgDictionary[imgKeyword])
    } else if (phImageLocs) {
      for (var h in phImageLocs) {
        content = this.resolvePlaceholderImg(phImageLocs[h]) ? content.replace(phImageLocs[h], this.resolvePlaceholderImg(phImageLocs[h])) : content.replace(phImageLocs[h], '')
      }
    }

    return content
  }

  // Regex-defined custom placeholders
  const validRegexPlaceholder = ['title', 'description', 'summary']
  const regexPlaceholders = {} // Each key is a validRegexPlaceholder, and their values are an object of named placeholders with the modified content
  for (var b in validRegexPlaceholder) {
    const type = validRegexPlaceholder[b]
    const regexResults = evalRegexConfig(this[type], type)
    regexPlaceholders[type] = regexResults
  }

  // replace simple keywords
  this.convertKeywords = function (word) {
    let content = word.replace(/{date}/g, this.date)
            .replace(/{title}/g, this.title)
            .replace(/{author}/g, this.author)
            .replace(/{summary}/g, this.summary)
            .replace(/{subscriptions}/g, this.subscriptions)
            .replace(/{link}/g, this.link)
            .replace(/{description}/g, this.description)
            .replace(/{tags}/g, this.tags)

    for (var placeholder in regexPlaceholders) {
      for (var customName in regexPlaceholders[placeholder]) {
        const replacementQuery = new RegExp(`{${placeholder}:${escapeRegExp(customName)}}`, 'g')
        const replacementContent = regexPlaceholders[placeholder][customName]
        content = content.replace(replacementQuery, replacementContent)
      }
    }

    return this.convertImgs(content)
  }

  return this
}
<commit_msg>fixed typo with timeFallback<commit_after>const config = require('../../config.json')
const moment = require('moment-timezone')
const htmlConvert = require('html-to-text')
const defaultConfigs = require('../../util/configCheck.js').defaultConfigs

function dateHasNoTime(date) { // Determine if the time is T00:00:00.000Z
  const timeParts = [date.getUTCHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds()]
  for (var x in timeParts) {
    if (timeParts[x] !== 0) return false
  }
  return true
}

function setCurrentTime(momentObj) {
  const now = new Date()
  return momentObj.hours(now.getHours()).minutes(now.getMinutes()).seconds(now.getSeconds()).millisecond(now.getMilliseconds())
}

// To avoid stack call exceeded
function checkObjType (item, results) {
  if (Object.prototype.toString.call(item) === '[object Object]') {
    return function () {
      return findImages(item, results)
    }
  } else if (typeof item === 'string' && item.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/i) && !results.includes(item) && results.length < 9) results.push(item)
}

// Used to find images in any object values of the article
function findImages (obj, results) {
  for (var key in obj) {
    let value = checkObjType(obj[key], results)
    while (typeof value === 'function') {
      value = value()
    }
  }
}

function escapeRegExp (str) {
  return str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&')
}

function regexReplace (string, regexSearchQuery, replacementData, flags) {
  try {
    let newMatch
    if (replacementData.type === 'regex') {
      const newMatchIndex = replacementData.matchNumber
      const newRegExp = new RegExp(replacementData.content, flags)
      newMatch = newRegExp.exec(string)[newMatchIndex]
    } else if (replacementData.type === 'string') newMatch = replacementData.content

    const oldRegExp = new RegExp(regexSearchQuery, flags)
    const oldMatches = []
    let oldMatch
    while ((oldMatch = oldRegExp.exec(string)) && !oldMatches.includes(oldMatch[0])) {
      oldMatches.push(oldMatch[0])
    }
    for (var x in oldMatches) { // oldMatches is an array of strings
      let exp = new RegExp(escapeRegExp(oldMatches[x]), flags)
      string = string.replace(exp, newMatch)
    }

    return string
  } catch (e) {
    return e
  }
}

module.exports = function Article (rawArticle, guildRss, rssName) {
  const rssList = guildRss.sources

  function evalRegexConfig (text, placeholder) {
    const source = rssList[rssName]
    const customPlaceholders = {}

    if (typeof source.regexOps === 'object' && source.regexOps.disabled !== true && Array.isArray(source.regexOps[placeholder])) { // Eval regex if specified
      if (Array.isArray(source.regexOps.disabled) && source.regexOps.disabled.length > 0) { // .disabled can be an array of disabled placeholders, or just a boolean to disable everything
        for (var y in source.regexOps.disabled) { // Looping through strings of placeholders
          if (source.regexOps.disabled[y] === placeholder) return null // text
        }
      }

      for (var regexOpIndex in source.regexOps[placeholder]) { // Looping through each regexOp for a placeholder
        let regexOp = source.regexOps[placeholder][regexOpIndex]
        if (regexOp.disabled === true || typeof regexOp.name !== 'string') continue

        if (!customPlaceholders[regexOp.name]) customPlaceholders[regexOp.name] = text // Initialize with a value if it doesn't exist

        const clone = Object.assign({}, customPlaceholders)

        let modified = regexReplace(clone[regexOp.name], regexOp.search, regexOp.replacement, regexOp.flags)
        if (typeof modified !== 'string') {
          if (config.feedSettings.showRegexErrs !== false) console.log(`Error found while evaluating regex for feed ${source.link}:\n`, modified)
        } else customPlaceholders[regexOp.name] = modified // newText = modified
      }
    } else return null
    return customPlaceholders
  }

  function cleanRandoms (text, imgSrcs) {
    if (!text) return text

    text = text.replace(/\*/gi, '')
            .replace(/<(strong|b)>(.*?)<\/(strong|b)>/gi, '**$2**') // Bolded markdown
            .replace(/<(em|i)>(.*?)<(\/(em|i))>/gi, '*$2*') // Italicized markdown
            .replace(/<(u)>(.*?)<(\/(u))>/gi, '__$2__') // Underlined markdown

    text = htmlConvert.fromString(text, {
      wordwrap: null,
      ignoreHref: true,
      noLinkBrackets: true,
      format: {
        image: function (node, options) {
          if (Array.isArray(imgSrcs) && imgSrcs.length < 5 && typeof node.attribs.src === 'string' && node.attribs.src) {
            let link = node.attribs.src
            if (!link.startsWith('http') && link.startsWith('//')) link = 'http:' + link
            else if (!link.startsWith('http')) link = 'http://' + link
            imgSrcs.push(link)
          }
          const link = node.attribs.src

          let exist = true
          const globalExistOption = config.feedSettings.imageLinksExistence != null ? config.feedSettings.imageLinksExistence : defaultConfigs.feedSettings.imageLinksExistence.default // Always a boolean via startup checks
          exist = globalExistOption
          const specificExistOption = rssList[rssName].imageLinksExistence
          exist = typeof specificExistOption !== 'boolean' ? exist : specificExistOption
          if (!exist) return ''

          let image = ''
          const globalPreviewOption = config.feedSettings.imagePreviews != null ? config.feedSettings.imagePreviews : defaultConfigs.feedSettings.imagePreviews.default // Always a boolean via startup checks
          image = globalPreviewOption ? link : `<${link}>`
          const specificPreviewOption = rssList[rssName].imagePreviews
          image = typeof specificPreviewOption !== 'boolean' ? image : specificPreviewOption === true ? link : `<${link}>`

          return image
        },
        heading: function (node, fn, options) {
          let h = fn(node.children, options)
          return h
        }
      }
    })

    text = text.replace(/\n\s*\n\s*\n/g, '\n\n') // Replace triple line breaks with double
    return text
  }

  this.rawDescrip = rawArticle.guid && rawArticle.guid.startsWith('yt:video') && rawArticle['media:group'] && rawArticle['media:group']['media:description'] && rawArticle['media:group']['media:description']['#'] ? cleanRandoms(rawArticle['media:group']['media:description']['#']) : cleanRandoms(rawArticle.description) // Account for youtube's description
  this.rawSummary = cleanRandoms(rawArticle.summary)
  this.meta = rawArticle.meta
  this.guid = rawArticle.guid
  this.author = (rawArticle.author) ? cleanRandoms(rawArticle.author) : ''
  this.link = (rawArticle.link) ? rawArticle.link.split(' ')[0].trim() : '' // Sometimes HTML is appended at the end of links for some reason
  if (this.meta.link && this.meta.link.includes('www.reddit.com') && this.link.startsWith('/r/')) this.link = 'https://www.reddit.com' + this.link

  // Title
  const rawTitleImgs = []
  this.title = (!rawArticle.title) ? '' : cleanRandoms(rawArticle.title, rawTitleImgs)
  this.title = this.title.length > 150 ? this.title.slice(0, 150) + ' [...]' : this.title
  this.titleImgs = rawTitleImgs

  // Date
  if ((rawArticle.pubdate && rawArticle.toString() !== 'Invalid Date') || config.feedSettings.dateFallback === true) {
    const guildTimezone = guildRss.timezone
    const timezone = (guildTimezone && moment.tz.zone(guildTimezone)) ? guildTimezone : config.feedSettings.timezone
    const timeFormat = (config.feedSettings.timeFormat) ? config.feedSettings.timeFormat : 'ddd, D MMMM YYYY, h:mm A z'

    const useDateFallback = config.feedSettings.dateFallback === true && (!rawArticle.pubdate || rawArticle.pubdate.toString() === 'Invalid Date')
    const useTimeFallback = config.feedSettings.timeFallback === true && rawArticle.pubdate.toString() !== 'Invalid Date' && dateHasNoTime(rawArticle.pubdate)
    const date = useDateFallback ? new Date() : rawArticle.pubdate
    let vanityDateMoment = moment(date)
    if (useTimeFallback) vanityDateMoment = setCurrentTime(vanityDateMoment)

    const vanityDate = vanityDateMoment.tz(timezone).format(timeFormat)
    this.date = (vanityDate !== 'Invalid Date') ? vanityDate : ''
  }

  // Description
  let rawArticleDescrip = ''
  const rawDescripImgs = []
  // YouTube doesn't use the regular description field, thus manually setting it as the description
  if (rawArticle.guid && rawArticle.guid.startsWith('yt:video') && rawArticle['media:group'] && rawArticle['media:group']['media:description'] && rawArticle['media:group']['media:description']['#']) rawArticleDescrip = rawArticle['media:group']['media:description']['#']
  else if (rawArticle.description) rawArticleDescrip = cleanRandoms(rawArticle.description, rawDescripImgs)
  rawArticleDescrip = (rawArticleDescrip.length > 800) ? `${rawArticleDescrip.slice(0, 790)} [...]` : rawArticleDescrip

  if (this.meta.link && this.meta.link.includes('reddit')) {
    rawArticleDescrip = rawArticleDescrip.replace('\n[link] [comments]', '') // truncate the useless end of reddit description
  }

  this.description = rawArticleDescrip
  this.descriptionImgs = rawDescripImgs

  // Summary
  let rawArticleSummary = ''
  const rawSummaryImgs = []
  if (rawArticle.summary) rawArticleSummary = cleanRandoms(rawArticle.summary, rawSummaryImgs)
  rawArticleSummary = (rawArticleSummary.length > 800) ? `${rawArticleSummary.slice(0, 790)} [...]` : rawArticleSummary
  this.summary = rawArticleSummary
  this.summaryImgs = rawSummaryImgs

  // List all {imageX} to string
  const imageLinks = []
  findImages(rawArticle, imageLinks)
  this.images = (imageLinks.length === 0) ? undefined : imageLinks

  this.listImages = function () {
    let imageList = ''
    for (var image in this.images) {
      imageList += `[Image${parseInt(image, 10) + 1} URL]: {image${parseInt(image, 10) + 1}}\n${this.images[image]}`
      if (parseInt(image, 10) !== this.images.length - 1) imageList += '\n'
    }
    return imageList
  }

  // List all {placeholder:imageX} to string
  this.listPlaceholderImages = function () {
    const validPlaceholders = ['title', 'description', 'summary']
    const listedImages = []
    let list = ''
    for (var k in validPlaceholders) {
      const placeholderImgs = this[validPlaceholders[k] + 'Imgs']
      for (var l in placeholderImgs) {
        if (listedImages.includes(placeholderImgs[l])) continue
        listedImages.push(placeholderImgs[l])
        const placeholder = validPlaceholders[k].slice(0, 1).toUpperCase() + validPlaceholders[k].substr(1, validPlaceholders[k].length)
        const imgNum = parseInt(l, 10) + 1
        list += `\n[${placeholder} Image${imgNum}]: {${validPlaceholders[k]}:image${imgNum}}\n${placeholderImgs[l]}`
      }
    }

    return list.trim()
  }

  // Categories/Tags
  if (rawArticle.categories) {
    let categoryList = ''
    for (var category in rawArticle.categories) {
      if (typeof rawArticle.categories[category] !== 'string') continue
      categoryList += rawArticle.categories[category].trim()
      if (parseInt(category, 10) !== rawArticle.categories.length - 1) categoryList += '\n'
    }
    this.tags = categoryList
  }

  this.resolvePlaceholderImg = function (input) {
    const arr = input.split(':')
    if (arr.length === 1 || !arr[1].startsWith('image') || arr[1].length !== 7) return

    const validPlaceholders = ['title', 'description', 'summary']
    const placeholder = arr[0].substr(1, arr[0].length)
    const placeholderImgs = this[placeholder + 'Imgs']
    if (!validPlaceholders.includes(placeholder) || !placeholderImgs || placeholderImgs.length < 1) return

    const imgNum = parseInt(arr[1].replace(/[^1-9]/g, ''), 10) - 1
    if (isNaN(imgNum) || imgNum > 4 || imgNum < 0) return

    return placeholderImgs[imgNum]
  }

  // {imageX} and {placeholder:imageX}
  this.convertImgs = function (content) {
    const imgDictionary = {}
    const imgLocs = content.match(/{image.+}/g)
    const phImageLocs = content.match(/({(description|image|title):image[1-5]})/gi)

    if (imgLocs) {
      for (var loc in imgLocs) {
        if (imgLocs[loc].length === 8) { // only single digit image numbers
          let imgNum = parseInt(imgLocs[loc].substr(6, 1), 10)
          if (!isNaN(imgNum) && imgNum !== 0 && this.images && this.images[imgNum - 1]) imgDictionary[imgLocs[loc]] = this.images[imgNum - 1] // key is {imageX}, value is article image URL
          else if (!isNaN(imgNum) || imgNum === 0 || !this.images) imgDictionary[imgLocs[loc]] = ''
        }
      }
      for (var imgKeyword in imgDictionary) content = content.replace(new RegExp(imgKeyword, 'g'), imgDictionary[imgKeyword])
    } else if (phImageLocs) {
      for (var h in phImageLocs) {
        content = this.resolvePlaceholderImg(phImageLocs[h]) ? content.replace(phImageLocs[h], this.resolvePlaceholderImg(phImageLocs[h])) : content.replace(phImageLocs[h], '')
      }
    }

    return content
  }

  // Regex-defined custom placeholders
  const validRegexPlaceholder = ['title', 'description', 'summary']
  const regexPlaceholders = {} // Each key is a validRegexPlaceholder, and their values are an object of named placeholders with the modified content
  for (var b in validRegexPlaceholder) {
    const type = validRegexPlaceholder[b]
    const regexResults = evalRegexConfig(this[type], type)
    regexPlaceholders[type] = regexResults
  }

  // replace simple keywords
  this.convertKeywords = function (word) {
    let content = word.replace(/{date}/g, this.date)
            .replace(/{title}/g, this.title)
            .replace(/{author}/g, this.author)
            .replace(/{summary}/g, this.summary)
            .replace(/{subscriptions}/g, this.subscriptions)
            .replace(/{link}/g, this.link)
            .replace(/{description}/g, this.description)
            .replace(/{tags}/g, this.tags)

    for (var placeholder in regexPlaceholders) {
      for (var customName in regexPlaceholders[placeholder]) {
        const replacementQuery = new RegExp(`{${placeholder}:${escapeRegExp(customName)}}`, 'g')
        const replacementContent = regexPlaceholders[placeholder][customName]
        content = content.replace(replacementQuery, replacementContent)
      }
    }

    return this.convertImgs(content)
  }

  return this
}
<|endoftext|>


<commit_before>import Component from '@glimmer/component';
import {CURRENCIES} from 'ghost-admin/components/gh-members-payments-setting';
import {action} from '@ember/object';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency-decorators';
import {tracked} from '@glimmer/tracking';

export default class GhLaunchWizardSetPricingComponent extends Component {
    @service config;
    @service membersUtils;
    @service settings;

    currencies = CURRENCIES;

    @tracked stripeMonthlyAmount = null;
    @tracked stripeYearlyAmount = null;

    get stripePlans() {
        const plans = this.settings.get('stripePlans') || [];
        const monthly = plans.find(plan => plan.interval === 'month');
        const yearly = plans.find(plan => plan.interval === 'year' && plan.name !== 'Complimentary');

        return {
            monthly: {
                amount: (parseInt(monthly?.amount) || 0) / 100 || 5,
                currency: monthly?.currency || this.currencies[0].value
            },
            yearly: {
                amount: (parseInt(yearly?.amount) || 0) / 100 || 50,
                currency: yearly?.currency || this.currencies[0].value
            }
        };
    }

    get selectedCurrency() {
        return this.currencies.findBy('value', this.stripePlans.monthly.currency);
    }

    get isFreeChecked() {
        const allowedPlans = this.settings.get('portalPlans') || [];
        return (this.settings.get('membersAllowFreeSignup') && allowedPlans.includes('free'));
    }

    get isMonthlyChecked() {
        const allowedPlans = this.settings.get('portalPlans') || [];
        return (this.membersUtils.isStripeEnabled && allowedPlans.includes('monthly'));
    }

    get isYearlyChecked() {
        const allowedPlans = this.settings.get('portalPlans') || [];
        return (this.membersUtils.isStripeEnabled && allowedPlans.includes('yearly'));
    }

    constructor() {
        super(...arguments);
        this.updatePreviewUrl();
    }

    willDestroy() {
        // clear any unsaved settings changes when going back/forward/closing
        this.settings.rollbackAttributes();
        this.args.updatePreview('');
    }

    @action
    setStripePlansCurrency(event) {
        const newCurrency = event.value;

        const updatedPlans = this.settings.get('stripePlans').map((plan) => {
            if (plan.name !== 'Complimentary') {
                return Object.assign({}, plan, {
                    currency: newCurrency
                });
            }
            return plan;
        });

        const currentComplimentaryPlan = updatedPlans.find((plan) => {
            return plan.name === 'Complimentary' && plan.currency === event.value;
        });

        if (!currentComplimentaryPlan) {
            updatedPlans.push({
                name: 'Complimentary',
                currency: event.value,
                interval: 'year',
                amount: 0
            });
        }

        this.settings.set('stripePlans', updatedPlans);
        this.updatePreviewUrl();
    }

    @action
    toggleFreePlan(event) {
        this.updateAllowedPlan('free', event.target.checked);
    }

    @action
    toggleMonthlyPlan(event) {
        this.updateAllowedPlan('monthly', event.target.checked);
    }

    @action
    toggleYearlyPlan(event) {
        this.updateAllowedPlan('yearly', event.target.checked);
    }

    @action
    validateStripePlans() {
        this.settings.errors.remove('stripePlans');
        this.settings.hasValidated.removeObject('stripePlans');

        if (this.stripeYearlyAmount === null) {
            this.stripeYearlyAmount = this.stripePlans.yearly.amount;
        }
        if (this.stripeMonthlyAmount === null) {
            this.stripeMonthlyAmount = this.stripePlans.monthly.amount;
        }

        try {
            const selectedCurrency = this.selectedCurrency;
            const yearlyAmount = parseInt(this.stripeYearlyAmount);
            const monthlyAmount = parseInt(this.stripeMonthlyAmount);
            if (!yearlyAmount || yearlyAmount < 1 || !monthlyAmount || monthlyAmount < 1) {
                throw new TypeError(`Subscription amount must be at least ${selectedCurrency.symbol}1.00`);
            }

            const updatedPlans = this.settings.get('stripePlans').map((plan) => {
                if (plan.name !== 'Complimentary') {
                    let newAmount;
                    if (plan.interval === 'year') {
                        newAmount = yearlyAmount * 100;
                    } else if (plan.interval === 'month') {
                        newAmount = monthlyAmount * 100;
                    }
                    return Object.assign({}, plan, {
                        amount: newAmount
                    });
                }
                return plan;
            });

            this.settings.set('stripePlans', updatedPlans);
            this.updatePreviewUrl();
        } catch (err) {
            this.settings.errors.add('stripePlans', err.message);
        } finally {
            this.settings.hasValidated.pushObject('stripePlans');
        }
    }

    @task
    *saveAndContinue() {
        yield this.validateStripePlans();

        if (this.settings.errors.length > 0) {
            return false;
        }

        yield this.settings.save();
        this.args.nextStep();
    }

    updateAllowedPlan(plan, isChecked) {
        const allowedPlans = this.settings.get('portalPlans') || [];

        if (!isChecked) {
            this.settings.set('portalPlans', allowedPlans.filter(p => p !== plan));
        } else {
            allowedPlans.push(plan);
            this.settings.set('portalPlans', [...allowedPlans]);
        }

        this.updatePreviewUrl();
    }

    updatePreviewUrl() {
        const options = {
            disableBackground: true,
            currency: this.selectedCurrency.value,
            monthlyPrice: this.stripePlans.monthly.amount,
            yearlyPrice: this.stripePlans.yearly.amount,
            isMonthlyChecked: this.isMonthlyChecked,
            isYearlyChecked: this.isYearlyChecked,
            isFreeChecked: this.isFreeChecked
        };

        const url = this.membersUtils.getPortalPreviewUrl(options);
        this.args.updatePreview(url);
    }
}
<commit_msg>Fixed currencies for launch wizard (#1854)<commit_after>import Component from '@glimmer/component';
import {action} from '@ember/object';
import {currencies} from 'ghost-admin/utils/currency';
import {inject as service} from '@ember/service';
import {task} from 'ember-concurrency-decorators';
import {tracked} from '@glimmer/tracking';

const CURRENCIES = currencies.map((currency) => {
    return {
        value: currency.isoCode.toLowerCase(),
        label: `${currency.isoCode} - ${currency.name}`,
        isoCode: currency.isoCode
    };
});

export default class GhLaunchWizardSetPricingComponent extends Component {
    @service config;
    @service membersUtils;
    @service settings;

    currencies = CURRENCIES;

    @tracked stripeMonthlyAmount = null;
    @tracked stripeYearlyAmount = null;

    get stripePlans() {
        const plans = this.settings.get('stripePlans') || [];
        const monthly = plans.find(plan => plan.interval === 'month');
        const yearly = plans.find(plan => plan.interval === 'year' && plan.name !== 'Complimentary');

        return {
            monthly: {
                amount: (parseInt(monthly?.amount) || 0) / 100 || 5,
                currency: monthly?.currency || this.currencies[0].value
            },
            yearly: {
                amount: (parseInt(yearly?.amount) || 0) / 100 || 50,
                currency: yearly?.currency || this.currencies[0].value
            }
        };
    }

    get selectedCurrency() {
        return this.currencies.findBy('value', this.stripePlans.monthly.currency);
    }

    get isFreeChecked() {
        const allowedPlans = this.settings.get('portalPlans') || [];
        return (this.settings.get('membersAllowFreeSignup') && allowedPlans.includes('free'));
    }

    get isMonthlyChecked() {
        const allowedPlans = this.settings.get('portalPlans') || [];
        return (this.membersUtils.isStripeEnabled && allowedPlans.includes('monthly'));
    }

    get isYearlyChecked() {
        const allowedPlans = this.settings.get('portalPlans') || [];
        return (this.membersUtils.isStripeEnabled && allowedPlans.includes('yearly'));
    }

    constructor() {
        super(...arguments);
        this.updatePreviewUrl();
    }

    willDestroy() {
        // clear any unsaved settings changes when going back/forward/closing
        this.settings.rollbackAttributes();
        this.args.updatePreview('');
    }

    @action
    setStripePlansCurrency(event) {
        const newCurrency = event.value;

        const updatedPlans = this.settings.get('stripePlans').map((plan) => {
            if (plan.name !== 'Complimentary') {
                return Object.assign({}, plan, {
                    currency: newCurrency
                });
            }
            return plan;
        });

        const currentComplimentaryPlan = updatedPlans.find((plan) => {
            return plan.name === 'Complimentary' && plan.currency === event.value;
        });

        if (!currentComplimentaryPlan) {
            updatedPlans.push({
                name: 'Complimentary',
                currency: event.value,
                interval: 'year',
                amount: 0
            });
        }

        this.settings.set('stripePlans', updatedPlans);
        this.updatePreviewUrl();
    }

    @action
    toggleFreePlan(event) {
        this.updateAllowedPlan('free', event.target.checked);
    }

    @action
    toggleMonthlyPlan(event) {
        this.updateAllowedPlan('monthly', event.target.checked);
    }

    @action
    toggleYearlyPlan(event) {
        this.updateAllowedPlan('yearly', event.target.checked);
    }

    @action
    validateStripePlans() {
        this.settings.errors.remove('stripePlans');
        this.settings.hasValidated.removeObject('stripePlans');

        if (this.stripeYearlyAmount === null) {
            this.stripeYearlyAmount = this.stripePlans.yearly.amount;
        }
        if (this.stripeMonthlyAmount === null) {
            this.stripeMonthlyAmount = this.stripePlans.monthly.amount;
        }

        try {
            const selectedCurrency = this.selectedCurrency;
            const yearlyAmount = parseInt(this.stripeYearlyAmount);
            const monthlyAmount = parseInt(this.stripeMonthlyAmount);
            if (!yearlyAmount || yearlyAmount < 1 || !monthlyAmount || monthlyAmount < 1) {
                throw new TypeError(`Subscription amount must be at least ${selectedCurrency.symbol}1.00`);
            }

            const updatedPlans = this.settings.get('stripePlans').map((plan) => {
                if (plan.name !== 'Complimentary') {
                    let newAmount;
                    if (plan.interval === 'year') {
                        newAmount = yearlyAmount * 100;
                    } else if (plan.interval === 'month') {
                        newAmount = monthlyAmount * 100;
                    }
                    return Object.assign({}, plan, {
                        amount: newAmount
                    });
                }
                return plan;
            });

            this.settings.set('stripePlans', updatedPlans);
            this.updatePreviewUrl();
        } catch (err) {
            this.settings.errors.add('stripePlans', err.message);
        } finally {
            this.settings.hasValidated.pushObject('stripePlans');
        }
    }

    @task
    *saveAndContinue() {
        yield this.validateStripePlans();

        if (this.settings.errors.length > 0) {
            return false;
        }

        yield this.settings.save();
        this.args.nextStep();
    }

    updateAllowedPlan(plan, isChecked) {
        const allowedPlans = this.settings.get('portalPlans') || [];

        if (!isChecked) {
            this.settings.set('portalPlans', allowedPlans.filter(p => p !== plan));
        } else {
            allowedPlans.push(plan);
            this.settings.set('portalPlans', [...allowedPlans]);
        }

        this.updatePreviewUrl();
    }

    updatePreviewUrl() {
        const options = {
            disableBackground: true,
            currency: this.selectedCurrency.value,
            monthlyPrice: this.stripePlans.monthly.amount,
            yearlyPrice: this.stripePlans.yearly.amount,
            isMonthlyChecked: this.isMonthlyChecked,
            isYearlyChecked: this.isYearlyChecked,
            isFreeChecked: this.isFreeChecked
        };

        const url = this.membersUtils.getPortalPreviewUrl(options);
        this.args.updatePreview(url);
    }
}
<|endoftext|>


<commit_before>import {
    LocaisService
} from "./LocaisService";
import {
    UsersService
} from "./usersService";

import Reserva from '../model/Reserva';
import _ from '../util/util';

require('../config/db_config')();

const _consultarReservas = (params = {}) => new Promise(
    (resolve, reject) => Reserva.find({},
        (err, result) => (err) ? reject(err) : resolve(result)
    )
);

/**
 * Persiste uma reserva no banco de dados.
 *
 * @param {Object}   reserva  Reserva a ser persistida.
 */
const _persistirReserva = (reserva) => new Promise(
    (resolve, reject) => reserva.save(
        (err, resultado) => (err) ? reject(err) : resolve(resultado.toObject())
    )
);

const _removerReserva = async reserva => new Promise(
    (resolve, reject) => reserva.remove(
        err => err ? reject(err) : resolve("Deleção efetuada com sucesso.")
    )
);

/**
 * Realiza as validações relacionadas ao horário da reserva.
 * Verifica se o horário da reserva intercepta algum outro
 * para o mesmo dia.
 * Valida se o intervalo de horas é positivo.
 *
 * TODO: Se esse método crescer, criar um validador. @author Eric Breno
 *
 * @param {Reserva} reserva Reserva a ser validada.
 */
const _validarHorario = reserva => new Promise((resolve, reject) => {
    const intervaloNegativo = reserva.inicio >= reserva.fim;
    if (intervaloNegativo) return reject("Intervalo de horários inválido.");

    return Reserva.findByDayAndLocalId(reserva.dia, reserva.localId, (err, reservas) => {
        if (err) return reject(err);
        for (let i in reservas) {
            const r = reservas[i];
            if (r._id.toString() === reserva._id) {
                continue;
            }
            if (hasChoqueHorario(r, reserva)) {
                return reject("Horário ocupado.");
            }
        }
        return resolve(null);
    });
});

const _getReservaById = (token, idReserva) => new Promise(async(resolve, reject) => {
    const usuario = await UsersService.getUser(token);
    return Reserva.findById(usuario.email, idReserva, (err, result) => (err) ? reject(err) : resolve(result));
});

/**
 * Verifica se as duas reservas têm choque de horários.
 *
 * * Validações da reserva atualizada AT com a reserva já salva BD:
 * 1- Intervalo de AT tem início de BD.
 *         BD            AT
 *  * 01:00-02:00 <> 00:30-01:30
 *  * 01:00-02:00 <> 00:30-02:30
 *
 * 2- Intervalo de AT tem fim de BD.
 *         BD            AT
 *  * 01:00-02:00 <> 01:30-02:30
 *  * 01:00-02:00 <> 00:30-02:30
 *
 * 3- AT está contida ou tem mesmo intervalo de BD.
 *         BD            AT
 *  * 00:00-01:00 <> 00:30-00:50
 *  * 00:00-01:00 <> 00:00-00:50
 *  * 00:00-01:00 <> 00:30-01:00
 *
 * 4- BD está contida ou tem mesmo intervalo de AT.
 *         BD            AT
 *  * 01:00-02:00 <> 00:30-02:30
 *  * 01:00-02:00 <> 01:00-02:30
 *  * 01:00-02:00 <> 00:30-02:00
 *
 * @param {Reserva} reserva1
 * @param {Reserva} reserva2
 * @return {Boolean} True caso haja choque de horário entre as reservas.
 */
function hasChoqueHorario(reserva1, reserva2) {
    const inicio1 = reserva1.inicio;
    const fim1 = reserva1.fim;
    const inicio2 = reserva2.inicio;
    const fim2 = reserva2.fim;

    const caso1 = inicio1 < inicio2 && fim1 > inicio2;
    const caso2 = inicio1 < fim2 && fim1 > fim2;
    const caso3 = inicio1 >= inicio2 && fim1 <= fim2;
    const caso4 = inicio1 <= inicio2 && fim1 >= fim2;
    return caso1 || caso2 || caso3 || caso4;
}

const ReservasService = {

    /**
     * Obtém todas as reservas.
     *
     * @param {String}   token    Token de identificação do usuário logado.
     */
    async getReservas(token) {
        await UsersService.getUser(token);
        return _consultarReservas();
    },

    /**
     * Obtém todas as reservas do local que teve o id especificado.
     *
     * @param {String}   token    Token de identificação do usuário logado.
     * @param {String}   localId  Id do local a ter suas reservas retornadas.
     */
    async getReservasDoLocal(token, localId) {
        await UsersService.getUser(token);
        return _consultarReservas({
            localId: localId
        });
    },

    /**
     * Cria uma nova reserva em nosso banco de dados.
     *
     * @param {String}   token    Token de identificação do usuário logado.
     * @param {Object}   reserva  Nova reserva a ser persistida.
     */
    async salvarReserva(token, reserva) {
        const usuario = await UsersService.getUser(token);
        await LocaisService.consultarLocalPorId(reserva.localId);

        reserva.emailAutor = usuario.email;
        reserva.userId = usuario.user_id;
        reserva.autor = usuario.user_metadata.nome_completo;

        await _validarHorario(reserva);
        return _persistirReserva(new Reserva(reserva));
    },

    /**
     * Atualiza as propriedades de uma reserva já
     * existente em nosso banco de dados.
     *
     * @param {String}   token       Token de identificação do usuário logado.
     * @param {String}   idReserva   Id da reserva original.
     * @param {Object}   novaReserva Reserva atualizada a ser persistida.
     */
    async atualizarReserva(token, idReserva, novaReserva) {
        const reserva = await _getReservaById(token, idReserva);
        let reservaAntiga = reserva;
        // FIXME: Desfazer isso quando passarmos a utilizar
        // patch corretamente. Necessário por a atualização
        // atual assumir que deve deletar propriedades que não
        // foram enviadas do cliente, e estas não são realmente enviadas nunca.
        // @author Eric Breno - 29/07/17
        novaReserva.emailAutor = reservaAntiga.emailAutor;
        novaReserva.userId = reservaAntiga.userId;
        novaReserva.autor = reservaAntiga.autor;

        await _validarHorario(novaReserva);
        _.updateModel(reservaAntiga, novaReserva);
        return _persistirReserva(reservaAntiga);
    },

    /**
     * Obtém uma reserva dado o seu Id.
     *
     * @param {String}   token      Token de identificação do usuário logado.
     * @param {String}   idReserva  Id da reserva desejada.
     */
    async getReservaById(token, idReserva) {
        const reserva = await _getReservaById(token, idReserva);
        return (reserva || {}).toObject();
    },

    /**
     * Deleta uma reserva dado o seu Id.
     *
     * @param {String}   token     Token de identificação do usuário logado.
     * @param {String}   idReserva Id da reserva a ser deletada.
     */
    async deletarReserva(token, idReserva) {
        //TODO: Validar deleção
        const reserva = await _getReservaById(token, idReserva);
        return _removerReserva(reserva);
    }
};

export {
    ReservasService
};<commit_msg>JSDoc<commit_after>import {
    LocaisService
} from "./LocaisService";
import {
    UsersService
} from "./usersService";

import Reserva from '../model/Reserva';
import _ from '../util/util';

require('../config/db_config')();

/**
 * Consulta uma reserva dados os parâmetros de busca.
 *
 * @param   {Object}           [params = {}] Parâmetros da consulta de reservas.
 * @returns {Promise.<Object>} Promise resolvida com reserva consultada diretamente pelo Mongoose.
 * @private
 */
const _consultarReservas = (params = {}) => new Promise(
    (resolve, reject) => Reserva.find({},
        (err, result) => (err) ? reject(err) : resolve(result)
    )
);

/**
 * Persiste uma reserva no banco de dados.
 *
 * @param   {Reserva}          reserva  Reserva a ser persistida (Schema).
 * @returns {Promise.<Object>} Promise resolvida com reserva persistida (objeto extraído do mongoose).
 * @private
 */
const _persistirReserva = reserva => new Promise(
    (resolve, reject) => reserva.save(
        (err, resultado) => (err) ? reject(err) : resolve(resultado.toObject())
    )
);

/**
 * Remove uma reserva do banco de dados.
 *
 * @param   {Reserva}           reserva  Reserva a ser removida (Schema).
 * @returns {Promise.<Object>} Promise resolvida com mensagem de remoção com sucesso.
 * @private
 */
const _removerReserva = reserva => new Promise(
    (resolve, reject) => reserva.remove(
        err => err ? reject(err) : resolve("Remoção efetuada com sucesso.")
    )
);

/**
 * Realiza as validações relacionadas ao horário da reserva.
 * Verifica se o horário da reserva intercepta algum outro
 * para o mesmo dia.
 * Valida se o intervalo de horas é positivo.
 *
 * TODO: Se esse método crescer, criar um validador. @author Eric Breno
 *
 * @param   {Reserva}                 reserva Reserva a ser validada.
 * @returns {Promise.<String | null>} Promise resolvida com null ou rejeitada com mensagem de erro.
 * @private
 */
const _validarHorario = reserva => new Promise((resolve, reject) => {
    const intervaloNegativo = reserva.inicio >= reserva.fim;
    if (intervaloNegativo) return reject("Intervalo de horários inválido.");

    return Reserva.findByDayAndLocalId(reserva.dia, reserva.localId, (err, reservas) => {
        if (err) return reject(err);
        for (let i in reservas) {
            const r = reservas[i];
            if (r._id.toString() === reserva._id) {
                continue;
            }
            if (hasChoqueHorario(r, reserva)) {
                return reject("Horário ocupado.");
            }
        }
        return resolve(null);
    });
});

// TODO: Atualizar JSDoc daqui pra baixo
const _getReservaById = (token, idReserva) => new Promise(async(resolve, reject) => {
    const usuario = await UsersService.getUser(token);
    return Reserva.findById(usuario.email, idReserva,
        (err, result) => err ? reject(err) : resolve(result)
    );
});

/**
 * Verifica se as duas reservas têm choque de horários.
 *
 * * Validações da reserva atualizada AT com a reserva já salva BD:
 * 1- Intervalo de AT tem início de BD.
 *         BD            AT
 *  * 01:00-02:00 <> 00:30-01:30
 *  * 01:00-02:00 <> 00:30-02:30
 *
 * 2- Intervalo de AT tem fim de BD.
 *         BD            AT
 *  * 01:00-02:00 <> 01:30-02:30
 *  * 01:00-02:00 <> 00:30-02:30
 *
 * 3- AT está contida ou tem mesmo intervalo de BD.
 *         BD            AT
 *  * 00:00-01:00 <> 00:30-00:50
 *  * 00:00-01:00 <> 00:00-00:50
 *  * 00:00-01:00 <> 00:30-01:00
 *
 * 4- BD está contida ou tem mesmo intervalo de AT.
 *         BD            AT
 *  * 01:00-02:00 <> 00:30-02:30
 *  * 01:00-02:00 <> 01:00-02:30
 *  * 01:00-02:00 <> 00:30-02:00
 *
 * @param {Reserva} reserva1
 * @param {Reserva} reserva2
 * @return {Boolean} True caso haja choque de horário entre as reservas.
 */
function hasChoqueHorario(reserva1, reserva2) {
    const inicio1 = reserva1.inicio;
    const fim1 = reserva1.fim;
    const inicio2 = reserva2.inicio;
    const fim2 = reserva2.fim;

    const caso1 = inicio1 < inicio2 && fim1 > inicio2;
    const caso2 = inicio1 < fim2 && fim1 > fim2;
    const caso3 = inicio1 >= inicio2 && fim1 <= fim2;
    const caso4 = inicio1 <= inicio2 && fim1 >= fim2;
    return caso1 || caso2 || caso3 || caso4;
}

export const ReservasService = {

    /**
     * Obtém todas as reservas.
     *
     * @param {String}   token    Token de identificação do usuário logado.
     */
    async getReservas(token) {
        await UsersService.getUser(token);
        return _consultarReservas();
    },

    /**
     * Obtém todas as reservas do local que teve o id especificado.
     *
     * @param {String}   token    Token de identificação do usuário logado.
     * @param {String}   localId  Id do local a ter suas reservas retornadas.
     */
    async getReservasDoLocal(token, localId) {
        await UsersService.getUser(token);
        return _consultarReservas({
            localId: localId
        });
    },

    /**
     * Cria uma nova reserva em nosso banco de dados.
     *
     * @param {String}   token    Token de identificação do usuário logado.
     * @param {Object}   reserva  Nova reserva a ser persistida.
     */
    async salvarReserva(token, reserva) {
        const usuario = await UsersService.getUser(token);
        await LocaisService.consultarLocalPorId(reserva.localId);

        reserva.emailAutor = usuario.email;
        reserva.userId = usuario.user_id;
        reserva.autor = usuario.user_metadata.nome_completo;

        await _validarHorario(reserva);
        return _persistirReserva(new Reserva(reserva));
    },

    /**
     * Atualiza as propriedades de uma reserva já
     * existente em nosso banco de dados.
     *
     * @param {String}   token       Token de identificação do usuário logado.
     * @param {String}   idReserva   Id da reserva original.
     * @param {Object}   novaReserva Reserva atualizada a ser persistida.
     */
    async atualizarReserva(token, idReserva, novaReserva) {
        const reserva = await _getReservaById(token, idReserva);
        let reservaAntiga = reserva;
        // FIXME: Desfazer isso quando passarmos a utilizar
        // patch corretamente. Necessário por a atualização
        // atual assumir que deve deletar propriedades que não
        // foram enviadas do cliente, e estas não são realmente enviadas nunca.
        // @author Eric Breno - 29/07/17
        novaReserva.emailAutor = reservaAntiga.emailAutor;
        novaReserva.userId = reservaAntiga.userId;
        novaReserva.autor = reservaAntiga.autor;

        await _validarHorario(novaReserva);
        _.updateModel(reservaAntiga, novaReserva);
        return _persistirReserva(reservaAntiga);
    },

    /**
     * Obtém uma reserva dado o seu Id.
     *
     * @param {String}   token      Token de identificação do usuário logado.
     * @param {String}   idReserva  Id da reserva desejada.
     */
    async getReservaById(token, idReserva) {
        const reserva = await _getReservaById(token, idReserva);
        return (reserva || {}).toObject();
    },

    /**
     * Deleta uma reserva dado o seu Id.
     *
     * @param {String}   token     Token de identificação do usuário logado.
     * @param {String}   idReserva Id da reserva a ser deletada.
     */
    async deletarReserva(token, idReserva) {
        //TODO: Validar deleção
        const reserva = await _getReservaById(token, idReserva);
        return _removerReserva(reserva);
    }
};
<|endoftext|>


<commit_before>/**
 * Copyright 2013-2015, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule RelayTypes
 * @flow
 * @typechecks
 */

'use strict';

/**
 * Types that Relay framework users may find useful.
 */
import type URI from 'URI';
import type {
  DataID,
  FieldValue,
  Record,
  Records,
  RootCallMap
} from 'RelayInternalTypes';
import type RelayFragmentReference from 'RelayFragmentReference';
import type RelayMetaRoute from 'RelayMetaRoute';
import type RelayMutationTransaction from 'RelayMutationTransaction';
import type RelayQuery from 'RelayQuery';

// Routes and variables
export type Variables = {[name: string]: mixed};

// Ready state
export type ComponentReadyState = {
  aborted: boolean;
  done: boolean;
  error: ?Error;
  mounted: boolean;
  ready: boolean;
  stale: boolean;
};
export type ComponentReadyStateChangeCallback =
  (readyState: ComponentReadyState) => void;

export type ComponentFetchState = {
  done: boolean;
  stale: boolean;
};

export type ReadyState = {
  aborted: boolean;
  done: boolean;
  error: ?Error;
  ready: boolean;
  stale: boolean;
};
export type ReadyStateChangeCallback = (readyState: ReadyState) => void;

// Containers
export type RelayContainer = ReactClass<any, any, any>;

export type RelayProp = {
  forceFetch: (
    partialVariables?: ?Variables,
    callback?: ?ComponentReadyStateChangeCallback
  ) => void,
  getFragmentError: (
    fragmentReference: RelayFragmentReference,
    record: Object
  ) => ?Error,
  getPendingTransactions: (record: Object) => ?Array<RelayMutationTransaction>,
  hasFragmentData: (
    fragmentReference: RelayFragmentReference,
    record: Object
  ) => boolean,
  hasOptimisticUpdate: (
    record: Object
  ) => boolean,
  route: RelayMetaRoute,
  setVariables: (
    partialVariables?: ?Variables,
    callback?: ?ComponentReadyStateChangeCallback
  ) => void,
  variables: Variables,
};

// Mutations
export type RelayMutationTransactionCommitFailureCallback = (
  transaction: RelayMutationTransaction,
  preventAutoRollback: () => void,
) => void;
export type RelayMutationTransactionCommitSuccessCallback = (
  response: {[key: string]: Object}
) => void;
export type RelayMutationTransactionCommitCallbacks = {
  onFailure?: ?RelayMutationTransactionCommitFailureCallback;
  onSuccess?: ?RelayMutationTransactionCommitSuccessCallback;
};
export type RelayMutationConfig = {
  type: 'FIELDS_CHANGE',
  fieldIDs: {[fieldName: string]: DataID | Array<DataID>},
} | {
  type: 'RANGE_ADD',
  parentName: string,
  parentID: string,
  connectionName: string,
  edgeName: string,
  // from GraphQLMutatorConstants.RANGE_OPERATIONS
  rangeBehaviors: {[call: string]: 'append' | 'prepend' | 'remove'},
} | {
  type: 'NODE_DELETE',
  parentName: string;
  parentID: string;
  connectionName: string;
  deletedIDFieldName: string;
} | {
  type: 'RANGE_DELETE';
  parentName: string;
  parentID: string;
  connectionName: string;
  deletedIDFieldName: string;
  pathToConnection: Array<string>;
} | {
  type: 'REQUIRED_CHILDREN',
  children: Array<RelayQuery.Node>,
};

// Observable
export type Observable<T> = {
  subscribe: (callbacks: SubscriptionCallbacks<T>) => Subscription;
};

export type MultiObservable<T> = {
  subscribe: (callbacks: SubscriptionCallbacks<Array<T>>) => Subscription;
  setDataIDs: (dataIDs: Array<DataID>) => void;
};

export type Subscription = {
  dispose(): void;
};

export type SubscriptionCallbacks<T> = {
  onNext: ((value: T) => void);
  onError: ((error: Error) => void);
  onCompleted: (() => void);
};

// Store
export type StoreReaderData = Object;
export type StoreReaderOptions = {
  traverseFragmentReferences?: boolean;
  traverseGeneratedFields?: boolean;
};

// Disk Cache
export type CacheManager = {
  getMutationWriter: () => CacheWriter;
  getQueryWriter: () => CacheWriter;
  readAllData: (
    cachedRecords: Records,
    rootCallData: RootCallMap,
    callback: Function
  ) => void;
  reset: () => void;
};

export type CacheWriter = {
  writeField: (
    dataID: DataID,
    field: string,
    value: ?FieldValue,
    typeName: ?string
  ) => void;
  writeNode: (dataID: DataID, record: ?Record) => void;
  writeRootCall: (
    storageKey: string,
    identifyingArgValue: string,
    dataID: DataID
  ) => void;
};

// Network requests
export type RequestOptions = {
  data?: ?{[key: string]: mixed};
  errorHandler?: ?(error: XHRErrorData) => void;
  headers?: ?{[key: string]: string};
  method: string;
  rawData?: mixed;
  responseHandler?: ?(
    responseText: string,
    responseHeaders: ?string,
    isComplete: boolean
  ) => void;
  timeout?: ?number;
  timeoutHandler?: ?() => void;
  transportBuilder?: any;
  uri: URI;
};
type XHRErrorData = {
  errorCode: ?string;
  errorMsg: ?string;
  errorType: ?string;
};
export type MutationResult = {
  response: Object;
};
export type QueryResult = {
  error?: ?Error;
  ref_params?: ?{[name: string]: mixed};
  response: Object;
};

// Utility
export type Abortable = {
  abort: () => void;
};
<commit_msg>fix flow type for CacheManager<commit_after>/**
 * Copyright 2013-2015, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule RelayTypes
 * @flow
 * @typechecks
 */

'use strict';

/**
 * Types that Relay framework users may find useful.
 */
import type URI from 'URI';
import type {
  DataID,
  FieldValue,
  Record,
  Records,
  RootCallMap
} from 'RelayInternalTypes';
import type RelayFragmentReference from 'RelayFragmentReference';
import type RelayMetaRoute from 'RelayMetaRoute';
import type RelayMutationTransaction from 'RelayMutationTransaction';
import type RelayQuery from 'RelayQuery';

// Routes and variables
export type Variables = {[name: string]: mixed};

// Ready state
export type ComponentReadyState = {
  aborted: boolean;
  done: boolean;
  error: ?Error;
  mounted: boolean;
  ready: boolean;
  stale: boolean;
};
export type ComponentReadyStateChangeCallback =
  (readyState: ComponentReadyState) => void;

export type ComponentFetchState = {
  done: boolean;
  stale: boolean;
};

export type ReadyState = {
  aborted: boolean;
  done: boolean;
  error: ?Error;
  ready: boolean;
  stale: boolean;
};
export type ReadyStateChangeCallback = (readyState: ReadyState) => void;

// Containers
export type RelayContainer = ReactClass<any, any, any>;

export type RelayProp = {
  forceFetch: (
    partialVariables?: ?Variables,
    callback?: ?ComponentReadyStateChangeCallback
  ) => void,
  getFragmentError: (
    fragmentReference: RelayFragmentReference,
    record: Object
  ) => ?Error,
  getPendingTransactions: (record: Object) => ?Array<RelayMutationTransaction>,
  hasFragmentData: (
    fragmentReference: RelayFragmentReference,
    record: Object
  ) => boolean,
  hasOptimisticUpdate: (
    record: Object
  ) => boolean,
  route: RelayMetaRoute,
  setVariables: (
    partialVariables?: ?Variables,
    callback?: ?ComponentReadyStateChangeCallback
  ) => void,
  variables: Variables,
};

// Mutations
export type RelayMutationTransactionCommitFailureCallback = (
  transaction: RelayMutationTransaction,
  preventAutoRollback: () => void,
) => void;
export type RelayMutationTransactionCommitSuccessCallback = (
  response: {[key: string]: Object}
) => void;
export type RelayMutationTransactionCommitCallbacks = {
  onFailure?: ?RelayMutationTransactionCommitFailureCallback;
  onSuccess?: ?RelayMutationTransactionCommitSuccessCallback;
};
export type RelayMutationConfig = {
  type: 'FIELDS_CHANGE',
  fieldIDs: {[fieldName: string]: DataID | Array<DataID>},
} | {
  type: 'RANGE_ADD',
  parentName: string,
  parentID: string,
  connectionName: string,
  edgeName: string,
  // from GraphQLMutatorConstants.RANGE_OPERATIONS
  rangeBehaviors: {[call: string]: 'append' | 'prepend' | 'remove'},
} | {
  type: 'NODE_DELETE',
  parentName: string;
  parentID: string;
  connectionName: string;
  deletedIDFieldName: string;
} | {
  type: 'RANGE_DELETE';
  parentName: string;
  parentID: string;
  connectionName: string;
  deletedIDFieldName: string;
  pathToConnection: Array<string>;
} | {
  type: 'REQUIRED_CHILDREN',
  children: Array<RelayQuery.Node>,
};

// Observable
export type Observable<T> = {
  subscribe: (callbacks: SubscriptionCallbacks<T>) => Subscription;
};

export type MultiObservable<T> = {
  subscribe: (callbacks: SubscriptionCallbacks<Array<T>>) => Subscription;
  setDataIDs: (dataIDs: Array<DataID>) => void;
};

export type Subscription = {
  dispose(): void;
};

export type SubscriptionCallbacks<T> = {
  onNext: ((value: T) => void);
  onError: ((error: Error) => void);
  onCompleted: (() => void);
};

// Store
export type StoreReaderData = Object;
export type StoreReaderOptions = {
  traverseFragmentReferences?: boolean;
  traverseGeneratedFields?: boolean;
};

// Disk Cache
export type CacheManager = {
  clear: () => void;
  getMutationWriter: () => CacheWriter;
  getQueryWriter: () => CacheWriter;
  readAllData: (
    cachedRecords: Records,
    rootCallData: RootCallMap,
    callback: Function
  ) => void;
};

export type CacheWriter = {
  writeField: (
    dataID: DataID,
    field: string,
    value: ?FieldValue,
    typeName: ?string
  ) => void;
  writeNode: (dataID: DataID, record: ?Record) => void;
  writeRootCall: (
    storageKey: string,
    identifyingArgValue: string,
    dataID: DataID
  ) => void;
};

// Network requests
export type RequestOptions = {
  data?: ?{[key: string]: mixed};
  errorHandler?: ?(error: XHRErrorData) => void;
  headers?: ?{[key: string]: string};
  method: string;
  rawData?: mixed;
  responseHandler?: ?(
    responseText: string,
    responseHeaders: ?string,
    isComplete: boolean
  ) => void;
  timeout?: ?number;
  timeoutHandler?: ?() => void;
  transportBuilder?: any;
  uri: URI;
};
type XHRErrorData = {
  errorCode: ?string;
  errorMsg: ?string;
  errorType: ?string;
};
export type MutationResult = {
  response: Object;
};
export type QueryResult = {
  error?: ?Error;
  ref_params?: ?{[name: string]: mixed};
  response: Object;
};

// Utility
export type Abortable = {
  abort: () => void;
};
<|endoftext|>


<commit_before>describe 'Express'
  before_each
    Express.routes = []
  end

  describe '.version'
    it 'should be properly formatted'
      Express.version.should.match /\d+\.\d+\.\d+/
    end
  end

  describe '.parseParams()'
    it 'should parse a string of parameters'
      string = 'user[name]=tj&user[pass]=test&foo=some bar stuff&cookies=awesome'
      params = Express.parseParams(string)
      params.user.name.should.eql 'tj'
      params.user.pass.should.eql 'test'
      params.foo.should.eql 'some bar stuff'
      params.cookies.should.eql 'awesome'
    end
  end

  describe '.parseNestedParams()'
    it 'should parse nested params hash provided by node'
      params = { 'foo' : 'bar', 'user[name]' : 'tj', 'user[info][email]' : 'tj@vision-media.ca', 'user[info][city]' : 'Victoria' }
      nested = Express.parseNestedParams(params)
      nested.foo.should.eql 'bar'
      nested.user.name.should.eql 'tj'
      nested.user.info.email.should.eql 'tj@vision-media.ca'
      nested.user.info.city.should.eql 'Victoria'
    end
  end

  describe '.parseCookie()'
    it 'should parse cookie pairs'
      var cookie = 'expires=Fri, 31-Dec-2010 23:59:59 GMT; path=/; q=foo%3dbar; domain=example.net'
      parts = Express.parseCookie(cookie)
      parts.expires.should.eql 'Fri, 31-Dec-2010 23:59:59 GMT'
      parts.path.should.eql '/'
      parts.q.should.eql 'foo=bar'
      parts.domain.should.eql 'example.net'
    end
  end

  describe '.toArray()'
    it 'should return an array of arguments'
      Express.toArray(-{ return arguments }('foo', 'bar')).should.eql ['foo', 'bar']
    end

    it 'should allow an offset'
      Express.toArray(-{ return arguments }('foo', 'bar'), 1).should.eql ['bar']
    end
  end

  describe '.escape()'
    it 'should escape html'
      Express.escape('<>&""').should.eql '<>&""'
    end
  end

  describe '.escapeRegexp()'
    it 'should escape regexp special characters'
      Express.escapeRegexp('/users/(name)').should.eql '\\/users\\/\\(name\\)'
    end

    it 'should accept a string of space-delimited characters'
      Express.escapeRegexp('/foo/#bar?user[name]=tj', '/ [ ]').should.eql '\\/foo\\/#bar?user\\[name\\]=tj'
    end
  end

  describe '.contentsOf()'
    it 'should return the body of a function as a string'
      Express.contentsOf(function(){ return 'foo' }).should.include 'return', 'foo'
    end
  end

  describe '.header()'
    it 'should set / get headers'
      Express.header('Content-Type', 'text/html')
      Express.header('Content-Type').should.eql 'text/html'
      Express.header('content-type').should.eql 'text/html'
    end
  end

  describe '.arg()'
    it 'should return the given path segment'
      Express.request = mockRequest({ uri : { path : 'some/foo-bar/baz' }})
      Express.arg(0).should.eql 'some'
      Express.arg(1).should.eql 'foo-bar'
      Express.arg(2).should.eql 'baz'
    end

    it 'should return null when segment does not exist'
      Express.arg(12).should.be_null
    end
  end

  describe '.status()'
    after_each
      Express.response.status = 200
    end

    it 'should set response status code by number'
      Express.status(404)
      Express.response.status.should.eql 404
    end

    it 'should set using the status string map'
      Express.status('Not Found')
      Express.response.status.should.eql 404
    end

    it 'should be case insensitive'
      Express.status('forbidden')
      Express.response.status.should.eql 403
    end
  end

  describe '.halt()'
    after_each
      Express.response.status = 200
    end

    it 'should set response status and body'
      get('/foo', function(){
        halt('Page or file cannot be found', 'Not Found')
      })
      get('/foo').status.should.eql 404
      get('/foo').body.should.eql 'Page or file cannot be found'
    end

    it 'should allow specific status to be passed'
      get('/foo', function(){
        halt('Page or file cannot be found', 'Not Found')
      })
      get('/foo').status.should.eql 404
      get('/foo').body.should.eql 'Page or file cannot be found'
    end
  end

  describe '.redirect()'
    it 'should redirect to the url specified'
      get('foo', function(){ redirect('http://google.com') })
      response = get('foo')
      response.headers.location.should.eql 'http://google.com'
      response.status.should.eql 302
    end
  end

  describe 'RedirectHelpers'
    before_each
      Express.response.headers = {}
    end

    describe '.home'
      it 'should redirect home'
        get('foo', function(){ redirect(home) })
        get('foo').headers.location.should.eql '/'
      end

      it 'should not always set location'
        get('foo', function(){ 'bar' })
        get('foo').headers.should.not.have_property 'location'
      end
    end

    describe '.back'
      it 'should redirect to the referrer'
        uri = 'http://vision-media.ca'
        get('foo', function(){ redirect(back) })
        request = mockRequest({ headers : { 'Referer': uri }})
        get('foo', { request: request }).headers.location.should.eql uri
        Express.back.should.eql uri
      end
    end
  end

  describe 'BodyDecoder'
    it 'should parse urlencoded bodies'
      request = mockRequest({
          method: 'POST',
          headers : { 'content-type': 'application/x-www-form-urlencoded' },
          body : 'user[name]=foo%20bar&status=1'
        })
      post('admin', function(){ param('user').name + ' ' + param('status') })
      post('admin', { request : request }).body.should.eql 'foo bar 1'
    end

    it 'should parse JSON bodies'
      request = mockRequest({
          method: 'POST',
          headers : { 'content-type': 'application/json' },
          body : '{ foo : "bar" }'
        })
      post('foo', function(){ param('foo') })
      post('foo', { request : request }).body.should.eql 'bar'
    end
  end

  describe 'ContentLength'
    it 'should set the content length header'
      get('foo', function(){ 'foo bar' })
      get('foo').headers['content-length'].should.eql 7
    end
  end

  describe 'MethodOverride'
    it 'should override request method when _method param is present'
      request = mockRequest({
          method: 'POST',
          headers : { 'content-type': 'application/x-www-form-urlencoded' },
          body : '_method=delete'
        })
      del('foo', function(){ 'Deleted' })
      post('foo', { request : request}).body.should.eql 'Deleted'
    end
  end

  describe 'DefaultContentType'
    it 'should default to text/html'
      get('foo', function(){ 'bar' })
      get('foo').headers['content-type'].should.eql 'text/html'
    end

    it 'should be overridable'
      get('foo', function(){ header('Content-Type', 'application/json'); 'bar' })
      get('foo').headers['content-type'].should.eql 'application/json'
    end
  end
end<commit_msg>More specs for halt()<commit_after>describe 'Express'
  before_each
    Express.routes = []
  end

  describe '.version'
    it 'should be properly formatted'
      Express.version.should.match /\d+\.\d+\.\d+/
    end
  end

  describe '.parseParams()'
    it 'should parse a string of parameters'
      string = 'user[name]=tj&user[pass]=test&foo=some bar stuff&cookies=awesome'
      params = Express.parseParams(string)
      params.user.name.should.eql 'tj'
      params.user.pass.should.eql 'test'
      params.foo.should.eql 'some bar stuff'
      params.cookies.should.eql 'awesome'
    end
  end

  describe '.parseNestedParams()'
    it 'should parse nested params hash provided by node'
      params = { 'foo' : 'bar', 'user[name]' : 'tj', 'user[info][email]' : 'tj@vision-media.ca', 'user[info][city]' : 'Victoria' }
      nested = Express.parseNestedParams(params)
      nested.foo.should.eql 'bar'
      nested.user.name.should.eql 'tj'
      nested.user.info.email.should.eql 'tj@vision-media.ca'
      nested.user.info.city.should.eql 'Victoria'
    end
  end

  describe '.parseCookie()'
    it 'should parse cookie pairs'
      var cookie = 'expires=Fri, 31-Dec-2010 23:59:59 GMT; path=/; q=foo%3dbar; domain=example.net'
      parts = Express.parseCookie(cookie)
      parts.expires.should.eql 'Fri, 31-Dec-2010 23:59:59 GMT'
      parts.path.should.eql '/'
      parts.q.should.eql 'foo=bar'
      parts.domain.should.eql 'example.net'
    end
  end

  describe '.toArray()'
    it 'should return an array of arguments'
      Express.toArray(-{ return arguments }('foo', 'bar')).should.eql ['foo', 'bar']
    end

    it 'should allow an offset'
      Express.toArray(-{ return arguments }('foo', 'bar'), 1).should.eql ['bar']
    end
  end

  describe '.escape()'
    it 'should escape html'
      Express.escape('<>&""').should.eql '<>&""'
    end
  end

  describe '.escapeRegexp()'
    it 'should escape regexp special characters'
      Express.escapeRegexp('/users/(name)').should.eql '\\/users\\/\\(name\\)'
    end

    it 'should accept a string of space-delimited characters'
      Express.escapeRegexp('/foo/#bar?user[name]=tj', '/ [ ]').should.eql '\\/foo\\/#bar?user\\[name\\]=tj'
    end
  end

  describe '.contentsOf()'
    it 'should return the body of a function as a string'
      Express.contentsOf(function(){ return 'foo' }).should.include 'return', 'foo'
    end
  end

  describe '.header()'
    it 'should set / get headers'
      Express.header('Content-Type', 'text/html')
      Express.header('Content-Type').should.eql 'text/html'
      Express.header('content-type').should.eql 'text/html'
    end
  end

  describe '.arg()'
    it 'should return the given path segment'
      Express.request = mockRequest({ uri : { path : 'some/foo-bar/baz' }})
      Express.arg(0).should.eql 'some'
      Express.arg(1).should.eql 'foo-bar'
      Express.arg(2).should.eql 'baz'
    end

    it 'should return null when segment does not exist'
      Express.arg(12).should.be_null
    end
  end

  describe '.status()'
    after_each
      Express.response.status = 200
    end

    it 'should set response status code by number'
      Express.status(404)
      Express.response.status.should.eql 404
    end

    it 'should set using the status string map'
      Express.status('Not Found')
      Express.response.status.should.eql 404
    end

    it 'should be case insensitive'
      Express.status('forbidden')
      Express.response.status.should.eql 403
    end
  end

  describe '.halt()'
    after_each
      Express.response.status = 200
    end

    it 'should default response status to 200'
      get('/foo', function(){
        halt('Foo bar')
      })
      get('/foo').status.should.eql 200
      get('/foo').body.should.eql 'Foo bar'
    end

    it 'should set response status and body'
      get('/foo', function(){
        halt('Page or file cannot be found', 'Not Found')
      })
      get('/foo').status.should.eql 404
      get('/foo').body.should.eql 'Page or file cannot be found'
    end

    it 'should allow specific status to be passed'
      get('/foo', function(){
        halt('Page or file cannot be found', 'Not Found')
      })
      get('/foo').status.should.eql 404
      get('/foo').body.should.eql 'Page or file cannot be found'
    end
  end

  describe '.redirect()'
    it 'should redirect to the url specified'
      get('foo', function(){ redirect('http://google.com') })
      response = get('foo')
      response.headers.location.should.eql 'http://google.com'
      response.status.should.eql 302
    end
  end

  describe 'RedirectHelpers'
    before_each
      Express.response.headers = {}
    end

    describe '.home'
      it 'should redirect home'
        get('foo', function(){ redirect(home) })
        get('foo').headers.location.should.eql '/'
      end

      it 'should not always set location'
        get('foo', function(){ 'bar' })
        get('foo').headers.should.not.have_property 'location'
      end
    end

    describe '.back'
      it 'should redirect to the referrer'
        uri = 'http://vision-media.ca'
        get('foo', function(){ redirect(back) })
        request = mockRequest({ headers : { 'Referer': uri }})
        get('foo', { request: request }).headers.location.should.eql uri
        Express.back.should.eql uri
      end
    end
  end

  describe 'BodyDecoder'
    it 'should parse urlencoded bodies'
      request = mockRequest({
          method: 'POST',
          headers : { 'content-type': 'application/x-www-form-urlencoded' },
          body : 'user[name]=foo%20bar&status=1'
        })
      post('admin', function(){ param('user').name + ' ' + param('status') })
      post('admin', { request : request }).body.should.eql 'foo bar 1'
    end

    it 'should parse JSON bodies'
      request = mockRequest({
          method: 'POST',
          headers : { 'content-type': 'application/json' },
          body : '{ foo : "bar" }'
        })
      post('foo', function(){ param('foo') })
      post('foo', { request : request }).body.should.eql 'bar'
    end
  end

  describe 'ContentLength'
    it 'should set the content length header'
      get('foo', function(){ 'foo bar' })
      get('foo').headers['content-length'].should.eql 7
    end
  end

  describe 'MethodOverride'
    it 'should override request method when _method param is present'
      request = mockRequest({
          method: 'POST',
          headers : { 'content-type': 'application/x-www-form-urlencoded' },
          body : '_method=delete'
        })
      del('foo', function(){ 'Deleted' })
      post('foo', { request : request}).body.should.eql 'Deleted'
    end
  end

  describe 'DefaultContentType'
    it 'should default to text/html'
      get('foo', function(){ 'bar' })
      get('foo').headers['content-type'].should.eql 'text/html'
    end

    it 'should be overridable'
      get('foo', function(){ header('Content-Type', 'application/json'); 'bar' })
      get('foo').headers['content-type'].should.eql 'application/json'
    end
  end
end<|endoftext|>


<commit_before>/*
 * ------------------------------------------
 * 富媒体编辑器输入区封装实现文件
 * @version  1.0
 * @author   genify(caijf@corp.netease.com)
 * ------------------------------------------
 */
var f = function(){
    var _  = NEJ.P,
        _o = NEJ.O,
        _v = _('nej.v'),
        _e = _('nej.e'),
        _u = _('nej.u'),
        _h = _('nej.h'),
        _p = _('nej.ut'),
        _proEditorArea;
    if (!!_p._$$EditorArea) return;
    /**
     * 富媒体编辑器输入区封装
     * @class   {nej.ut._$$EditorArea} 富媒体编辑器输入区封装
     * @extends {nej.ut._$$Event}
     * @param   {Object} _options 可选配置参数，已处理参数列表如下
     * @config  {Node|String|Function} parent     父节点或者编辑器加入父节点执行函数
     * @config  {String}                style      编辑器初始样式
     * @config  {Boolean}                focus     是否自动聚焦
     * @config  {String}                content     文本内容
     *
     * [hr]
     *
     * @event  {onselectionchange}
     */
    _p._$$EditorArea = NEJ.C();
      _proEditorArea = _p._$$EditorArea._$extend(_p._$$Event);
    /**
     * 控件初始化
     * @protected
     * @method {__init}
     * @return {Void}
     */
    _proEditorArea.__init = function(){
        this.__fopt = {visible:!0
                      ,onload:this.__onIFrameLoaded._$bind(this)};
        this.__supInit();
    };
    /**
     * 控件重置
     * @protected
     * @method {__reset}
     * @param  {Object} 可选配置参数
     * @return {Void}
     */
    _proEditorArea.__reset = (function(){
        var _cnttpl = '';
        return function(_options){
            this.__supReset(_options);
            this.__focus = !!_options.focus;
            this.__content = _cnttpl.replace(
                            '#<UDCS>',_options.style||'');
            this.__initcnt = _options.content||'';
            this.__fopt.parent = _e._$get(
                  _options.parent)||document.body;
            this.__iframe = _e._$createXFrame(this.__fopt);
        };
    })();
    /**
     * 控件销毁
     * @protected
     * @method {__destroy}
     * @return {Void}
     */
    _proEditorArea.__destroy = function(){
        this.__supDestroy();
        _h.__clearRange(this._$getDocument());
        delete this.__content;
        delete this.__fopt.parent;
        delete this.__initcnt;
        delete this.__type;
        this.__iframe = _e._$remove(this.__iframe);
    };
    /**
     * 保存选中状态
     * @protected
     * @method {__doSaveRange}
     * @return {Void}
     */
    _proEditorArea.__doSaveRange = function(){
        _h.__saveRange(this._$getDocument());
    };
    /**
     * iframe载入完成触发事件
     * @protected
     * @method {__onIFrameLoaded}
     * @return {Void}
     */
    _proEditorArea.__onIFrameLoaded = function(_iframe){
        var _document = this._$getDocument();
        _document.open();
        _document.write(this.__content);
        if (location.hostname!=document.domain)
            _document.domain = document.domain;
        _document.close(); // <- will trigger iframe onload
        this.__doInitDomEvent([
            [_document,'click',this.__onDocumentClick._$bind(this)]
           ,[_document,'selectionchange',this.__onSelectionChange._$bind(this)]
           ,[_document,'beforedeactivate',this.__doSaveRange._$bind(this)]
           ,[_document,'keyup',this.__onDocumentEvent._$bind(this)]
           ,[_document.body,'paste',this.__onDocumentEvent._$bind(this)]
           ,[_document.body,'drop',this.__onDocumentEvent._$bind(this)]
        ]);
        // init content and focus
        if (!!this.__initcnt){
            this._$setContent(this.__initcnt);
        }
        if (this.__focus){
            this._$focus();
            delete this.__focus;
        }
    };
    /**
     * 文档点击事件
     * @protected
     * @method {__onDocumentClick}
     * @param  {Event} 事件对象
     * @return {Void}
     */
    _proEditorArea.__onDocumentClick = function(_event){
        _v._$dispatchEvent(document,'click');
    };
    /**
     * 选中内容变化触发事件
     * @protected
     * @method {__onSelectionChange}
     * @return {Void}
     */
    _proEditorArea.__onSelectionChange = function(){
        // TODO something
        this._$dispatchEvent('onselectionchange');
    };
    /**
     * 输入事件
     * @return {[type]} [description]
     */
    _proEditorArea.__onDocumentEvent = function(_event){
        this.__type = _event.type;
        setTimeout(this.__doCompareContent._$bind(this),50);
    };
    /**
     * 比较富文本的内容
     * @return {[type]} [description]
     */
    _proEditorArea.__doCompareContent = function(){
        if(this.__initcnt === this._$getContent()){
            return !1;
        }else{
            this.__initcnt = this._$getContent();
            this._$dispatchEvent('oninput',{cont:this.__initcnt,txt:this._$getTextContent()});
        }
    };
    /**
     * 聚焦编辑器
     * @method {_$focus}
     * @param  {Number} 光标位置，默认为0，0-末尾、1-起始、2-不变
     * @return {nej.ut._$$EditorArea}
     */
    _proEditorArea._$focus = function(_cursor){
        var _document = this._$getDocument();
        if (!_document) return this;
        _h.__focusRange(_document.body);
        _h.__moveCursorPosition(_document.body,
                         parseInt(_cursor)||0);
        this.__onSelectionChange();
        return this;
    };
    /**
     * 取编辑器文档对象
     * @method {_$getDocument}
     * @return {Node} 文档对象
     */
    _proEditorArea._$getDocument = function(){
        return this.__iframe.contentWindow.document;
    };
    /**
     * 取编辑内容
     * @method {_$getContent}
     * @return {String} 内容
     */
    _proEditorArea._$getContent = function(_filter){
        var _document = this._$getDocument();
        _html = _h.__filterContent(!_document?'':_document.body.innerHTML);
        if(!_filter){
            _html = _h.__filterContentStyle(_html);
        }
        return !_h.__filterWordContent?_html:_h.__filterWordContent(_html);
    };
    /**
     * 取纯文本的编辑内容
     * @method {_$getTextContent}
     * @return {String} 内容
     */
    _proEditorArea._$getTextContent = function(){
        var _document = this._$getDocument();
        return !_document?'':_document.body.innerText||_document.body.textContent;
    };
    /**
     * 设置内容
     * @method {_$setContent}
     * @param  {String} 编辑内容
     * @return {nej.ut._$$EditorArea}
     */
    _proEditorArea._$setContent = function(_content){
        var _document = this._$getDocument();
        if (!_document) return this;
        _document.body.innerHTML = _content;
        return this;
    };
    /**
     * 执行编辑命令
     * @method {_$execCommand}
     * @param  {String} 命令名称
     * @param  {String} 命令值
     * @param  {String} 是否通过style的方式来改变样式，比如superscript命令
     * @return {nej.ut._$$EditorArea}
     */
    _proEditorArea._$execCommand = function(_command,_value,_css){
        var _document = this._$getDocument();
        if (!_document) return this;
        _h.__execCommand(_document,'styleWithCSS',false);
        _h.__execCommand(_document,_command,_value);
        this._$focus(2);
        return this;
    };
    /**
     * 查询命令的状态
     * @method {_$queryCommand}
     * @param  {String}   命令名称
     * @param  {String}   查询类型，State/Enabled/Value
     * @return {Variable} 查询结果
     */
    _proEditorArea._$queryCommand = function(_command,_type){
        var _document = this._$getDocument();
        return !_document ? null
               :_document['queryCommand'+_type](_command);
    };
    /**
     * 获取选中内容的文本
     * @method {_$getSelectText}
     * @return {String} 文本内容
     */
    _proEditorArea._$getSelectText = function(){
        this._$focus(2);
        return _h.__getSelectText(this._$getDocument());
    };
    /**
     * 获取选择内容的HTML
     * @method {_$getSelectHtml}
     * @return  {String} HTML代码
     */
    _proEditorArea._$getSelectHtml = function(){
        this._$focus(2);
        return _h.__getSelectHtml(this._$getDocument());
    };
};
NEJ.define('{lib}util/editor/area.js',
      ['{lib}util/event.js'
      ,'{patch}editor.js'],f);<commit_msg>add base to rich editor<commit_after>/*
 * ------------------------------------------
 * 富媒体编辑器输入区封装实现文件
 * @version  1.0
 * @author   genify(caijf@corp.netease.com)
 * ------------------------------------------
 */
var f = function(){
    var _  = NEJ.P,
        _o = NEJ.O,
        _v = _('nej.v'),
        _e = _('nej.e'),
        _u = _('nej.u'),
        _h = _('nej.h'),
        _p = _('nej.ut'),
        _proEditorArea;
    if (!!_p._$$EditorArea) return;
    /**
     * 富媒体编辑器输入区封装
     * @class   {nej.ut._$$EditorArea} 富媒体编辑器输入区封装
     * @extends {nej.ut._$$Event}
     * @param   {Object} _options 可选配置参数，已处理参数列表如下
     * @config  {Node|String|Function} parent  父节点或者编辑器加入父节点执行函数
     * @config  {String}               style   编辑器初始样式
     * @config  {Boolean}              focus   是否自动聚焦
     * @config  {String}               content 文本内容
     *
     * [hr]
     *
     * @event  {onselectionchange}
     */
    _p._$$EditorArea = NEJ.C();
      _proEditorArea = _p._$$EditorArea._$extend(_p._$$Event);
    /**
     * 控件初始化
     * @protected
     * @method {__init}
     * @return {Void}
     */
    _proEditorArea.__init = function(){
        this.__fopt = {
            visible:!0,
            onload:this.__onIFrameLoaded._$bind(this)
        };
        this.__supInit();
    };
    /**
     * 控件重置
     * @protected
     * @method {__reset}
     * @param  {Object} 可选配置参数
     * @return {Void}
     */
    _proEditorArea.__reset = (function(){
        var _cnttpl = '<base href="#<BSUL>"/>';
        return function(_options){
            this.__supReset(_options);
            this.__focus = !!_options.focus;
            this.__content = _cnttpl
                .replace('#<UDCS>',_options.style||'')
                .replace('#<BSUL>',_options.base||location.href);
            this.__initcnt = _options.content||'';
            this.__fopt.parent = _e._$get(
                  _options.parent)||document.body;
            this.__iframe = _e._$createXFrame(this.__fopt);
        };
    })();
    /**
     * 控件销毁
     * @protected
     * @method {__destroy}
     * @return {Void}
     */
    _proEditorArea.__destroy = function(){
        this.__supDestroy();
        _h.__clearRange(this._$getDocument());
        delete this.__content;
        delete this.__fopt.parent;
        delete this.__initcnt;
        delete this.__type;
        this.__iframe = _e._$remove(this.__iframe);
    };
    /**
     * 保存选中状态
     * @protected
     * @method {__doSaveRange}
     * @return {Void}
     */
    _proEditorArea.__doSaveRange = function(){
        _h.__saveRange(this._$getDocument());
    };
    /**
     * iframe载入完成触发事件
     * @protected
     * @method {__onIFrameLoaded}
     * @return {Void}
     */
    _proEditorArea.__onIFrameLoaded = function(_iframe){
        var _document = this._$getDocument();
        _document.open();
        _document.write(this.__content);
        if (location.hostname!=document.domain)
            _document.domain = document.domain;
        _document.close(); // <- will trigger iframe onload
        this.__doInitDomEvent([
            [_document,'click',this.__onDocumentClick._$bind(this)]
           ,[_document,'selectionchange',this.__onSelectionChange._$bind(this)]
           ,[_document,'beforedeactivate',this.__doSaveRange._$bind(this)]
           ,[_document,'keyup',this.__onDocumentEvent._$bind(this)]
           ,[_document.body,'paste',this.__onDocumentEvent._$bind(this)]
           ,[_document.body,'drop',this.__onDocumentEvent._$bind(this)]
        ]);
        // init content and focus
        if (!!this.__initcnt){
            this._$setContent(this.__initcnt);
        }
        if (this.__focus){
            this._$focus();
            delete this.__focus;
        }
    };
    /**
     * 文档点击事件
     * @protected
     * @method {__onDocumentClick}
     * @param  {Event} 事件对象
     * @return {Void}
     */
    _proEditorArea.__onDocumentClick = function(_event){
        _v._$dispatchEvent(document,'click');
    };
    /**
     * 选中内容变化触发事件
     * @protected
     * @method {__onSelectionChange}
     * @return {Void}
     */
    _proEditorArea.__onSelectionChange = function(){
        // TODO something
        this._$dispatchEvent('onselectionchange');
    };
    /**
     * 输入事件
     * @return {[type]} [description]
     */
    _proEditorArea.__onDocumentEvent = function(_event){
        this.__type = _event.type;
        setTimeout(this.__doCompareContent._$bind(this),50);
    };
    /**
     * 比较富文本的内容
     * @return {[type]} [description]
     */
    _proEditorArea.__doCompareContent = function(){
        if(this.__initcnt === this._$getContent()){
            return !1;
        }else{
            this.__initcnt = this._$getContent();
            this._$dispatchEvent('oninput',{cont:this.__initcnt,txt:this._$getTextContent()});
        }
    };
    /**
     * 聚焦编辑器
     * @method {_$focus}
     * @param  {Number} 光标位置，默认为0，0-末尾、1-起始、2-不变
     * @return {nej.ut._$$EditorArea}
     */
    _proEditorArea._$focus = function(_cursor){
        var _document = this._$getDocument();
        if (!_document) return this;
        _h.__focusRange(_document.body);
        _h.__moveCursorPosition(_document.body,
                         parseInt(_cursor)||0);
        this.__onSelectionChange();
        return this;
    };
    /**
     * 取编辑器文档对象
     * @method {_$getDocument}
     * @return {Node} 文档对象
     */
    _proEditorArea._$getDocument = function(){
        return this.__iframe.contentWindow.document;
    };
    /**
     * 取编辑内容
     * @method {_$getContent}
     * @return {String} 内容
     */
    _proEditorArea._$getContent = function(_filter){
        var _document = this._$getDocument();
        _html = _h.__filterContent(!_document?'':_document.body.innerHTML);
        if(!_filter){
            _html = _h.__filterContentStyle(_html);
        }
        return !_h.__filterWordContent?_html:_h.__filterWordContent(_html);
    };
    /**
     * 取纯文本的编辑内容
     * @method {_$getTextContent}
     * @return {String} 内容
     */
    _proEditorArea._$getTextContent = function(){
        var _document = this._$getDocument();
        return !_document?'':_document.body.innerText||_document.body.textContent;
    };
    /**
     * 设置内容
     * @method {_$setContent}
     * @param  {String} 编辑内容
     * @return {nej.ut._$$EditorArea}
     */
    _proEditorArea._$setContent = function(_content){
        var _document = this._$getDocument();
        if (!_document) return this;
        _document.body.innerHTML = _content;
        return this;
    };
    /**
     * 执行编辑命令
     * @method {_$execCommand}
     * @param  {String} 命令名称
     * @param  {String} 命令值
     * @param  {String} 是否通过style的方式来改变样式，比如superscript命令
     * @return {nej.ut._$$EditorArea}
     */
    _proEditorArea._$execCommand = function(_command,_value,_css){
        var _document = this._$getDocument();
        if (!_document) return this;
        _h.__execCommand(_document,'styleWithCSS',false);
        _h.__execCommand(_document,_command,_value);
        this._$focus(2);
        return this;
    };
    /**
     * 查询命令的状态
     * @method {_$queryCommand}
     * @param  {String}   命令名称
     * @param  {String}   查询类型，State/Enabled/Value
     * @return {Variable} 查询结果
     */
    _proEditorArea._$queryCommand = function(_command,_type){
        var _document = this._$getDocument();
        return !_document ? null
               :_document['queryCommand'+_type](_command);
    };
    /**
     * 获取选中内容的文本
     * @method {_$getSelectText}
     * @return {String} 文本内容
     */
    _proEditorArea._$getSelectText = function(){
        this._$focus(2);
        return _h.__getSelectText(this._$getDocument());
    };
    /**
     * 获取选择内容的HTML
     * @method {_$getSelectHtml}
     * @return  {String} HTML代码
     */
    _proEditorArea._$getSelectHtml = function(){
        this._$focus(2);
        return _h.__getSelectHtml(this._$getDocument());
    };
};
NEJ.define('{lib}util/editor/area.js',
      ['{lib}util/event.js'
      ,'{patch}editor.js'],f);<|endoftext|>