diff --git a/.imgbotconfig b/.imgbotconfig new file mode 100644 index 000000000..6a1dfe1ed --- /dev/null +++ b/.imgbotconfig @@ -0,0 +1,5 @@ +{ + "ignoredFiles": [ + "test/resources/*" + ] +} diff --git a/package.json b/package.json index 0f0dda57a..d5b85fc8c 100644 --- a/package.json +++ b/package.json @@ -180,6 +180,7 @@ "portscanner": "2.2.0", "postcss-loader": "3.0.0", "prismjs": "1.18.0", + "probe-image-size": "5.0.0", "progress-bar-webpack-plugin": "1.12.1", "promise-limit": "2.7.0", "promise-sequential": "1.1.1", diff --git a/src/@types/probe-image-size.d.ts b/src/@types/probe-image-size.d.ts new file mode 100644 index 000000000..665edcf2e --- /dev/null +++ b/src/@types/probe-image-size.d.ts @@ -0,0 +1,27 @@ +declare module 'probe-image-size' { + import { ReadStream } from 'fs'; + + type ProbeOptions = { + retries: 1; + timeout: 30000; + }; + + type ProbeResult = { + width: number; + height: number; + length?: number; + type: string; + mime: string; + wUnits: 'in' | 'mm' | 'cm' | 'pt' | 'pc' | 'px' | 'em' | 'ex'; + hUnits: 'in' | 'mm' | 'cm' | 'pt' | 'pc' | 'px' | 'em' | 'ex'; + url?: string; + }; + + function probeImageSize(src: string | ReadStream, options?: ProbeOptions): Promise; + function probeImageSize(src: string | ReadStream, callback: (err: Error | null, result?: ProbeResult) => void): void; + function probeImageSize(src: string | ReadStream, options: ProbeOptions, callback: (err: Error | null, result?: ProbeResult) => void): void; + + namespace probeImageSize {} // Hack + + export = probeImageSize; +} diff --git a/src/misc/check-svg.ts b/src/misc/check-svg.ts deleted file mode 100644 index 8ddeefede..000000000 --- a/src/misc/check-svg.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as fs from 'fs'; -import isSvg from 'is-svg'; - -export default function(path: string) { - try { - const size = fs.statSync(path).size; - if (size > 1 * 1024 * 1024) return false; - return isSvg(fs.readFileSync(path)); - } catch { - return false; - } -} diff --git a/src/misc/detect-mine.ts b/src/misc/detect-mine.ts deleted file mode 100644 index f47f12735..000000000 --- a/src/misc/detect-mine.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as fs from 'fs'; -import checkSvg from '../misc/check-svg'; -const FileType = require('file-type'); - -export async function detectMine(path: string) { - return new Promise<[string, string | null]>((res, rej) => { - const readable = fs.createReadStream(path); - readable - .on('error', rej) - .once('data', async (buffer: Buffer) => { - readable.destroy(); - const type = await FileType.fromBuffer(buffer); - if (type) { - if (type.mime == 'application/xml' && checkSvg(path)) { - res(['image/svg+xml', 'svg']); - } else { - res([type.mime, type.ext]); - } - } else if (checkSvg(path)) { - res(['image/svg+xml', 'svg']); - } else { - // 種類が同定できなかったら application/octet-stream にする - res(['application/octet-stream', null]); - } - }) - .on('end', () => { - // maybe 0 bytes - res(['application/octet-stream', null]); - }); - }); -} diff --git a/src/misc/detect-url-mine.ts b/src/misc/detect-url-mime.ts similarity index 57% rename from src/misc/detect-url-mine.ts rename to src/misc/detect-url-mime.ts index eef64cfc5..8d71cd013 100644 --- a/src/misc/detect-url-mine.ts +++ b/src/misc/detect-url-mime.ts @@ -1,14 +1,14 @@ import { createTemp } from './create-temp'; import { downloadUrl } from './donwload-url'; -import { detectMine } from './detect-mine'; +import { detectType } from './get-file-info'; -export async function detectUrlMine(url: string) { +export async function detectUrlMime(url: string) { const [path, cleanup] = await createTemp(); try { await downloadUrl(url, path); - const [type] = await detectMine(path); - return type; + const { mime } = await detectType(path); + return mime; } finally { cleanup(); } diff --git a/src/misc/get-file-info.ts b/src/misc/get-file-info.ts new file mode 100644 index 000000000..5ccb28026 --- /dev/null +++ b/src/misc/get-file-info.ts @@ -0,0 +1,201 @@ +import * as fs from 'fs'; +import * as crypto from 'crypto'; +import * as fileType from 'file-type'; +import isSvg from 'is-svg'; +import * as probeImageSize from 'probe-image-size'; +import * as sharp from 'sharp'; + +export type FileInfo = { + size: number; + md5: string; + type: { + mime: string; + ext: string | null; + }; + width?: number; + height?: number; + avgColor?: number[]; + warnings: string[]; +}; + +const TYPE_OCTET_STREAM = { + mime: 'application/octet-stream', + ext: null +}; + +const TYPE_SVG = { + mime: 'image/svg+xml', + ext: 'svg' +}; + +/** + * Get file information + */ +export async function getFileInfo(path: string): Promise { + const warnings = [] as string[]; + + const size = await getFileSize(path); + const md5 = await calcHash(path); + + let type = await detectType(path); + + // image dimensions + let width: number | undefined; + let height: number | undefined; + + if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/bmp', 'image/tiff', 'image/svg+xml', 'image/vnd.adobe.photoshop'].includes(type.mime)) { + const imageSize = await detectImageSize(path).catch(e => { + warnings.push(`detectImageSize failed: ${e}`); + return undefined; + }); + + // うまく判定できない画像は octet-stream にする + if (!imageSize) { + warnings.push(`cannot detect image dimensions`); + type = TYPE_OCTET_STREAM; + } else if (imageSize.wUnits === 'px') { + width = imageSize.width; + height = imageSize.height; + + // 制限を超えている画像は octet-stream にする + if (imageSize.width > 16383 || imageSize.height > 16383) { + warnings.push(`image dimensions exceeds limits`); + type = TYPE_OCTET_STREAM; + } + } else { + warnings.push(`unsupported unit type: ${imageSize.wUnits}`); + } + } + + // average color + let avgColor: number[] | undefined; + + if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/svg+xml'].includes(type.mime)) { + avgColor = await calcAvgColor(path).catch(e => { + warnings.push(`calcAvgColor failed: ${e}`); + return undefined; + }); + } + + return { + size, + md5, + type, + width, + height, + avgColor, + warnings, + }; +} + +/** + * Detect MIME Type and extension + */ +export async function detectType(path: string) { + // Check 0 byte + const fileSize = await getFileSize(path); + if (fileSize === 0) { + return TYPE_OCTET_STREAM; + } + + const type = await fileType.fromFile(path); + + if (type) { + // XMLはSVGかもしれない + if (type.mime === 'application/xml' && await checkSvg(path)) { + return TYPE_SVG; + } + + return { + mime: type.mime, + ext: type.ext + }; + } + + // 種類が不明でもSVGかもしれない + if (await checkSvg(path)) { + return TYPE_SVG; + } + + // それでも種類が不明なら application/octet-stream にする + return TYPE_OCTET_STREAM; +} + +/** + * Check the file is SVG or not + */ +export async function checkSvg(path: string) { + try { + const size = await getFileSize(path); + if (size > 1 * 1024 * 1024) return false; + return isSvg(fs.readFileSync(path)); + } catch { + return false; + } +} + +/** + * Get file size + */ +export async function getFileSize(path: string): Promise { + return new Promise((res, rej) => { + fs.stat(path, (err, stats) => { + if (err) return rej(err); + res(stats.size); + }); + }); +} + +/** + * Calculate MD5 hash + */ +async function calcHash(path: string): Promise { + return new Promise((res, rej) => { + const readable = fs.createReadStream(path); + const hash = crypto.createHash('md5'); + const chunks: Buffer[] = []; + readable + .on('error', rej) + .pipe(hash) + .on('error', rej) + .on('data', chunk => chunks.push(chunk)) + .on('end', () => { + const buffer = Buffer.concat(chunks); + res(buffer.toString('hex')); + }); + }); +} + +/** + * Detect dimensions of image + */ +async function detectImageSize(path: string): Promise<{ + width: number; + height: number; + wUnits: string; + hUnits: string; +}> { + const readable = fs.createReadStream(path); + const imageSize = await probeImageSize(readable); + readable.destroy(); + return imageSize; +} + +/** + * Calculate average color of image + */ +async function calcAvgColor(path: string): Promise { + const img = sharp(path); + + const info = await (img as any).stats(); + + if (info.isOpaque) { + const r = Math.round(info.channels[0].mean); + const g = Math.round(info.channels[1].mean); + const b = Math.round(info.channels[2].mean); + + return [r, g, b]; + } else { + return [255, 255, 255]; + } +} diff --git a/src/server/api/endpoints/admin/emoji/add.ts b/src/server/api/endpoints/admin/emoji/add.ts index 5345876da..3a17760e5 100644 --- a/src/server/api/endpoints/admin/emoji/add.ts +++ b/src/server/api/endpoints/admin/emoji/add.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; import define from '../../../define'; -import { detectUrlMine } from '../../../../../misc/detect-url-mine'; +import { detectUrlMime } from '../../../../../misc/detect-url-mime'; import { Emojis } from '../../../../../models'; import { genId } from '../../../../../misc/gen-id'; import { getConnection } from 'typeorm'; @@ -46,7 +46,7 @@ export const meta = { }; export default define(meta, async (ps, me) => { - const type = await detectUrlMine(ps.url); + const type = await detectUrlMime(ps.url); const exists = await Emojis.findOne({ name: ps.name, diff --git a/src/server/api/endpoints/admin/emoji/update.ts b/src/server/api/endpoints/admin/emoji/update.ts index f4a01a397..0651b8d28 100644 --- a/src/server/api/endpoints/admin/emoji/update.ts +++ b/src/server/api/endpoints/admin/emoji/update.ts @@ -1,6 +1,6 @@ import $ from 'cafy'; import define from '../../../define'; -import { detectUrlMine } from '../../../../../misc/detect-url-mine'; +import { detectUrlMime } from '../../../../../misc/detect-url-mime'; import { ID } from '../../../../../misc/cafy-id'; import { Emojis } from '../../../../../models'; import { getConnection } from 'typeorm'; @@ -52,7 +52,7 @@ export default define(meta, async (ps) => { if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); - const type = await detectUrlMine(ps.url); + const type = await detectUrlMime(ps.url); await Emojis.update(emoji.id, { updatedAt: new Date(), diff --git a/src/server/file/send-drive-file.ts b/src/server/file/send-drive-file.ts index 228343579..0b1437858 100644 --- a/src/server/file/send-drive-file.ts +++ b/src/server/file/send-drive-file.ts @@ -8,7 +8,7 @@ import { contentDisposition } from '../../misc/content-disposition'; import { DriveFiles } from '../../models'; import { InternalStorage } from '../../services/drive/internal-storage'; import { downloadUrl } from '../../misc/donwload-url'; -import { detectMine } from '../../misc/detect-mine'; +import { detectType } from '../../misc/get-file-info'; import { convertToJpeg, convertToPng } from '../../services/drive/image-processor'; import { GenerateVideoThumbnail } from '../../services/drive/generate-video-thumbnail'; @@ -52,15 +52,15 @@ export default async function(ctx: Koa.Context) { try { await downloadUrl(file.uri, path); - const [type, ext] = await detectMine(path); + const { mime, ext } = await detectType(path); const convertFile = async () => { if (isThumbnail) { - if (['image/jpeg', 'image/webp'].includes(type)) { + if (['image/jpeg', 'image/webp'].includes(mime)) { return await convertToJpeg(path, 498, 280); - } else if (['image/png'].includes(type)) { + } else if (['image/png'].includes(mime)) { return await convertToPng(path, 498, 280); - } else if (type.startsWith('video/')) { + } else if (mime.startsWith('video/')) { return await GenerateVideoThumbnail(path); } } @@ -68,7 +68,7 @@ export default async function(ctx: Koa.Context) { return { data: fs.readFileSync(path), ext, - type, + type: mime, }; }; diff --git a/src/server/proxy/proxy-media.ts b/src/server/proxy/proxy-media.ts index 232b7a09c..6b90e9992 100644 --- a/src/server/proxy/proxy-media.ts +++ b/src/server/proxy/proxy-media.ts @@ -4,7 +4,7 @@ import { serverLogger } from '..'; import { IImage, convertToPng, convertToJpeg } from '../../services/drive/image-processor'; import { createTemp } from '../../misc/create-temp'; import { downloadUrl } from '../../misc/donwload-url'; -import { detectMine } from '../../misc/detect-mine'; +import { detectType } from '../../misc/get-file-info'; export async function proxyMedia(ctx: Koa.Context) { const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url; @@ -15,21 +15,21 @@ export async function proxyMedia(ctx: Koa.Context) { try { await downloadUrl(url, path); - const [type, ext] = await detectMine(path); + const { mime, ext } = await detectType(path); - if (!type.startsWith('image/')) throw 403; + if (!mime.startsWith('image/')) throw 403; let image: IImage; - if ('static' in ctx.query && ['image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng'].includes(type)) { + if ('static' in ctx.query && ['image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng'].includes(mime)) { image = await convertToPng(path, 498, 280); - } else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng'].includes(type)) { + } else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng'].includes(mime)) { image = await convertToJpeg(path, 200, 200); } else { image = { data: fs.readFileSync(path), ext, - type, + type: mime, }; } diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts index 350e4dfe1..7cc710c8b 100644 --- a/src/services/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -1,9 +1,6 @@ -import { Buffer } from 'buffer'; import * as fs from 'fs'; -import * as crypto from 'crypto'; import { v4 as uuid } from 'uuid'; -import * as sharp from 'sharp'; import { publishMainStream, publishDriveStream } from '../stream'; import { deleteFile } from './delete-file'; @@ -12,7 +9,7 @@ import { GenerateVideoThumbnail } from './generate-video-thumbnail'; import { driveLogger } from './logger'; import { IImage, convertToJpeg, convertToWebp, convertToPng } from './image-processor'; import { contentDisposition } from '../../misc/content-disposition'; -import { detectMine } from '../../misc/detect-mine'; +import { getFileInfo } from '../../misc/get-file-info'; import { DriveFiles, DriveFolders, Users, Instances, UserProfiles } from '../../models'; import { InternalStorage } from './internal-storage'; import { DriveFile } from '../../models/entities/drive-file'; @@ -271,41 +268,16 @@ export default async function( uri: string | null = null, sensitive: boolean | null = null ): Promise { - // Calc md5 hash - const calcHash = new Promise((res, rej) => { - const readable = fs.createReadStream(path); - const hash = crypto.createHash('md5'); - const chunks: Buffer[] = []; - readable - .on('error', rej) - .pipe(hash) - .on('error', rej) - .on('data', chunk => chunks.push(chunk)) - .on('end', () => { - const buffer = Buffer.concat(chunks); - res(buffer.toString('hex')); - }); - }); - - // Get file size - const getFileSize = new Promise((res, rej) => { - fs.stat(path, (err, stats) => { - if (err) return rej(err); - res(stats.size); - }); - }); - - const [hash, [mime, ext], size] = await Promise.all([calcHash, detectMine(path), getFileSize]); - - logger.info(`hash: ${hash}, mime: ${mime}, ext: ${ext}, size: ${size}`); + const info = await getFileInfo(path); + logger.info(`${JSON.stringify(info)}`); // detect name - const detectedName = name || (ext ? `untitled.${ext}` : 'untitled'); + const detectedName = name || (info.type.ext ? `untitled.${info.type.ext}` : 'untitled'); if (!force) { // Check if there is a file with the same hash const much = await DriveFiles.findOne({ - md5: hash, + md5: info.md5, userId: user.id, }); @@ -325,7 +297,7 @@ export default async function( logger.debug(`drive usage is ${usage} (max: ${driveCapacity})`); // If usage limit exceeded - if (usage + size > driveCapacity) { + if (usage + info.size > driveCapacity) { if (Users.isLocalUser(user)) { throw new Error('no-free-space'); } else { @@ -351,57 +323,24 @@ export default async function( return driveFolder; }; - const properties: {[key: string]: any} = {}; + const properties: { + width?: number; + height?: number; + avgColor?: string; + } = {}; - let propPromises: Promise[] = []; + if (info.width) { + properties['width'] = info.width; + properties['height'] = info.height; + } - const isImage = ['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/svg+xml'].includes(mime); - - if (isImage) { - const img = sharp(path); - - // Calc width and height - const calcWh = async () => { - logger.debug('calculating image width and height...'); - - // Calculate width and height - const meta = await img.metadata(); - - logger.debug(`image width and height is calculated: ${meta.width}, ${meta.height}`); - - properties['width'] = meta.width; - properties['height'] = meta.height; - }; - - // Calc average color - const calcAvg = async () => { - logger.debug('calculating average color...'); - - try { - const info = await img.stats(); - - if (info.isOpaque) { - const r = Math.round(info.channels[0].mean); - const g = Math.round(info.channels[1].mean); - const b = Math.round(info.channels[2].mean); - - logger.debug(`average color is calculated: ${r}, ${g}, ${b}`); - - properties['avgColor'] = `rgb(${r},${g},${b})`; - } else { - logger.debug(`this image is not opaque so average color is 255, 255, 255`); - - properties['avgColor'] = `rgb(255,255,255)`; - } - } catch (e) { } - }; - - propPromises = [calcWh(), calcAvg()]; + if (info.avgColor) { + properties['avgColor'] = `rgb(${info.avgColor.join(',')}`; } const profile = await UserProfiles.findOne(user.id); - const [folder] = await Promise.all([fetchFolder(), Promise.all(propPromises)]); + const folder = await fetchFolder(); let file = new DriveFile(); file.id = genId(); @@ -436,9 +375,9 @@ export default async function( if (isLink) { try { file.size = 0; - file.md5 = hash; + file.md5 = info.md5; file.name = detectedName; - file.type = mime; + file.type = info.type.mime; file.storedInternal = false; file = await DriveFiles.save(file); @@ -457,7 +396,7 @@ export default async function( } } } else { - file = await (save(file, path, detectedName, mime, hash, size)); + file = await (save(file, path, detectedName, info.type.mime, info.md5, info.size)); } logger.succ(`drive file has been created ${file.id}`); diff --git a/test/get-file-info.ts b/test/get-file-info.ts new file mode 100644 index 000000000..920df0738 --- /dev/null +++ b/test/get-file-info.ts @@ -0,0 +1,152 @@ +/* + * Tests for detection of file information + * + * How to run the tests: + * > TS_NODE_FILES=true npx mocha test/get-file-info.ts --require ts-node/register + * + * To specify test: + * > TS_NODE_FILES=true npx mocha test/get-file-info.ts --require ts-node/register -g 'test name' + */ + +import * as assert from 'assert'; +import { async } from './utils'; +import { getFileInfo } from '../src/misc/get-file-info'; + +describe('Get file info', () => { + it('Empty file', async (async () => { + const path = `${__dirname}/resources/emptyfile`; + const info = await getFileInfo(path); + delete info.warnings; + assert.deepStrictEqual(info, { + size: 0, + md5: 'd41d8cd98f00b204e9800998ecf8427e', + type: { + mime: 'application/octet-stream', + ext: null + }, + width: undefined, + height: undefined, + avgColor: undefined + }); + })); + + it('Generic JPEG', async (async () => { + const path = `${__dirname}/resources/Lenna.jpg`; + const info = await getFileInfo(path); + delete info.warnings; + assert.deepStrictEqual(info, { + size: 25360, + md5: '091b3f259662aa31e2ffef4519951168', + type: { + mime: 'image/jpeg', + ext: 'jpg' + }, + width: 512, + height: 512, + avgColor: [ 181, 99, 106 ] + }); + })); + + it('Generic APNG', async (async () => { + const path = `${__dirname}/resources/anime.png`; + const info = await getFileInfo(path); + delete info.warnings; + assert.deepStrictEqual(info, { + size: 1868, + md5: '08189c607bea3b952704676bb3c979e0', + type: { + mime: 'image/apng', + ext: 'apng' + }, + width: 256, + height: 256, + avgColor: [ 249, 253, 250 ] + }); + })); + + it('Generic AGIF', async (async () => { + const path = `${__dirname}/resources/anime.gif`; + const info = await getFileInfo(path); + delete info.warnings; + assert.deepStrictEqual(info, { + size: 2248, + md5: '32c47a11555675d9267aee1a86571e7e', + type: { + mime: 'image/gif', + ext: 'gif' + }, + width: 256, + height: 256, + avgColor: [ 249, 253, 250 ] + }); + })); + + it('PNG with alpha', async (async () => { + const path = `${__dirname}/resources/with-alpha.png`; + const info = await getFileInfo(path); + delete info.warnings; + assert.deepStrictEqual(info, { + size: 3772, + md5: 'f73535c3e1e27508885b69b10cf6e991', + type: { + mime: 'image/png', + ext: 'png' + }, + width: 256, + height: 256, + avgColor: [ 255, 255, 255 ] + }); + })); + + it('Generic SVG', async (async () => { + const path = `${__dirname}/resources/image.svg`; + const info = await getFileInfo(path); + delete info.warnings; + assert.deepStrictEqual(info, { + size: 505, + md5: 'b6f52b4b021e7b92cdd04509c7267965', + type: { + mime: 'image/svg+xml', + ext: 'svg' + }, + width: 256, + height: 256, + avgColor: [ 255, 255, 255 ] + }); + })); + + it('SVG with XML definition', async (async () => { + // https://github.com/syuilo/misskey/issues/4413 + const path = `${__dirname}/resources/with-xml-def.svg`; + const info = await getFileInfo(path); + delete info.warnings; + assert.deepStrictEqual(info, { + size: 544, + md5: '4b7a346cde9ccbeb267e812567e33397', + type: { + mime: 'image/svg+xml', + ext: 'svg' + }, + width: 256, + height: 256, + avgColor: [ 255, 255, 255 ] + }); + })); + + it('Dimension limit', async (async () => { + const path = `${__dirname}/resources/25000x25000.png`; + const info = await getFileInfo(path); + delete info.warnings; + assert.deepStrictEqual(info, { + size: 75933, + md5: '268c5dde99e17cf8fe09f1ab3f97df56', + type: { + mime: 'application/octet-stream', // do not treat as image + ext: null + }, + width: 25000, + height: 25000, + avgColor: undefined + }); + })); +}); diff --git a/test/resources/25000x25000.png b/test/resources/25000x25000.png new file mode 100644 index 000000000..0ed466692 Binary files /dev/null and b/test/resources/25000x25000.png differ diff --git a/test/resources/anime.gif b/test/resources/anime.gif new file mode 100644 index 000000000..256ba495c Binary files /dev/null and b/test/resources/anime.gif differ diff --git a/test/resources/anime.png b/test/resources/anime.png new file mode 100644 index 000000000..f13600f7a Binary files /dev/null and b/test/resources/anime.png differ diff --git a/test/resources/emptyfile b/test/resources/emptyfile new file mode 100644 index 000000000..e69de29bb diff --git a/test/resources/with-alpha.png b/test/resources/with-alpha.png new file mode 100644 index 000000000..adc8d0180 Binary files /dev/null and b/test/resources/with-alpha.png differ diff --git a/test/resources/with-xml-def.svg b/test/resources/with-xml-def.svg new file mode 100644 index 000000000..90971215a --- /dev/null +++ b/test/resources/with-xml-def.svg @@ -0,0 +1,2 @@ + + diff --git a/yarn.lock b/yarn.lock index d6208cc76..764d48cbe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3038,6 +3038,13 @@ debug-fabulous@1.X: memoizee "0.4.X" object-assign "4.X" +debug@2, debug@^2.2.0, debug@^2.3.3, debug@^2.5.2: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + debug@3.1.0, debug@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" @@ -3059,13 +3066,6 @@ debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: dependencies: ms "^2.1.1" -debug@^2.2.0, debug@^2.3.3, debug@^2.5.2: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - debuglog@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -3110,7 +3110,7 @@ deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= -deepmerge@^4.2.2: +deepmerge@^4.0.0, deepmerge@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== @@ -8040,6 +8040,17 @@ prismjs@1.18.0: optionalDependencies: clipboard "^2.0.0" +probe-image-size@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/probe-image-size/-/probe-image-size-5.0.0.tgz#1b87d20340ab8fcdb4324ec77fbc8a5f53419878" + integrity sha512-V6uBYw5eBc5UVIE7MUZD6Nxg0RYuGDWLDenEn0B1WC6PcTvn1xdQ6HLDDuznefsiExC6rNrCz7mFRBo0f3Xekg== + dependencies: + deepmerge "^4.0.0" + inherits "^2.0.3" + next-tick "^1.0.0" + request "^2.83.0" + stream-parser "~0.3.1" + process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -8652,7 +8663,7 @@ request-stats@3.0.0: http-headers "^3.0.1" once "^1.4.0" -request@2.88.0, request@^2.73.0, request@^2.88.0: +request@2.88.0, request@^2.73.0, request@^2.83.0, request@^2.88.0: version "2.88.0" resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== @@ -9356,6 +9367,13 @@ stream-http@^2.7.2: to-arraybuffer "^1.0.0" xtend "^4.0.0" +stream-parser@~0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/stream-parser/-/stream-parser-0.3.1.tgz#1618548694420021a1182ff0af1911c129761773" + integrity sha1-FhhUhpRCACGhGC/wrxkRwSl2F3M= + dependencies: + debug "2" + stream-shift@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"