Skip to content

Commit

Permalink
feat: allow/disallow remote urls
Browse files Browse the repository at this point in the history
  • Loading branch information
farnabaz committed Nov 17, 2020
1 parent 3f277a3 commit 0856343
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 7 deletions.
1 change: 1 addition & 0 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default {
'@nuxt/typescript-build'
],
image: {
accept: ['nuxtjs.org'],
twicpics: {
baseURL: 'https://i5acur1u.twic.pics'
},
Expand Down
3 changes: 3 additions & 0 deletions playground/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<h2>SVG image inside project</h2>
<NuxtImg src="images/nuxt-white.svg" width="400" height="400" />

<h2>SVG image from remote url</h2>
<NuxtImg src="https://nuxtjs.org/logos/nuxt.svg" width="400" height="400" />

<h2>JPEG image inside project</h2>
<NuxtImg src="/images/damavand.jpg" />

Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ async function imageModule (moduleOptions: ModuleOptions) {
providers: {}, // user custom providers
presets: [],
intersectOptions: {},
accept: 'nuxtjs.org',
sizes: [320, 420, 768, 1024, 1200, 1600],
...nuxt.options.image,
...moduleOptions
Expand All @@ -24,7 +25,8 @@ async function imageModule (moduleOptions: ModuleOptions) {
intersectOptions: options.intersectOptions,
imports: {} as { [name: string]: string },
providers: [] as { name: string, import: string, options: any }[],
presets: options.presets
presets: options.presets,
allow: options.accept
}

const providers = await getProviders(nuxt, options)
Expand Down
59 changes: 59 additions & 0 deletions src/runtime/allowlist.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { AllowList } from 'types'

const REGEX_RULES = [
{ matcher: /[\\$.|*+(){^]/g, replacer: match => `\\${match}` }
]

const regexCache = {}
function makeRegex (pattern: string | RegExp, ignorecase: boolean): RegExp {
if (pattern instanceof RegExp) {
return pattern
}

if (!regexCache[pattern]) {
const source = REGEX_RULES.reduce(
(prev, { matcher, replacer }) => prev.replace(matcher, replacer),
pattern
)

regexCache[pattern] = ignorecase
? new RegExp(source, 'i')
: new RegExp(source)
}
return regexCache[pattern]
}

function allowFunction (options: any, ignorecase: boolean) {
if (typeof options === 'function') {
return options
}

if (typeof options === 'string') {
return allowFunction([options], ignorecase)
}

if (Array.isArray(options)) {
const patterns = options.map(option => makeRegex(option, ignorecase))
return (value) => {
return !!patterns.some(pattern => pattern.test(value))
}
}
throw new Error('Unsupported options')
}

export function allowList (options, ignorecase: boolean = false): AllowList {
const allow = {
accept: (_value: string) => true,
reject: (_value: string) => false,
allow: (value: string) => allow.accept(value) && !allow.reject(value)
}

if (options && (options.accept || options.reject)) {
allow.accept = options.accept ? allowFunction(options.accept, ignorecase) : allow.accept
allow.reject = options.reject ? allowFunction(options.reject, ignorecase) : allow.reject
} else {
allow.accept = allowFunction(options, ignorecase)
}

return allow
}
23 changes: 18 additions & 5 deletions src/runtime/image.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { CreateImageOptions, ImageModifiers, ImagePreset, ImageSize } from 'types'
import { getMeta } from './meta'
import { cleanDoubleSlashes } from './utils'
import { cleanDoubleSlashes, isRemoteUrl } from './utils'

function processSource (src: string) {
if (!src.includes(':') || src.match('^https?://')) {
if (!src.includes(':') || isRemoteUrl(src)) {
return { src }
}

Expand Down Expand Up @@ -33,7 +33,7 @@ function getCache (context) {
return context.cache
}

export function createImage (context, { providers, defaultProvider, presets, intersectOptions, responsiveSizes }: CreateImageOptions) {
export function createImage (context, { providers, defaultProvider, presets, intersectOptions, responsiveSizes, allow }: CreateImageOptions) {
const presetMap = presets.reduce((map, preset) => {
map[preset.name] = preset
return map
Expand All @@ -59,6 +59,19 @@ export function createImage (context, { providers, defaultProvider, presets, int

function parseImage (source: string, modifiers: ImageModifiers, options: any = {}) {
const { src, provider: sourceProvider, preset: sourcePreset } = processSource(source)
const isRemote = isRemoteUrl(src)

if (isRemote && !allow.accept(src)) {
return {
src,
provider: null,
preset: null,
image: {
url: src,
isStatic: false
}
}
}
const provider = getProvider(sourceProvider || options.provider || defaultProvider)
const preset = getPreset(sourcePreset || options.preset)

Expand All @@ -69,7 +82,7 @@ export function createImage (context, { providers, defaultProvider, presets, int
)

// apply router base & remove double slashes
const base = String(image.url)[0] === '/' ? context.base : ''
const base = isRemoteUrl(image.url) ? '' : context.base
image.url = cleanDoubleSlashes(base + image.url)

return {
Expand Down Expand Up @@ -189,7 +202,7 @@ export function createImage (context, { providers, defaultProvider, presets, int
Object.assign(meta, await image.getMeta())
} else {
const internalUrl = context.ssrContext ? context.ssrContext.internalUrl : ''
const absoluteUrl = image.url[0] === '/' ? internalUrl + image.url : image.url
const absoluteUrl = isRemoteUrl(image.url) ? image.url : internalUrl + image.url
Object.assign(meta, await getMeta(absoluteUrl, getCache(context)))
}

Expand Down
4 changes: 4 additions & 0 deletions src/runtime/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { OperationGeneratorConfig } from 'types'

export function isRemoteUrl (url) {
return !!url.match('^https?://')
}

export function cleanDoubleSlashes (path) {
return path.replace(/(https?:\/\/)|(\/)+/g, '$1$2')
}
Expand Down
5 changes: 4 additions & 1 deletion templates/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Vue from 'vue'
import NuxtImg from '~image/nuxt-img'
import NuxtPicture from '~image/nuxt-picture'
import { createImage } from '~image/image'
import { allowList } from '~image/allowlist'

<%=Object.entries(options.imports).map(([name, path]) => `import ${name} from '${path}'`).join('\n')%>

Expand All @@ -23,6 +24,7 @@ Vue.component(NuxtPicture.name, NuxtPicture)
<% if (features.componentAliases) { %>Vue.component('NImg', NuxtImg)
Vue.component('NPicture', NuxtPicture)<% } %>

const allow = allowList(<%= devalue(options.allow) %>)

// TODO: directly plugin into vue
export default function (context, inject) {
Expand All @@ -31,7 +33,8 @@ export default function (context, inject) {
providers,
presets,
intersectOptions,
responsiveSizes
responsiveSizes,
allow
})

inject('img', image)
Expand Down
1 change: 1 addition & 0 deletions types/module.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface ModuleOptions {
providers: {
[name: string]: any;
}
accept: any;
intersectOptions: object;
}

Expand Down
7 changes: 7 additions & 0 deletions types/runtime.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export interface AllowList {
allow: (valu: string) => boolean;
accept: (valu: string) => boolean;
reject: (valu: string) => boolean;
}

// -- $img() utility --

export interface CreateImageOptions {
Expand All @@ -11,6 +17,7 @@ export interface CreateImageOptions {
defaultProvider: string
intersectOptions: object
responsiveSizes: number[]
allow: AllowList
}

export interface $Image {
Expand Down

0 comments on commit 0856343

Please sign in to comment.