aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/shared')
-rw-r--r--client/src/app/shared/forms/form-validators/video-abuse-validators.service.ts8
-rw-r--r--client/src/app/shared/forms/markdown-textarea.component.ts2
-rw-r--r--client/src/app/shared/misc/help.component.ts2
-rw-r--r--client/src/app/shared/misc/utils.ts13
-rw-r--r--client/src/app/shared/renderer/html-renderer.service.ts35
-rw-r--r--client/src/app/shared/renderer/index.ts3
-rw-r--r--client/src/app/shared/renderer/linkifier.service.ts115
-rw-r--r--client/src/app/shared/renderer/markdown.service.ts79
-rw-r--r--client/src/app/shared/shared.module.ts9
-rw-r--r--client/src/app/shared/video-abuse/video-abuse.service.ts4
10 files changed, 255 insertions, 15 deletions
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 {
10 10
11 constructor (private i18n: I18n) { 11 constructor (private i18n: I18n) {
12 this.VIDEO_ABUSE_REASON = { 12 this.VIDEO_ABUSE_REASON = {
13 VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ], 13 VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
14 MESSAGES: { 14 MESSAGES: {
15 'required': this.i18n('Report reason is required.'), 15 'required': this.i18n('Report reason is required.'),
16 'minlength': this.i18n('Report reason must be at least 2 characters long.'), 16 'minlength': this.i18n('Report reason must be at least 2 characters long.'),
17 'maxlength': this.i18n('Report reason cannot be more than 300 characters long.') 17 'maxlength': this.i18n('Report reason cannot be more than 3000 characters long.')
18 } 18 }
19 } 19 }
20 20
21 this.VIDEO_ABUSE_MODERATION_COMMENT = { 21 this.VIDEO_ABUSE_MODERATION_COMMENT = {
22 VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ], 22 VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
23 MESSAGES: { 23 MESSAGES: {
24 'required': this.i18n('Moderation comment is required.'), 24 'required': this.i18n('Moderation comment is required.'),
25 'minlength': this.i18n('Moderation comment must be at least 2 characters long.'), 25 'minlength': this.i18n('Moderation comment must be at least 2 characters long.'),
26 'maxlength': this.i18n('Moderation comment cannot be more than 300 characters long.') 26 'maxlength': this.i18n('Moderation comment cannot be more than 3000 characters long.')
27 } 27 }
28 } 28 }
29 } 29 }
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 @@
1import { debounceTime, distinctUntilChanged } from 'rxjs/operators' 1import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
2import { Component, forwardRef, Input, OnInit } from '@angular/core' 2import { Component, forwardRef, Input, OnInit } from '@angular/core'
3import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 3import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
4import { MarkdownService } from '@app/videos/shared'
5import { Subject } from 'rxjs' 4import { Subject } from 'rxjs'
6import truncate from 'lodash-es/truncate' 5import truncate from 'lodash-es/truncate'
7import { ScreenService } from '@app/shared/misc/screen.service' 6import { ScreenService } from '@app/shared/misc/screen.service'
7import { MarkdownService } from '@app/shared/renderer'
8 8
9@Component({ 9@Component({
10 selector: 'my-markdown-textarea', 10 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 @@
1import { Component, Input, OnChanges, OnInit } from '@angular/core' 1import { Component, Input, OnChanges, OnInit } from '@angular/core'
2import { MarkdownService } from '@app/videos/shared'
3import { I18n } from '@ngx-translate/i18n-polyfill' 2import { I18n } from '@ngx-translate/i18n-polyfill'
3import { MarkdownService } from '@app/shared/renderer'
4 4
5@Component({ 5@Component({
6 selector: 'my-help', 6 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) {
102 return fd 102 return fd
103} 103}
104 104
105function lineFeedToHtml (obj: any, keyToNormalize: string) { 105function objectLineFeedToHtml (obj: any, keyToNormalize: string) {
106 return immutableAssign(obj, { 106 return immutableAssign(obj, {
107 [keyToNormalize]: obj[keyToNormalize].replace(/\r?\n|\r/g, '<br />') 107 [keyToNormalize]: lineFeedToHtml(obj[keyToNormalize])
108 }) 108 })
109} 109}
110 110
111function lineFeedToHtml (text: string) {
112 if (!text) return text
113
114 return text.replace(/\r?\n|\r/g, '<br />')
115}
116
111function removeElementFromArray <T> (arr: T[], elem: T) { 117function removeElementFromArray <T> (arr: T[], elem: T) {
112 const index = arr.indexOf(elem) 118 const index = arr.indexOf(elem)
113 if (index !== -1) arr.splice(index, 1) 119 if (index !== -1) arr.splice(index, 1)
@@ -131,6 +137,7 @@ function scrollToTop () {
131export { 137export {
132 sortBy, 138 sortBy,
133 durationToString, 139 durationToString,
140 lineFeedToHtml,
134 objectToUrlEncoded, 141 objectToUrlEncoded,
135 getParameterByName, 142 getParameterByName,
136 populateAsyncUserVideoChannels, 143 populateAsyncUserVideoChannels,
@@ -138,7 +145,7 @@ export {
138 dateToHuman, 145 dateToHuman,
139 immutableAssign, 146 immutableAssign,
140 objectToFormData, 147 objectToFormData,
141 lineFeedToHtml, 148 objectLineFeedToHtml,
142 removeElementFromArray, 149 removeElementFromArray,
143 scrollToTop 150 scrollToTop
144} 151}
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 @@
1import { Injectable } from '@angular/core'
2import { LinkifierService } from '@app/shared/renderer/linkifier.service'
3import * as sanitizeHtml from 'sanitize-html'
4
5@Injectable()
6export class HtmlRendererService {
7
8 constructor (private linkifier: LinkifierService) {
9
10 }
11
12 toSafeHtml (text: string) {
13 // Convert possible markdown to html
14 const html = this.linkifier.linkify(text)
15
16 return sanitizeHtml(html, {
17 allowedTags: [ 'a', 'p', 'span', 'br' ],
18 allowedSchemes: [ 'http', 'https' ],
19 allowedAttributes: {
20 'a': [ 'href', 'class', 'target' ]
21 },
22 transformTags: {
23 a: (tagName, attribs) => {
24 return {
25 tagName,
26 attribs: Object.assign(attribs, {
27 target: '_blank',
28 rel: 'noopener noreferrer'
29 })
30 }
31 }
32 }
33 })
34 }
35}
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 @@
1export * from './html-renderer.service'
2export * from './linkifier.service'
3export * 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 @@
1import { Injectable } from '@angular/core'
2import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
3// FIXME: use @types/linkify when https://github.com/DefinitelyTyped/DefinitelyTyped/pull/29682/files is merged?
4const linkify = require('linkifyjs')
5const linkifyHtml = require('linkifyjs/html')
6
7@Injectable()
8export class LinkifierService {
9
10 static CLASSNAME = 'linkified'
11
12 private linkifyOptions = {
13 className: {
14 mention: LinkifierService.CLASSNAME + '-mention',
15 url: LinkifierService.CLASSNAME + '-url'
16 }
17 }
18
19 constructor () {
20 // Apply plugin
21 this.mentionWithDomainPlugin(linkify)
22 }
23
24 linkify (text: string) {
25 return linkifyHtml(text, this.linkifyOptions)
26 }
27
28 private mentionWithDomainPlugin (linkify: any) {
29 const TT = linkify.scanner.TOKENS // Text tokens
30 const { TOKENS: MT, State } = linkify.parser // Multi tokens, state
31 const MultiToken = MT.Base
32 const S_START = linkify.parser.start
33
34 const TT_AT = TT.AT
35 const TT_DOMAIN = TT.DOMAIN
36 const TT_LOCALHOST = TT.LOCALHOST
37 const TT_NUM = TT.NUM
38 const TT_COLON = TT.COLON
39 const TT_SLASH = TT.SLASH
40 const TT_TLD = TT.TLD
41 const TT_UNDERSCORE = TT.UNDERSCORE
42 const TT_DOT = TT.DOT
43
44 function MENTION (this: any, value: any) {
45 this.v = value
46 }
47
48 linkify.inherits(MultiToken, MENTION, {
49 type: 'mentionWithDomain',
50 isLink: true,
51 toHref () {
52 return getAbsoluteAPIUrl() + '/services/redirect/accounts/' + this.toString().substr(1)
53 }
54 })
55
56 const S_AT = S_START.jump(TT_AT) // @
57 const S_AT_SYMS = new State()
58 const S_MENTION = new State(MENTION)
59 const S_MENTION_DIVIDER = new State()
60 const S_MENTION_DIVIDER_SYMS = new State()
61
62 // @_,
63 S_AT.on(TT_UNDERSCORE, S_AT_SYMS)
64
65 // @_*
66 S_AT_SYMS
67 .on(TT_UNDERSCORE, S_AT_SYMS)
68 .on(TT_DOT, S_AT_SYMS)
69
70 // Valid mention (not made up entirely of symbols)
71 S_AT
72 .on(TT_DOMAIN, S_MENTION)
73 .on(TT_LOCALHOST, S_MENTION)
74 .on(TT_TLD, S_MENTION)
75 .on(TT_NUM, S_MENTION)
76
77 S_AT_SYMS
78 .on(TT_DOMAIN, S_MENTION)
79 .on(TT_LOCALHOST, S_MENTION)
80 .on(TT_TLD, S_MENTION)
81 .on(TT_NUM, S_MENTION)
82
83 // More valid mentions
84 S_MENTION
85 .on(TT_DOMAIN, S_MENTION)
86 .on(TT_LOCALHOST, S_MENTION)
87 .on(TT_TLD, S_MENTION)
88 .on(TT_COLON, S_MENTION)
89 .on(TT_NUM, S_MENTION)
90 .on(TT_UNDERSCORE, S_MENTION)
91
92 // Mention with a divider
93 S_MENTION
94 .on(TT_AT, S_MENTION_DIVIDER)
95 .on(TT_SLASH, S_MENTION_DIVIDER)
96 .on(TT_DOT, S_MENTION_DIVIDER)
97
98 // Mention _ trailing stash plus syms
99 S_MENTION_DIVIDER.on(TT_UNDERSCORE, S_MENTION_DIVIDER_SYMS)
100 S_MENTION_DIVIDER_SYMS.on(TT_UNDERSCORE, S_MENTION_DIVIDER_SYMS)
101
102 // Once we get a word token, mentions can start up again
103 S_MENTION_DIVIDER
104 .on(TT_DOMAIN, S_MENTION)
105 .on(TT_LOCALHOST, S_MENTION)
106 .on(TT_TLD, S_MENTION)
107 .on(TT_NUM, S_MENTION)
108
109 S_MENTION_DIVIDER_SYMS
110 .on(TT_DOMAIN, S_MENTION)
111 .on(TT_LOCALHOST, S_MENTION)
112 .on(TT_TLD, S_MENTION)
113 .on(TT_NUM, S_MENTION)
114 }
115}
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 @@
1import { Injectable } from '@angular/core'
2
3import * as MarkdownIt from 'markdown-it'
4
5@Injectable()
6export class MarkdownService {
7 static TEXT_RULES = [
8 'linkify',
9 'autolink',
10 'emphasis',
11 'link',
12 'newline',
13 'list'
14 ]
15 static ENHANCED_RULES = MarkdownService.TEXT_RULES.concat([ 'image' ])
16
17 private textMarkdownIt: MarkdownIt.MarkdownIt
18 private enhancedMarkdownIt: MarkdownIt.MarkdownIt
19
20 constructor () {
21 this.textMarkdownIt = this.createMarkdownIt(MarkdownService.TEXT_RULES)
22 this.enhancedMarkdownIt = this.createMarkdownIt(MarkdownService.ENHANCED_RULES)
23 }
24
25 textMarkdownToHTML (markdown: string) {
26 if (!markdown) return ''
27
28 const html = this.textMarkdownIt.render(markdown)
29 return this.avoidTruncatedTags(html)
30 }
31
32 enhancedMarkdownToHTML (markdown: string) {
33 if (!markdown) return ''
34
35 const html = this.enhancedMarkdownIt.render(markdown)
36 return this.avoidTruncatedTags(html)
37 }
38
39 private createMarkdownIt (rules: string[]) {
40 const markdownIt = new MarkdownIt('zero', { linkify: true, breaks: true })
41
42 for (let rule of rules) {
43 markdownIt.enable(rule)
44 }
45
46 this.setTargetToLinks(markdownIt)
47
48 return markdownIt
49 }
50
51 private setTargetToLinks (markdownIt: MarkdownIt.MarkdownIt) {
52 // Snippet from markdown-it documentation: https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
53 const defaultRender = markdownIt.renderer.rules.link_open || function (tokens, idx, options, env, self) {
54 return self.renderToken(tokens, idx, options)
55 }
56
57 markdownIt.renderer.rules.link_open = function (tokens, index, options, env, self) {
58 const token = tokens[index]
59
60 const targetIndex = token.attrIndex('target')
61 if (targetIndex < 0) token.attrPush([ 'target', '_blank' ])
62 else token.attrs[targetIndex][1] = '_blank'
63
64 const relIndex = token.attrIndex('rel')
65 if (relIndex < 0) token.attrPush([ 'rel', 'noopener noreferrer' ])
66 else token.attrs[relIndex][1] = 'noopener noreferrer'
67
68 // pass token to default renderer.
69 return defaultRender(tokens, index, options, env, self)
70 }
71 }
72
73 private avoidTruncatedTags (html: string) {
74 return html.replace(/\*\*?([^*]+)$/, '$1')
75 .replace(/<a[^>]+>([^<]+)<\/a>\s*...((<\/p>)|(<\/li>)|(<\/strong>))?$/mi, '$1...')
76 .replace(/\[[^\]]+\]?\(?([^\)]+)$/, '$1')
77
78 }
79}
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'
6import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' 6import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component'
7import { HelpComponent } from '@app/shared/misc/help.component' 7import { HelpComponent } from '@app/shared/misc/help.component'
8import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' 8import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
9import { MarkdownService } from '@app/videos/shared'
10 9
11import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' 10import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
12import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' 11import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
@@ -34,10 +33,10 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
34import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 33import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
35import { 34import {
36 CustomConfigValidatorsService, 35 CustomConfigValidatorsService,
36 InstanceValidatorsService,
37 LoginValidatorsService, 37 LoginValidatorsService,
38 ReactiveFileComponent, 38 ReactiveFileComponent,
39 ResetPasswordValidatorsService, 39 ResetPasswordValidatorsService,
40 InstanceValidatorsService,
41 TextareaAutoResizeDirective, 40 TextareaAutoResizeDirective,
42 UserValidatorsService, 41 UserValidatorsService,
43 VideoAbuseValidatorsService, 42 VideoAbuseValidatorsService,
@@ -67,6 +66,7 @@ import { UserHistoryService } from '@app/shared/users/user-history.service'
67import { UserNotificationService } from '@app/shared/users/user-notification.service' 66import { UserNotificationService } from '@app/shared/users/user-notification.service'
68import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component' 67import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component'
69import { InstanceService } from '@app/shared/instance/instance.service' 68import { InstanceService } from '@app/shared/instance/instance.service'
69import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer'
70 70
71@NgModule({ 71@NgModule({
72 imports: [ 72 imports: [
@@ -167,7 +167,6 @@ import { InstanceService } from '@app/shared/instance/instance.service'
167 UserService, 167 UserService,
168 VideoService, 168 VideoService,
169 AccountService, 169 AccountService,
170 MarkdownService,
171 VideoChannelService, 170 VideoChannelService,
172 VideoCaptionService, 171 VideoCaptionService,
173 VideoImportService, 172 VideoImportService,
@@ -192,6 +191,10 @@ import { InstanceService } from '@app/shared/instance/instance.service'
192 UserHistoryService, 191 UserHistoryService,
193 InstanceService, 192 InstanceService,
194 193
194 MarkdownService,
195 LinkifierService,
196 HtmlRendererService,
197
195 I18nPrimengCalendarService, 198 I18nPrimengCalendarService,
196 ScreenService, 199 ScreenService,
197 200
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 {
32 32
33 reportVideo (id: number, reason: string) { 33 reportVideo (id: number, reason: string) {
34 const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + id + '/abuse' 34 const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + id + '/abuse'
35 const body = { 35 const body = { reason }
36 reason
37 }
38 36
39 return this.authHttp.post(url, body) 37 return this.authHttp.post(url, body)
40 .pipe( 38 .pipe(