From: Chocobozzz Date: Fri, 22 Dec 2017 08:14:50 +0000 (+0100) Subject: Create comment on replied mastodon statutes X-Git-Tag: v0.0.1-alpha~89 X-Git-Url: https://git.immae.eu/?a=commitdiff_plain;h=6d8524702874120a4667269a81a61e3c7c5e300d;p=github%2FChocobozzz%2FPeerTube.git Create comment on replied mastodon statutes --- diff --git a/client/yarn.lock b/client/yarn.lock index 10af86a55..a3928ef40 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2069,8 +2069,8 @@ es5-ext@^0.10.14, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14: es6-symbol "~3.1.1" es5-shim@^4.5.1: - version "4.5.9" - resolved "https://registry.yarnpkg.com/es5-shim/-/es5-shim-4.5.9.tgz#2a1e2b9e583ff5fed0c20a3ee2cbf3f75230a5c0" + version "4.5.10" + resolved "https://registry.yarnpkg.com/es5-shim/-/es5-shim-4.5.10.tgz#b7e17ef4df2a145b821f1497b50c25cf94026205" es6-iterator@^2.0.1, es6-iterator@~2.0.1: version "2.0.3" diff --git a/package.json b/package.json index 71da491be..f6e10d709 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "request": "^2.81.0", "rimraf": "^2.5.4", "safe-buffer": "^5.0.1", + "sanitize-html": "^1.16.3", "scripty": "^1.5.0", "sequelize": "4.25.2", "sequelize-typescript": "^0.6.1", @@ -110,6 +111,7 @@ "@types/node": "^8.0.3", "@types/pem": "^1.9.3", "@types/request": "^2.0.3", + "@types/sanitize-html": "^1.14.0", "@types/sequelize": "^4.0.55", "@types/supertest": "^2.0.3", "@types/validator": "^6.2.0", diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts new file mode 100644 index 000000000..b69aa5d40 --- /dev/null +++ b/server/controllers/api/videos/comment.ts @@ -0,0 +1,88 @@ +// import * as express from 'express' +// import { logger, getFormattedObjects } from '../../../helpers' +// import { +// authenticate, +// ensureUserHasRight, +// videosBlacklistAddValidator, +// videosBlacklistRemoveValidator, +// paginationValidator, +// blacklistSortValidator, +// setBlacklistSort, +// setPagination, +// asyncMiddleware +// } from '../../../middlewares' +// import { BlacklistedVideo, UserRight } from '../../../../shared' +// import { VideoBlacklistModel } from '../../../models/video/video-blacklist' +// +// const videoCommentRouter = express.Router() +// +// videoCommentRouter.get('/:videoId/comment', +// authenticate, +// ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), +// asyncMiddleware(listVideoCommentsThreadsValidator), +// asyncMiddleware(listVideoCommentsThreads) +// ) +// +// videoCommentRouter.post('/:videoId/comment', +// authenticate, +// ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), +// asyncMiddleware(videosBlacklistAddValidator), +// asyncMiddleware(addVideoToBlacklist) +// ) +// +// videoCommentRouter.get('/blacklist', +// authenticate, +// ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), +// paginationValidator, +// blacklistSortValidator, +// setBlacklistSort, +// setPagination, +// asyncMiddleware(listBlacklist) +// ) +// +// videoCommentRouter.delete('/:videoId/blacklist', +// authenticate, +// ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), +// asyncMiddleware(videosBlacklistRemoveValidator), +// asyncMiddleware(removeVideoFromBlacklistController) +// ) +// +// // --------------------------------------------------------------------------- +// +// export { +// videoCommentRouter +// } +// +// // --------------------------------------------------------------------------- +// +// async function addVideoToBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) { +// const videoInstance = res.locals.video +// +// const toCreate = { +// videoId: videoInstance.id +// } +// +// await VideoBlacklistModel.create(toCreate) +// return res.type('json').status(204).end() +// } +// +// async function listBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) { +// const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort) +// +// return res.json(getFormattedObjects(resultList.data, resultList.total)) +// } +// +// async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) { +// const blacklistedVideo = res.locals.blacklistedVideo as VideoBlacklistModel +// +// try { +// await blacklistedVideo.destroy() +// +// logger.info('Video %s removed from blacklist.', res.locals.video.uuid) +// +// return res.sendStatus(204) +// } catch (err) { +// logger.error('Some error while removing video %s from blacklist.', res.locals.video.uuid, err) +// throw err +// } +// } diff --git a/server/helpers/custom-validators/activitypub/activity.ts b/server/helpers/custom-validators/activitypub/activity.ts index c402800a4..f2e137061 100644 --- a/server/helpers/custom-validators/activitypub/activity.ts +++ b/server/helpers/custom-validators/activitypub/activity.ts @@ -6,6 +6,7 @@ import { isActivityPubUrlValid } from './misc' import { isDislikeActivityValid, isLikeActivityValid } from './rate' import { isUndoActivityValid } from './undo' import { isVideoChannelDeleteActivityValid, isVideoChannelUpdateActivityValid } from './video-channels' +import { isVideoCommentCreateActivityValid } from './video-comments' import { isVideoFlagValid, isVideoTorrentCreateActivityValid, @@ -59,7 +60,8 @@ function checkCreateActivity (activity: any) { return isViewActivityValid(activity) || isDislikeActivityValid(activity) || isVideoTorrentCreateActivityValid(activity) || - isVideoFlagValid(activity) + isVideoFlagValid(activity) || + isVideoCommentCreateActivityValid(activity) } function checkUpdateActivity (activity: any) { diff --git a/server/helpers/custom-validators/activitypub/video-comments.ts b/server/helpers/custom-validators/activitypub/video-comments.ts new file mode 100644 index 000000000..489ff27de --- /dev/null +++ b/server/helpers/custom-validators/activitypub/video-comments.ts @@ -0,0 +1,40 @@ +import * as validator from 'validator' +import { exists, isDateValid } from '../misc' +import { isActivityPubUrlValid, isBaseActivityValid } from './misc' +import * as sanitizeHtml from 'sanitize-html' + +function isVideoCommentCreateActivityValid (activity: any) { + return isBaseActivityValid(activity, 'Create') && + isVideoCommentObjectValid(activity.object) +} + +function isVideoCommentObjectValid (comment: any) { + return comment.type === 'Note' && + isActivityPubUrlValid(comment.id) && + sanitizeCommentHTML(comment) && + isCommentContentValid(comment.content) && + isActivityPubUrlValid(comment.inReplyTo) && + isDateValid(comment.published) && + isActivityPubUrlValid(comment.url) +} + +// --------------------------------------------------------------------------- + +export { + isVideoCommentCreateActivityValid +} + +// --------------------------------------------------------------------------- + +function sanitizeCommentHTML (comment: any) { + return sanitizeHtml(comment.content, { + allowedTags: [ 'b', 'i', 'em', 'span', 'a' ], + allowedAttributes: { + 'a': [ 'href' ] + } + }) +} + +function isCommentContentValid (content: any) { + return exists(content) && validator.isLength('' + content, { min: 1 }) +} diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 100a77622..c8b21d10d 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -175,6 +175,9 @@ const CONSTRAINTS_FIELDS = { }, VIDEO_EVENTS: { COUNT: { min: 0 } + }, + COMMENT: { + URL: { min: 3, max: 2000 } // Length } } diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 0b3f695f7..852db68a0 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts @@ -18,6 +18,7 @@ import { VideoModel } from '../models/video/video' import { VideoAbuseModel } from '../models/video/video-abuse' import { VideoBlacklistModel } from '../models/video/video-blacklist' import { VideoChannelModel } from '../models/video/video-channel' +import { VideoCommentModel } from '../models/video/video-comment' import { VideoFileModel } from '../models/video/video-file' import { VideoShareModel } from '../models/video/video-share' import { VideoTagModel } from '../models/video/video-tag' @@ -73,7 +74,8 @@ async function initDatabaseModels (silent: boolean) { VideoFileModel, VideoBlacklistModel, VideoTagModel, - VideoModel + VideoModel, + VideoCommentModel ]) if (!silent) logger.info('Database %s is ready.', dbname) diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index 1ddd817db..102e54b19 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts @@ -1,6 +1,7 @@ import * as Bluebird from 'bluebird' import { ActivityCreate, VideoTorrentObject } from '../../../../shared' import { DislikeObject, VideoAbuseObject, ViewObject } from '../../../../shared/models/activitypub/objects' +import { VideoCommentObject } from '../../../../shared/models/activitypub/objects/video-comment-object' import { VideoRateType } from '../../../../shared/models/videos' import { logger, retryTransactionWrapper } from '../../../helpers' import { sequelizeTypescript } from '../../../initializers' @@ -9,6 +10,7 @@ import { ActorModel } from '../../../models/activitypub/actor' import { TagModel } from '../../../models/video/tag' import { VideoModel } from '../../../models/video/video' import { VideoAbuseModel } from '../../../models/video/video-abuse' +import { VideoCommentModel } from '../../../models/video/video-comment' import { VideoFileModel } from '../../../models/video/video-file' import { getOrCreateActorAndServerAndModel } from '../actor' import { forwardActivity } from '../send/misc' @@ -28,6 +30,8 @@ async function processCreateActivity (activity: ActivityCreate) { return processCreateVideo(actor, activity) } else if (activityType === 'Flag') { return processCreateVideoAbuse(actor, activityObject as VideoAbuseObject) + } else if (activityType === 'Note') { + return processCreateVideoComment(actor, activity) } logger.warn('Unknown activity object type %s when creating activity.', activityType, { activity: activity.id }) @@ -184,7 +188,7 @@ function createVideoDislike (byActor: ActorModel, activity: ActivityCreate) { }) } -async function processCreateView (byAccount: ActorModel, activity: ActivityCreate) { +async function processCreateView (byActor: ActorModel, activity: ActivityCreate) { const view = activity.object as ViewObject const video = await VideoModel.loadByUrlAndPopulateAccount(view.object) @@ -198,7 +202,7 @@ async function processCreateView (byAccount: ActorModel, activity: ActivityCreat if (video.isOwned()) { // Don't resend the activity to the sender - const exceptions = [ byAccount ] + const exceptions = [ byActor ] await forwardActivity(activity, undefined, exceptions) } } @@ -236,3 +240,48 @@ function addRemoteVideoAbuse (actor: ActorModel, videoAbuseToCreateData: VideoAb logger.info('Remote abuse for video uuid %s created', videoAbuseToCreateData.object) }) } + +function processCreateVideoComment (byActor: ActorModel, activity: ActivityCreate) { + const options = { + arguments: [ byActor, activity ], + errorMessage: 'Cannot create video comment with many retries.' + } + + return retryTransactionWrapper(createVideoComment, options) +} + +function createVideoComment (byActor: ActorModel, activity: ActivityCreate) { + const comment = activity.object as VideoCommentObject + const byAccount = byActor.Account + + if (!byAccount) throw new Error('Cannot create video comment with the non account actor ' + byActor.url) + + return sequelizeTypescript.transaction(async t => { + const video = await VideoModel.loadByUrl(comment.inReplyTo, t) + + // This is a new thread + if (video) { + return VideoCommentModel.create({ + url: comment.id, + text: comment.content, + originCommentId: null, + inReplyToComment: null, + videoId: video.id, + actorId: byActor.id + }, { transaction: t }) + } + + const inReplyToComment = await VideoCommentModel.loadByUrl(comment.inReplyTo, t) + if (!inReplyToComment) throw new Error('Unknown replied comment ' + comment.inReplyTo) + + const originCommentId = inReplyToComment.originCommentId || inReplyToComment.id + return VideoCommentModel.create({ + url: comment.id, + text: comment.content, + originCommentId, + inReplyToCommentId: inReplyToComment.id, + videoId: inReplyToComment.videoId, + actorId: byActor.id + }, { transaction: t }) + }) +} diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts new file mode 100644 index 000000000..92c0c6112 --- /dev/null +++ b/server/models/video/video-comment.ts @@ -0,0 +1,95 @@ +import * as Sequelize from 'sequelize' +import { + AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, IFindOptions, Is, IsUUID, Model, Table, + UpdatedAt +} from 'sequelize-typescript' +import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub' +import { CONSTRAINTS_FIELDS } from '../../initializers' +import { ActorModel } from '../activitypub/actor' +import { throwIfNotValid } from '../utils' +import { VideoModel } from './video' + +@Table({ + tableName: 'videoComment', + indexes: [ + { + fields: [ 'videoId' ] + } + ] +}) +export class VideoCommentModel extends Model { + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @AllowNull(false) + @Is('VideoCommentUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url')) + @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max)) + url: string + + @AllowNull(false) + @Column(DataType.TEXT) + text: string + + @ForeignKey(() => VideoCommentModel) + @Column + originCommentId: number + + @BelongsTo(() => VideoCommentModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'CASCADE' + }) + OriginVideoComment: VideoCommentModel + + @ForeignKey(() => VideoCommentModel) + @Column + inReplyToCommentId: number + + @BelongsTo(() => VideoCommentModel, { + foreignKey: { + allowNull: true + }, + onDelete: 'CASCADE' + }) + InReplyToVideoComment: VideoCommentModel + + @ForeignKey(() => VideoModel) + @Column + videoId: number + + @BelongsTo(() => VideoModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'CASCADE' + }) + Video: VideoModel + + @ForeignKey(() => ActorModel) + @Column + actorId: number + + @BelongsTo(() => ActorModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'CASCADE' + }) + Actor: ActorModel + + static loadByUrl (url: string, t?: Sequelize.Transaction) { + const query: IFindOptions = { + where: { + url + } + } + + if (t !== undefined) query.transaction = t + + return VideoCommentModel.findOne(query) + } +} diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 8c49bc3af..b6a2ce6b5 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -491,6 +491,18 @@ export class VideoModel extends Model { return VideoModel.findById(id) } + static loadByUrl (url: string, t?: Sequelize.Transaction) { + const query: IFindOptions = { + where: { + url + } + } + + if (t !== undefined) query.transaction = t + + return VideoModel.findOne(query) + } + static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) { const query: IFindOptions = { where: { diff --git a/shared/models/activitypub/activity.ts b/shared/models/activitypub/activity.ts index d5359eba1..48b52d2cb 100644 --- a/shared/models/activitypub/activity.ts +++ b/shared/models/activitypub/activity.ts @@ -2,6 +2,7 @@ import { ActivityPubSignature } from './activitypub-signature' import { VideoTorrentObject } from './objects' import { DislikeObject } from './objects/dislike-object' import { VideoAbuseObject } from './objects/video-abuse-object' +import { VideoCommentObject } from './objects/video-comment-object' import { ViewObject } from './objects/view-object' export type Activity = ActivityCreate | ActivityUpdate | @@ -27,7 +28,7 @@ export interface BaseActivity { export interface ActivityCreate extends BaseActivity { type: 'Create' - object: VideoTorrentObject | VideoAbuseObject | ViewObject | DislikeObject + object: VideoTorrentObject | VideoAbuseObject | ViewObject | DislikeObject | VideoCommentObject } export interface ActivityUpdate extends BaseActivity { diff --git a/shared/models/activitypub/objects/video-comment-object.ts b/shared/models/activitypub/objects/video-comment-object.ts new file mode 100644 index 000000000..fc2a9e837 --- /dev/null +++ b/shared/models/activitypub/objects/video-comment-object.ts @@ -0,0 +1,8 @@ +export interface VideoCommentObject { + type: 'Note' + id: string + content: string + inReplyTo: string + published: string + url: string +} diff --git a/yarn.lock b/yarn.lock index 8f8f8235a..101428df8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -150,6 +150,10 @@ "@types/form-data" "*" "@types/node" "*" +"@types/sanitize-html@^1.14.0": + version "1.14.0" + resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-1.14.0.tgz#9a03ec58306e24feaa3fbdb8ab593934d53ecb05" + "@types/sequelize@4.0.79", "@types/sequelize@^4.0.55": version "4.0.79" resolved "https://registry.yarnpkg.com/@types/sequelize/-/sequelize-4.0.79.tgz#74c366407a978e493e70d7cea3d80c681aed15c0" @@ -342,7 +346,7 @@ array-union@^1.0.1: dependencies: array-uniq "^1.0.1" -array-uniq@^1.0.1: +array-uniq@^1.0.1, array-uniq@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" @@ -783,7 +787,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0: +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" dependencies: @@ -1196,6 +1200,34 @@ doctrine@^2.0.0: dependencies: esutils "^2.0.2" +dom-serializer@0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" + dependencies: + domelementtype "~1.1.1" + entities "~1.1.1" + +domelementtype@1, domelementtype@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" + +domelementtype@~1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" + +domhandler@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259" + dependencies: + domelementtype "1" + +domutils@^1.5.1: + version "1.6.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff" + dependencies: + dom-serializer "0" + domelementtype "1" + dot-prop@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" @@ -1250,6 +1282,10 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0: dependencies: once "^1.4.0" +entities@^1.1.1, entities@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" + error-ex@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" @@ -2033,6 +2069,17 @@ homedir-polyfill@^1.0.1: dependencies: parse-passwd "^1.0.0" +htmlparser2@^3.9.0: + version "3.9.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" + dependencies: + domelementtype "^1.3.0" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^2.0.2" + http-errors@1.6.2, http-errors@~1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" @@ -2551,6 +2598,10 @@ lodash.assign@^3.0.0: lodash._createassigner "^3.0.0" lodash.keys "^3.0.0" +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + lodash.cond@^4.3.0: version "4.5.2" resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5" @@ -2562,6 +2613,10 @@ lodash.defaults@^3.1.2: lodash.assign "^3.0.0" lodash.restparam "^3.0.0" +lodash.escaperegexp@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347" + lodash.isarguments@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" @@ -2578,6 +2633,10 @@ lodash.keys@^3.0.0: lodash.isarguments "^3.0.0" lodash.isarray "^3.0.0" +lodash.mergewith@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55" + lodash.restparam@^3.0.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" @@ -3267,6 +3326,14 @@ pluralize@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" +postcss@^6.0.14: + version "6.0.14" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.14.tgz#5534c72114739e75d0afcf017db853099f562885" + dependencies: + chalk "^2.3.0" + source-map "^0.6.1" + supports-color "^4.4.0" + postgres-array@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-1.0.2.tgz#8e0b32eb03bf77a5c0a7851e0441c169a256a238" @@ -3647,6 +3714,18 @@ safe-buffer@5.1.1, safe-buffer@^5.0.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, s version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" +sanitize-html@^1.16.3: + version "1.16.3" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.16.3.tgz#96c1b44a36ff7312e1c22a14b05274370ac8bd56" + dependencies: + htmlparser2 "^3.9.0" + lodash.clonedeep "^4.5.0" + lodash.escaperegexp "^4.1.2" + lodash.mergewith "^4.6.0" + postcss "^6.0.14" + srcset "^1.0.0" + xtend "^4.0.0" + scripty@^1.5.0: version "1.7.2" resolved "https://registry.yarnpkg.com/scripty/-/scripty-1.7.2.tgz#92367b724cb77b086729691f7b01aa57f3ddd356" @@ -3854,7 +3933,7 @@ source-map@^0.5.6: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" -source-map@^0.6.0: +source-map@^0.6.0, source-map@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -3882,6 +3961,13 @@ sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" +srcset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/srcset/-/srcset-1.0.0.tgz#a5669de12b42f3b1d5e83ed03c71046fc48f41ef" + dependencies: + array-uniq "^1.0.2" + number-is-nan "^1.0.0" + sshpk@^1.7.0: version "1.13.1" resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3" @@ -4067,7 +4153,7 @@ supports-color@^3.2.3: dependencies: has-flag "^1.0.0" -supports-color@^4.0.0: +supports-color@^4.0.0, supports-color@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" dependencies: