From 1506307f2f903ce0f80155072a33345c702b7c76 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 14 Jan 2019 16:48:38 +0100 Subject: Increase abuse length to 3000 And correctly handle new lines --- .../about-instance/about-instance.component.ts | 2 +- .../account-about/account-about.component.ts | 4 +- .../+admin/moderation/moderation.component.scss | 1 + .../video-abuse-list.component.html | 4 +- .../video-abuse-list/video-abuse-list.component.ts | 8 +- .../video-blacklist-list.component.html | 2 +- .../video-blacklist-list.component.ts | 6 ++ .../video-channel-about.component.ts | 2 +- .../video-abuse-validators.service.ts | 8 +- .../shared/forms/markdown-textarea.component.ts | 2 +- client/src/app/shared/misc/help.component.ts | 2 +- client/src/app/shared/misc/utils.ts | 13 ++- .../app/shared/renderer/html-renderer.service.ts | 35 +++++++ client/src/app/shared/renderer/index.ts | 3 + .../src/app/shared/renderer/linkifier.service.ts | 115 +++++++++++++++++++++ client/src/app/shared/renderer/markdown.service.ts | 79 ++++++++++++++ client/src/app/shared/shared.module.ts | 9 +- .../app/shared/video-abuse/video-abuse.service.ts | 4 +- .../+video-watch/comment/linkifier.service.ts | 115 --------------------- .../comment/video-comment.component.ts | 27 +---- .../+video-watch/comment/video-comment.service.ts | 6 +- .../+video-watch/modal/video-support.component.ts | 3 +- .../videos/+video-watch/video-watch.component.ts | 2 +- .../app/videos/+video-watch/video-watch.module.ts | 4 - client/src/app/videos/shared/index.ts | 1 - client/src/app/videos/shared/markdown.service.ts | 79 -------------- server/initializers/constants.ts | 6 +- .../migrations/0325-video-abuse-fields.ts | 37 +++++++ server/models/video/video-abuse.ts | 19 +--- server/tests/api/check-params/video-abuses.ts | 6 +- 30 files changed, 330 insertions(+), 274 deletions(-) create mode 100644 client/src/app/shared/renderer/html-renderer.service.ts create mode 100644 client/src/app/shared/renderer/index.ts create mode 100644 client/src/app/shared/renderer/linkifier.service.ts create mode 100644 client/src/app/shared/renderer/markdown.service.ts delete mode 100644 client/src/app/videos/+video-watch/comment/linkifier.service.ts delete mode 100644 client/src/app/videos/shared/index.ts delete mode 100644 client/src/app/videos/shared/markdown.service.ts create mode 100644 server/initializers/migrations/0325-video-abuse-fields.ts diff --git a/client/src/app/+about/about-instance/about-instance.component.ts b/client/src/app/+about/about-instance/about-instance.component.ts index d3ee8a1e4..a1b30fa8c 100644 --- a/client/src/app/+about/about-instance/about-instance.component.ts +++ b/client/src/app/+about/about-instance/about-instance.component.ts @@ -1,9 +1,9 @@ import { Component, OnInit, ViewChild } from '@angular/core' import { Notifier, ServerService } from '@app/core' -import { MarkdownService } from '@app/videos/shared' import { I18n } from '@ngx-translate/i18n-polyfill' import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' import { InstanceService } from '@app/shared/instance/instance.service' +import { MarkdownService } from '@app/shared/renderer' @Component({ selector: 'my-about-instance', diff --git a/client/src/app/+accounts/account-about/account-about.component.ts b/client/src/app/+accounts/account-about/account-about.component.ts index 6f3e6caa0..13890a0ee 100644 --- a/client/src/app/+accounts/account-about/account-about.component.ts +++ b/client/src/app/+accounts/account-about/account-about.component.ts @@ -1,9 +1,9 @@ -import { Component, OnInit, OnDestroy } from '@angular/core' +import { Component, OnDestroy, OnInit } from '@angular/core' import { Account } from '@app/shared/account/account.model' import { AccountService } from '@app/shared/account/account.service' import { I18n } from '@ngx-translate/i18n-polyfill' import { Subscription } from 'rxjs' -import { MarkdownService } from '@app/videos/shared' +import { MarkdownService } from '@app/shared/renderer' @Component({ selector: 'my-account-about', diff --git a/client/src/app/+admin/moderation/moderation.component.scss b/client/src/app/+admin/moderation/moderation.component.scss index 02ccfc8ca..13b019c5b 100644 --- a/client/src/app/+admin/moderation/moderation.component.scss +++ b/client/src/app/+admin/moderation/moderation.component.scss @@ -10,6 +10,7 @@ font-weight: $font-semibold; min-width: 200px; display: inline-block; + vertical-align: top; } .moderation-expanded-text { diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html index e862d5162..05b549de6 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html @@ -51,11 +51,11 @@
Reason: - {{ videoAbuse.reason }} +
Moderation comment: - {{ videoAbuse.moderationComment }} +
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts index f64234b74..00c871659 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts @@ -9,6 +9,7 @@ import { DropdownAction } from '../../../shared/buttons/action-dropdown.componen import { ConfirmService } from '../../../core/index' import { ModerationCommentModalComponent } from './moderation-comment-modal.component' import { Video } from '../../../shared/video/video.model' +import { MarkdownService } from '@app/shared/renderer' @Component({ selector: 'my-video-abuse-list', @@ -30,7 +31,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit { private notifier: Notifier, private videoAbuseService: VideoAbuseService, private confirmService: ConfirmService, - private i18n: I18n + private i18n: I18n, + private markdownRenderer: MarkdownService ) { super() @@ -108,6 +110,10 @@ export class VideoAbuseListComponent extends RestTable implements OnInit { } + toHtml (text: string) { + return this.markdownRenderer.textMarkdownToHTML(text) + } + protected loadData () { return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort) .subscribe( diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html index 6398af218..247f441c1 100644 --- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html +++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html @@ -41,7 +41,7 @@ Blacklist reason: - {{ videoBlacklist.reason }} + diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts index 6c6f17f0c..b27bbbfef 100644 --- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts +++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts @@ -7,6 +7,7 @@ import { VideoBlacklist } from '../../../../../../shared' import { I18n } from '@ngx-translate/i18n-polyfill' import { DropdownAction } from '../../../shared/buttons/action-dropdown.component' import { Video } from '../../../shared/video/video.model' +import { MarkdownService } from '@app/shared/renderer' @Component({ selector: 'my-video-blacklist-list', @@ -26,6 +27,7 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit { private notifier: Notifier, private confirmService: ConfirmService, private videoBlacklistService: VideoBlacklistService, + private markdownRenderer: MarkdownService, private i18n: I18n ) { super() @@ -52,6 +54,10 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit { return this.i18n('no') } + toHtml (text: string) { + return this.markdownRenderer.textMarkdownToHTML(text) + } + async removeVideoFromBlacklist (entry: VideoBlacklist) { const confirmMessage = this.i18n( 'Do you really want to remove this video from the blacklist? It will be available again in the videos list.' diff --git a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts index ea7b0e118..895b19064 100644 --- a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts +++ b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts @@ -3,7 +3,7 @@ import { VideoChannelService } from '@app/shared/video-channel/video-channel.ser import { VideoChannel } from '@app/shared/video-channel/video-channel.model' import { I18n } from '@ngx-translate/i18n-polyfill' import { Subscription } from 'rxjs' -import { MarkdownService } from '@app/videos/shared' +import { MarkdownService } from '@app/shared/renderer' @Component({ selector: 'my-video-channel-about', diff --git a/client/src/app/shared/forms/form-validators/video-abuse-validators.service.ts b/client/src/app/shared/forms/form-validators/video-abuse-validators.service.ts index 6e9806611..fcc966b84 100644 --- a/client/src/app/shared/forms/form-validators/video-abuse-validators.service.ts +++ b/client/src/app/shared/forms/form-validators/video-abuse-validators.service.ts @@ -10,20 +10,20 @@ export class VideoAbuseValidatorsService { constructor (private i18n: I18n) { this.VIDEO_ABUSE_REASON = { - VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ], + VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], MESSAGES: { 'required': this.i18n('Report reason is required.'), 'minlength': this.i18n('Report reason must be at least 2 characters long.'), - 'maxlength': this.i18n('Report reason cannot be more than 300 characters long.') + 'maxlength': this.i18n('Report reason cannot be more than 3000 characters long.') } } this.VIDEO_ABUSE_MODERATION_COMMENT = { - VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ], + VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], MESSAGES: { 'required': this.i18n('Moderation comment is required.'), 'minlength': this.i18n('Moderation comment must be at least 2 characters long.'), - 'maxlength': this.i18n('Moderation comment cannot be more than 300 characters long.') + 'maxlength': this.i18n('Moderation comment cannot be more than 3000 characters long.') } } } diff --git a/client/src/app/shared/forms/markdown-textarea.component.ts b/client/src/app/shared/forms/markdown-textarea.component.ts index b99169ed2..e87aca0d4 100644 --- a/client/src/app/shared/forms/markdown-textarea.component.ts +++ b/client/src/app/shared/forms/markdown-textarea.component.ts @@ -1,10 +1,10 @@ import { debounceTime, distinctUntilChanged } from 'rxjs/operators' import { Component, forwardRef, Input, OnInit } from '@angular/core' import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' -import { MarkdownService } from '@app/videos/shared' import { Subject } from 'rxjs' import truncate from 'lodash-es/truncate' import { ScreenService } from '@app/shared/misc/screen.service' +import { MarkdownService } from '@app/shared/renderer' @Component({ selector: 'my-markdown-textarea', diff --git a/client/src/app/shared/misc/help.component.ts b/client/src/app/shared/misc/help.component.ts index ba0452e77..f3426f70f 100644 --- a/client/src/app/shared/misc/help.component.ts +++ b/client/src/app/shared/misc/help.component.ts @@ -1,6 +1,6 @@ import { Component, Input, OnChanges, OnInit } from '@angular/core' -import { MarkdownService } from '@app/videos/shared' import { I18n } from '@ngx-translate/i18n-polyfill' +import { MarkdownService } from '@app/shared/renderer' @Component({ selector: 'my-help', diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts index 78e8e9682..7cc6055c2 100644 --- a/client/src/app/shared/misc/utils.ts +++ b/client/src/app/shared/misc/utils.ts @@ -102,12 +102,18 @@ function objectToFormData (obj: any, form?: FormData, namespace?: string) { return fd } -function lineFeedToHtml (obj: any, keyToNormalize: string) { +function objectLineFeedToHtml (obj: any, keyToNormalize: string) { return immutableAssign(obj, { - [keyToNormalize]: obj[keyToNormalize].replace(/\r?\n|\r/g, '
') + [keyToNormalize]: lineFeedToHtml(obj[keyToNormalize]) }) } +function lineFeedToHtml (text: string) { + if (!text) return text + + return text.replace(/\r?\n|\r/g, '
') +} + function removeElementFromArray (arr: T[], elem: T) { const index = arr.indexOf(elem) if (index !== -1) arr.splice(index, 1) @@ -131,6 +137,7 @@ function scrollToTop () { export { sortBy, durationToString, + lineFeedToHtml, objectToUrlEncoded, getParameterByName, populateAsyncUserVideoChannels, @@ -138,7 +145,7 @@ export { dateToHuman, immutableAssign, objectToFormData, - lineFeedToHtml, + objectLineFeedToHtml, removeElementFromArray, scrollToTop } diff --git a/client/src/app/shared/renderer/html-renderer.service.ts b/client/src/app/shared/renderer/html-renderer.service.ts new file mode 100644 index 000000000..d49df9b6d --- /dev/null +++ b/client/src/app/shared/renderer/html-renderer.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core' +import { LinkifierService } from '@app/shared/renderer/linkifier.service' +import * as sanitizeHtml from 'sanitize-html' + +@Injectable() +export class HtmlRendererService { + + constructor (private linkifier: LinkifierService) { + + } + + toSafeHtml (text: string) { + // Convert possible markdown to html + const html = this.linkifier.linkify(text) + + return sanitizeHtml(html, { + allowedTags: [ 'a', 'p', 'span', 'br' ], + allowedSchemes: [ 'http', 'https' ], + allowedAttributes: { + 'a': [ 'href', 'class', 'target' ] + }, + transformTags: { + a: (tagName, attribs) => { + return { + tagName, + attribs: Object.assign(attribs, { + target: '_blank', + rel: 'noopener noreferrer' + }) + } + } + } + }) + } +} diff --git a/client/src/app/shared/renderer/index.ts b/client/src/app/shared/renderer/index.ts new file mode 100644 index 000000000..39202b385 --- /dev/null +++ b/client/src/app/shared/renderer/index.ts @@ -0,0 +1,3 @@ +export * from './html-renderer.service' +export * from './linkifier.service' +export * from './markdown.service' diff --git a/client/src/app/shared/renderer/linkifier.service.ts b/client/src/app/shared/renderer/linkifier.service.ts new file mode 100644 index 000000000..2529c9eaf --- /dev/null +++ b/client/src/app/shared/renderer/linkifier.service.ts @@ -0,0 +1,115 @@ +import { Injectable } from '@angular/core' +import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' +// FIXME: use @types/linkify when https://github.com/DefinitelyTyped/DefinitelyTyped/pull/29682/files is merged? +const linkify = require('linkifyjs') +const linkifyHtml = require('linkifyjs/html') + +@Injectable() +export class LinkifierService { + + static CLASSNAME = 'linkified' + + private linkifyOptions = { + className: { + mention: LinkifierService.CLASSNAME + '-mention', + url: LinkifierService.CLASSNAME + '-url' + } + } + + constructor () { + // Apply plugin + this.mentionWithDomainPlugin(linkify) + } + + linkify (text: string) { + return linkifyHtml(text, this.linkifyOptions) + } + + private mentionWithDomainPlugin (linkify: any) { + const TT = linkify.scanner.TOKENS // Text tokens + const { TOKENS: MT, State } = linkify.parser // Multi tokens, state + const MultiToken = MT.Base + const S_START = linkify.parser.start + + const TT_AT = TT.AT + const TT_DOMAIN = TT.DOMAIN + const TT_LOCALHOST = TT.LOCALHOST + const TT_NUM = TT.NUM + const TT_COLON = TT.COLON + const TT_SLASH = TT.SLASH + const TT_TLD = TT.TLD + const TT_UNDERSCORE = TT.UNDERSCORE + const TT_DOT = TT.DOT + + function MENTION (this: any, value: any) { + this.v = value + } + + linkify.inherits(MultiToken, MENTION, { + type: 'mentionWithDomain', + isLink: true, + toHref () { + return getAbsoluteAPIUrl() + '/services/redirect/accounts/' + this.toString().substr(1) + } + }) + + const S_AT = S_START.jump(TT_AT) // @ + const S_AT_SYMS = new State() + const S_MENTION = new State(MENTION) + const S_MENTION_DIVIDER = new State() + const S_MENTION_DIVIDER_SYMS = new State() + + // @_, + S_AT.on(TT_UNDERSCORE, S_AT_SYMS) + + // @_* + S_AT_SYMS + .on(TT_UNDERSCORE, S_AT_SYMS) + .on(TT_DOT, S_AT_SYMS) + + // Valid mention (not made up entirely of symbols) + S_AT + .on(TT_DOMAIN, S_MENTION) + .on(TT_LOCALHOST, S_MENTION) + .on(TT_TLD, S_MENTION) + .on(TT_NUM, S_MENTION) + + S_AT_SYMS + .on(TT_DOMAIN, S_MENTION) + .on(TT_LOCALHOST, S_MENTION) + .on(TT_TLD, S_MENTION) + .on(TT_NUM, S_MENTION) + + // More valid mentions + S_MENTION + .on(TT_DOMAIN, S_MENTION) + .on(TT_LOCALHOST, S_MENTION) + .on(TT_TLD, S_MENTION) + .on(TT_COLON, S_MENTION) + .on(TT_NUM, S_MENTION) + .on(TT_UNDERSCORE, S_MENTION) + + // Mention with a divider + S_MENTION + .on(TT_AT, S_MENTION_DIVIDER) + .on(TT_SLASH, S_MENTION_DIVIDER) + .on(TT_DOT, S_MENTION_DIVIDER) + + // Mention _ trailing stash plus syms + S_MENTION_DIVIDER.on(TT_UNDERSCORE, S_MENTION_DIVIDER_SYMS) + S_MENTION_DIVIDER_SYMS.on(TT_UNDERSCORE, S_MENTION_DIVIDER_SYMS) + + // Once we get a word token, mentions can start up again + S_MENTION_DIVIDER + .on(TT_DOMAIN, S_MENTION) + .on(TT_LOCALHOST, S_MENTION) + .on(TT_TLD, S_MENTION) + .on(TT_NUM, S_MENTION) + + S_MENTION_DIVIDER_SYMS + .on(TT_DOMAIN, S_MENTION) + .on(TT_LOCALHOST, S_MENTION) + .on(TT_TLD, S_MENTION) + .on(TT_NUM, S_MENTION) + } +} diff --git a/client/src/app/shared/renderer/markdown.service.ts b/client/src/app/shared/renderer/markdown.service.ts new file mode 100644 index 000000000..07017eca5 --- /dev/null +++ b/client/src/app/shared/renderer/markdown.service.ts @@ -0,0 +1,79 @@ +import { Injectable } from '@angular/core' + +import * as MarkdownIt from 'markdown-it' + +@Injectable() +export class MarkdownService { + static TEXT_RULES = [ + 'linkify', + 'autolink', + 'emphasis', + 'link', + 'newline', + 'list' + ] + static ENHANCED_RULES = MarkdownService.TEXT_RULES.concat([ 'image' ]) + + private textMarkdownIt: MarkdownIt.MarkdownIt + private enhancedMarkdownIt: MarkdownIt.MarkdownIt + + constructor () { + this.textMarkdownIt = this.createMarkdownIt(MarkdownService.TEXT_RULES) + this.enhancedMarkdownIt = this.createMarkdownIt(MarkdownService.ENHANCED_RULES) + } + + textMarkdownToHTML (markdown: string) { + if (!markdown) return '' + + const html = this.textMarkdownIt.render(markdown) + return this.avoidTruncatedTags(html) + } + + enhancedMarkdownToHTML (markdown: string) { + if (!markdown) return '' + + const html = this.enhancedMarkdownIt.render(markdown) + return this.avoidTruncatedTags(html) + } + + private createMarkdownIt (rules: string[]) { + const markdownIt = new MarkdownIt('zero', { linkify: true, breaks: true }) + + for (let rule of rules) { + markdownIt.enable(rule) + } + + this.setTargetToLinks(markdownIt) + + return markdownIt + } + + private setTargetToLinks (markdownIt: MarkdownIt.MarkdownIt) { + // Snippet from markdown-it documentation: https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer + const defaultRender = markdownIt.renderer.rules.link_open || function (tokens, idx, options, env, self) { + return self.renderToken(tokens, idx, options) + } + + markdownIt.renderer.rules.link_open = function (tokens, index, options, env, self) { + const token = tokens[index] + + const targetIndex = token.attrIndex('target') + if (targetIndex < 0) token.attrPush([ 'target', '_blank' ]) + else token.attrs[targetIndex][1] = '_blank' + + const relIndex = token.attrIndex('rel') + if (relIndex < 0) token.attrPush([ 'rel', 'noopener noreferrer' ]) + else token.attrs[relIndex][1] = 'noopener noreferrer' + + // pass token to default renderer. + return defaultRender(tokens, index, options, env, self) + } + } + + private avoidTruncatedTags (html: string) { + return html.replace(/\*\*?([^*]+)$/, '$1') + .replace(/]+>([^<]+)<\/a>\s*...((<\/p>)|(<\/li>)|(<\/strong>))?$/mi, '$1...') + .replace(/\[[^\]]+\]?\(?([^\)]+)$/, '$1') + + } +} diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index d1320aeec..384f5d722 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -6,7 +6,6 @@ import { RouterModule } from '@angular/router' import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' import { HelpComponent } from '@app/shared/misc/help.component' import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' -import { MarkdownService } from '@app/videos/shared' import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' @@ -34,10 +33,10 @@ import { I18n } from '@ngx-translate/i18n-polyfill' import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' import { CustomConfigValidatorsService, + InstanceValidatorsService, LoginValidatorsService, ReactiveFileComponent, ResetPasswordValidatorsService, - InstanceValidatorsService, TextareaAutoResizeDirective, UserValidatorsService, VideoAbuseValidatorsService, @@ -67,6 +66,7 @@ import { UserHistoryService } from '@app/shared/users/user-history.service' import { UserNotificationService } from '@app/shared/users/user-notification.service' import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component' import { InstanceService } from '@app/shared/instance/instance.service' +import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer' @NgModule({ imports: [ @@ -167,7 +167,6 @@ import { InstanceService } from '@app/shared/instance/instance.service' UserService, VideoService, AccountService, - MarkdownService, VideoChannelService, VideoCaptionService, VideoImportService, @@ -192,6 +191,10 @@ import { InstanceService } from '@app/shared/instance/instance.service' UserHistoryService, InstanceService, + MarkdownService, + LinkifierService, + HtmlRendererService, + I18nPrimengCalendarService, ScreenService, diff --git a/client/src/app/shared/video-abuse/video-abuse.service.ts b/client/src/app/shared/video-abuse/video-abuse.service.ts index 61b7e1b98..b0b59ea0c 100644 --- a/client/src/app/shared/video-abuse/video-abuse.service.ts +++ b/client/src/app/shared/video-abuse/video-abuse.service.ts @@ -32,9 +32,7 @@ export class VideoAbuseService { reportVideo (id: number, reason: string) { const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + id + '/abuse' - const body = { - reason - } + const body = { reason } return this.authHttp.post(url, body) .pipe( diff --git a/client/src/app/videos/+video-watch/comment/linkifier.service.ts b/client/src/app/videos/+video-watch/comment/linkifier.service.ts deleted file mode 100644 index 2529c9eaf..000000000 --- a/client/src/app/videos/+video-watch/comment/linkifier.service.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Injectable } from '@angular/core' -import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' -// FIXME: use @types/linkify when https://github.com/DefinitelyTyped/DefinitelyTyped/pull/29682/files is merged? -const linkify = require('linkifyjs') -const linkifyHtml = require('linkifyjs/html') - -@Injectable() -export class LinkifierService { - - static CLASSNAME = 'linkified' - - private linkifyOptions = { - className: { - mention: LinkifierService.CLASSNAME + '-mention', - url: LinkifierService.CLASSNAME + '-url' - } - } - - constructor () { - // Apply plugin - this.mentionWithDomainPlugin(linkify) - } - - linkify (text: string) { - return linkifyHtml(text, this.linkifyOptions) - } - - private mentionWithDomainPlugin (linkify: any) { - const TT = linkify.scanner.TOKENS // Text tokens - const { TOKENS: MT, State } = linkify.parser // Multi tokens, state - const MultiToken = MT.Base - const S_START = linkify.parser.start - - const TT_AT = TT.AT - const TT_DOMAIN = TT.DOMAIN - const TT_LOCALHOST = TT.LOCALHOST - const TT_NUM = TT.NUM - const TT_COLON = TT.COLON - const TT_SLASH = TT.SLASH - const TT_TLD = TT.TLD - const TT_UNDERSCORE = TT.UNDERSCORE - const TT_DOT = TT.DOT - - function MENTION (this: any, value: any) { - this.v = value - } - - linkify.inherits(MultiToken, MENTION, { - type: 'mentionWithDomain', - isLink: true, - toHref () { - return getAbsoluteAPIUrl() + '/services/redirect/accounts/' + this.toString().substr(1) - } - }) - - const S_AT = S_START.jump(TT_AT) // @ - const S_AT_SYMS = new State() - const S_MENTION = new State(MENTION) - const S_MENTION_DIVIDER = new State() - const S_MENTION_DIVIDER_SYMS = new State() - - // @_, - S_AT.on(TT_UNDERSCORE, S_AT_SYMS) - - // @_* - S_AT_SYMS - .on(TT_UNDERSCORE, S_AT_SYMS) - .on(TT_DOT, S_AT_SYMS) - - // Valid mention (not made up entirely of symbols) - S_AT - .on(TT_DOMAIN, S_MENTION) - .on(TT_LOCALHOST, S_MENTION) - .on(TT_TLD, S_MENTION) - .on(TT_NUM, S_MENTION) - - S_AT_SYMS - .on(TT_DOMAIN, S_MENTION) - .on(TT_LOCALHOST, S_MENTION) - .on(TT_TLD, S_MENTION) - .on(TT_NUM, S_MENTION) - - // More valid mentions - S_MENTION - .on(TT_DOMAIN, S_MENTION) - .on(TT_LOCALHOST, S_MENTION) - .on(TT_TLD, S_MENTION) - .on(TT_COLON, S_MENTION) - .on(TT_NUM, S_MENTION) - .on(TT_UNDERSCORE, S_MENTION) - - // Mention with a divider - S_MENTION - .on(TT_AT, S_MENTION_DIVIDER) - .on(TT_SLASH, S_MENTION_DIVIDER) - .on(TT_DOT, S_MENTION_DIVIDER) - - // Mention _ trailing stash plus syms - S_MENTION_DIVIDER.on(TT_UNDERSCORE, S_MENTION_DIVIDER_SYMS) - S_MENTION_DIVIDER_SYMS.on(TT_UNDERSCORE, S_MENTION_DIVIDER_SYMS) - - // Once we get a word token, mentions can start up again - S_MENTION_DIVIDER - .on(TT_DOMAIN, S_MENTION) - .on(TT_LOCALHOST, S_MENTION) - .on(TT_TLD, S_MENTION) - .on(TT_NUM, S_MENTION) - - S_MENTION_DIVIDER_SYMS - .on(TT_DOMAIN, S_MENTION) - .on(TT_LOCALHOST, S_MENTION) - .on(TT_TLD, S_MENTION) - .on(TT_NUM, S_MENTION) - } -} diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.ts b/client/src/app/videos/+video-watch/comment/video-comment.component.ts index 00f0460a1..aba7f9d1c 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment.component.ts +++ b/client/src/app/videos/+video-watch/comment/video-comment.component.ts @@ -1,11 +1,10 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core' -import { LinkifierService } from '@app/videos/+video-watch/comment/linkifier.service' -import * as sanitizeHtml from 'sanitize-html' import { UserRight } from '../../../../../../shared/models/users' import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' import { AuthService } from '../../../core/auth' import { Video } from '../../../shared/video/video.model' import { VideoComment } from './video-comment.model' +import { HtmlRendererService } from '@app/shared/renderer' @Component({ selector: 'my-video-comment', @@ -29,7 +28,7 @@ export class VideoCommentComponent implements OnInit, OnChanges { newParentComments: VideoComment[] = [] constructor ( - private linkifierService: LinkifierService, + private htmlRenderer: HtmlRendererService, private authService: AuthService ) {} @@ -87,27 +86,7 @@ export class VideoCommentComponent implements OnInit, OnChanges { } private init () { - // Convert possible markdown to html - const html = this.linkifierService.linkify(this.comment.text) - - this.sanitizedCommentHTML = sanitizeHtml(html, { - allowedTags: [ 'a', 'p', 'span', 'br' ], - allowedSchemes: [ 'http', 'https' ], - allowedAttributes: { - 'a': [ 'href', 'class', 'target' ] - }, - transformTags: { - a: (tagName, attribs) => { - return { - tagName, - attribs: Object.assign(attribs, { - target: '_blank', - rel: 'noopener noreferrer' - }) - } - } - } - }) + this.sanitizedCommentHTML = this.htmlRenderer.toSafeHtml(this.comment.text) this.newParentComments = this.parentComments.concat([ this.comment ]) } diff --git a/client/src/app/videos/+video-watch/comment/video-comment.service.ts b/client/src/app/videos/+video-watch/comment/video-comment.service.ts index 921447d5b..b8e5878c5 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment.service.ts +++ b/client/src/app/videos/+video-watch/comment/video-comment.service.ts @@ -1,7 +1,7 @@ import { catchError, map } from 'rxjs/operators' import { HttpClient, HttpParams } from '@angular/common/http' import { Injectable } from '@angular/core' -import { lineFeedToHtml } from '@app/shared/misc/utils' +import { objectLineFeedToHtml } from '@app/shared/misc/utils' import { Observable } from 'rxjs' import { ResultList, FeedFormat } from '../../../../../../shared/models' import { @@ -28,7 +28,7 @@ export class VideoCommentService { addCommentThread (videoId: number | string, comment: VideoCommentCreate) { const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' - const normalizedComment = lineFeedToHtml(comment, 'text') + const normalizedComment = objectLineFeedToHtml(comment, 'text') return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment) .pipe( @@ -39,7 +39,7 @@ export class VideoCommentService { addCommentReply (videoId: number | string, inReplyToCommentId: number, comment: VideoCommentCreate) { const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId - const normalizedComment = lineFeedToHtml(comment, 'text') + const normalizedComment = objectLineFeedToHtml(comment, 'text') return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment) .pipe( diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.ts b/client/src/app/videos/+video-watch/modal/video-support.component.ts index 154002120..deb8fbc67 100644 --- a/client/src/app/videos/+video-watch/modal/video-support.component.ts +++ b/client/src/app/videos/+video-watch/modal/video-support.component.ts @@ -1,8 +1,7 @@ import { Component, Input, ViewChild } from '@angular/core' -import { MarkdownService } from '@app/videos/shared' - import { VideoDetails } from '../../../shared/video/video-details.model' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' +import { MarkdownService } from '@app/shared/renderer' @Component({ selector: 'my-video-support', diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index 67c5254b3..ee504bc58 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts @@ -19,7 +19,6 @@ import { AuthService, ConfirmService } from '../../core' import { RestExtractor, VideoBlacklistService } from '../../shared' import { VideoDetails } from '../../shared/video/video-details.model' import { VideoService } from '../../shared/video/video.service' -import { MarkdownService } from '../shared' import { VideoDownloadComponent } from './modal/video-download.component' import { VideoReportComponent } from './modal/video-report.component' import { VideoShareComponent } from './modal/video-share.component' @@ -30,6 +29,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill' import { environment } from '../../../environments/environment' import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' import { VideoCaptionService } from '@app/shared/video-caption' +import { MarkdownService } from '@app/shared/renderer' @Component({ selector: 'my-video-watch', diff --git a/client/src/app/videos/+video-watch/video-watch.module.ts b/client/src/app/videos/+video-watch/video-watch.module.ts index 54a12c126..2f448db78 100644 --- a/client/src/app/videos/+video-watch/video-watch.module.ts +++ b/client/src/app/videos/+video-watch/video-watch.module.ts @@ -1,9 +1,7 @@ import { NgModule } from '@angular/core' -import { LinkifierService } from '@app/videos/+video-watch/comment/linkifier.service' import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' import { ClipboardModule } from 'ngx-clipboard' import { SharedModule } from '../../shared' -import { MarkdownService } from '../shared' import { VideoCommentAddComponent } from './comment/video-comment-add.component' import { VideoCommentComponent } from './comment/video-comment.component' import { VideoCommentService } from './comment/video-comment.service' @@ -46,8 +44,6 @@ import { RecommendationsModule } from '@app/videos/recommendations/recommendatio ], providers: [ - MarkdownService, - LinkifierService, VideoCommentService ] }) diff --git a/client/src/app/videos/shared/index.ts b/client/src/app/videos/shared/index.ts deleted file mode 100644 index 7a66944b9..000000000 --- a/client/src/app/videos/shared/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './markdown.service' diff --git a/client/src/app/videos/shared/markdown.service.ts b/client/src/app/videos/shared/markdown.service.ts deleted file mode 100644 index 07017eca5..000000000 --- a/client/src/app/videos/shared/markdown.service.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Injectable } from '@angular/core' - -import * as MarkdownIt from 'markdown-it' - -@Injectable() -export class MarkdownService { - static TEXT_RULES = [ - 'linkify', - 'autolink', - 'emphasis', - 'link', - 'newline', - 'list' - ] - static ENHANCED_RULES = MarkdownService.TEXT_RULES.concat([ 'image' ]) - - private textMarkdownIt: MarkdownIt.MarkdownIt - private enhancedMarkdownIt: MarkdownIt.MarkdownIt - - constructor () { - this.textMarkdownIt = this.createMarkdownIt(MarkdownService.TEXT_RULES) - this.enhancedMarkdownIt = this.createMarkdownIt(MarkdownService.ENHANCED_RULES) - } - - textMarkdownToHTML (markdown: string) { - if (!markdown) return '' - - const html = this.textMarkdownIt.render(markdown) - return this.avoidTruncatedTags(html) - } - - enhancedMarkdownToHTML (markdown: string) { - if (!markdown) return '' - - const html = this.enhancedMarkdownIt.render(markdown) - return this.avoidTruncatedTags(html) - } - - private createMarkdownIt (rules: string[]) { - const markdownIt = new MarkdownIt('zero', { linkify: true, breaks: true }) - - for (let rule of rules) { - markdownIt.enable(rule) - } - - this.setTargetToLinks(markdownIt) - - return markdownIt - } - - private setTargetToLinks (markdownIt: MarkdownIt.MarkdownIt) { - // Snippet from markdown-it documentation: https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer - const defaultRender = markdownIt.renderer.rules.link_open || function (tokens, idx, options, env, self) { - return self.renderToken(tokens, idx, options) - } - - markdownIt.renderer.rules.link_open = function (tokens, index, options, env, self) { - const token = tokens[index] - - const targetIndex = token.attrIndex('target') - if (targetIndex < 0) token.attrPush([ 'target', '_blank' ]) - else token.attrs[targetIndex][1] = '_blank' - - const relIndex = token.attrIndex('rel') - if (relIndex < 0) token.attrPush([ 'rel', 'noopener noreferrer' ]) - else token.attrs[relIndex][1] = 'noopener noreferrer' - - // pass token to default renderer. - return defaultRender(tokens, index, options, env, self) - } - } - - private avoidTruncatedTags (html: string) { - return html.replace(/\*\*?([^*]+)$/, '$1') - .replace(/]+>([^<]+)<\/a>\s*...((<\/p>)|(<\/li>)|(<\/strong>))?$/mi, '$1...') - .replace(/\[[^\]]+\]?\(?([^\)]+)$/, '$1') - - } -} diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index b18884eeb..93fdd3f03 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -16,7 +16,7 @@ let config: IConfig = require('config') // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 320 +const LAST_MIGRATION_VERSION = 325 // --------------------------------------------------------------------------- @@ -316,8 +316,8 @@ let CONSTRAINTS_FIELDS = { BLOCKED_REASON: { min: 3, max: 250 } // Length }, VIDEO_ABUSES: { - REASON: { min: 2, max: 300 }, // Length - MODERATION_COMMENT: { min: 2, max: 300 } // Length + REASON: { min: 2, max: 3000 }, // Length + MODERATION_COMMENT: { min: 2, max: 3000 } // Length }, VIDEO_BLACKLIST: { REASON: { min: 2, max: 300 } // Length diff --git a/server/initializers/migrations/0325-video-abuse-fields.ts b/server/initializers/migrations/0325-video-abuse-fields.ts new file mode 100644 index 000000000..fca6d666f --- /dev/null +++ b/server/initializers/migrations/0325-video-abuse-fields.ts @@ -0,0 +1,37 @@ +import * as Sequelize from 'sequelize' + +async function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize +}): Promise { + + { + const data = { + type: Sequelize.STRING(3000), + allowNull: false, + defaultValue: null + } + + await utils.queryInterface.changeColumn('videoAbuse', 'reason', data) + } + + { + const data = { + type: Sequelize.STRING(3000), + allowNull: true, + defaultValue: null + } + + await utils.queryInterface.changeColumn('videoAbuse', 'moderationComment', data) + } +} + +function down (options) { + throw new Error('Not implemented.') +} + +export { + up, + down +} diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index 4c9e2d05e..cc47644f2 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts @@ -1,17 +1,4 @@ -import { - AfterCreate, - AllowNull, - BelongsTo, - Column, - CreatedAt, - DataType, - Default, - ForeignKey, - Is, - Model, - Table, - UpdatedAt -} from 'sequelize-typescript' +import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' import { VideoAbuse } from '../../../shared/models/videos' import { @@ -19,7 +6,6 @@ import { isVideoAbuseReasonValid, isVideoAbuseStateValid } from '../../helpers/custom-validators/video-abuses' -import { Emailer } from '../../lib/emailer' import { AccountModel } from '../account/account' import { getSort, throwIfNotValid } from '../utils' import { VideoModel } from './video' @@ -40,8 +26,9 @@ import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers' export class VideoAbuseModel extends Model { @AllowNull(false) + @Default(null) @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason')) - @Column + @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max)) reason: string @AllowNull(false) diff --git a/server/tests/api/check-params/video-abuses.ts b/server/tests/api/check-params/video-abuses.ts index a79ab4201..3b8f5f14d 100644 --- a/server/tests/api/check-params/video-abuses.ts +++ b/server/tests/api/check-params/video-abuses.ts @@ -113,8 +113,8 @@ describe('Test video abuses API validators', function () { await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) }) - it('Should fail with a reason too big', async function () { - const fields = { reason: 'super'.repeat(61) } + it('Should fail with a too big reason', async function () { + const fields = { reason: 'super'.repeat(605) } await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields }) }) @@ -154,7 +154,7 @@ describe('Test video abuses API validators', function () { }) it('Should fail with a bad moderation comment', async function () { - const body = { moderationComment: 'b'.repeat(305) } + const body = { moderationComment: 'b'.repeat(3001) } await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body, 400) }) -- cgit v1.2.3