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',
-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',
font-weight: $font-semibold;
min-width: 200px;
display: inline-block;
+ vertical-align: top;
}
.moderation-expanded-text {
<td class="moderation-expanded" colspan="6">
<div>
<span i18n class="moderation-expanded-label">Reason:</span>
- <span class="moderation-expanded-text">{{ videoAbuse.reason }}</span>
+ <span class="moderation-expanded-text" [innerHTML]="toHtml(videoAbuse.reason)"></span>
</div>
<div *ngIf="videoAbuse.moderationComment">
<span i18n class="moderation-expanded-label">Moderation comment:</span>
- <span class="moderation-expanded-text">{{ videoAbuse.moderationComment }}</span>
+ <span class="moderation-expanded-text" [innerHTML]="toHtml(videoAbuse.moderationComment)"></span>
</div>
</td>
</tr>
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',
private notifier: Notifier,
private videoAbuseService: VideoAbuseService,
private confirmService: ConfirmService,
- private i18n: I18n
+ private i18n: I18n,
+ private markdownRenderer: MarkdownService
) {
super()
}
+ toHtml (text: string) {
+ return this.markdownRenderer.textMarkdownToHTML(text)
+ }
+
protected loadData () {
return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort)
.subscribe(
<tr>
<td class="moderation-expanded" colspan="6">
<span i18n class="moderation-expanded-label">Blacklist reason:</span>
- <span class="moderation-expanded-text">{{ videoBlacklist.reason }}</span>
+ <span class="moderation-expanded-text" [innerHTML]="toHtml(videoBlacklist.reason)"></span>
</td>
</tr>
</ng-template>
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',
private notifier: Notifier,
private confirmService: ConfirmService,
private videoBlacklistService: VideoBlacklistService,
+ private markdownRenderer: MarkdownService,
private i18n: I18n
) {
super()
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.'
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',
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.')
}
}
}
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',
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',
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, '<br />')
+ [keyToNormalize]: lineFeedToHtml(obj[keyToNormalize])
})
}
+function lineFeedToHtml (text: string) {
+ if (!text) return text
+
+ return text.replace(/\r?\n|\r/g, '<br />')
+}
+
function removeElementFromArray <T> (arr: T[], elem: T) {
const index = arr.indexOf(elem)
if (index !== -1) arr.splice(index, 1)
export {
sortBy,
durationToString,
+ lineFeedToHtml,
objectToUrlEncoded,
getParameterByName,
populateAsyncUserVideoChannels,
dateToHuman,
immutableAssign,
objectToFormData,
- lineFeedToHtml,
+ objectLineFeedToHtml,
removeElementFromArray,
scrollToTop
}
--- /dev/null
+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'
+ })
+ }
+ }
+ }
+ })
+ }
+}
--- /dev/null
+export * from './html-renderer.service'
+export * from './linkifier.service'
+export * from './markdown.service'
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'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
import {
CustomConfigValidatorsService,
+ InstanceValidatorsService,
LoginValidatorsService,
ReactiveFileComponent,
ResetPasswordValidatorsService,
- InstanceValidatorsService,
TextareaAutoResizeDirective,
UserValidatorsService,
VideoAbuseValidatorsService,
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: [
UserService,
VideoService,
AccountService,
- MarkdownService,
VideoChannelService,
VideoCaptionService,
VideoImportService,
UserHistoryService,
InstanceService,
+ MarkdownService,
+ LinkifierService,
+ HtmlRendererService,
+
I18nPrimengCalendarService,
ScreenService,
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(
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',
newParentComments: VideoComment[] = []
constructor (
- private linkifierService: LinkifierService,
+ private htmlRenderer: HtmlRendererService,
private authService: AuthService
) {}
}
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 ])
}
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 {
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(
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(
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',
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'
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',
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'
],
providers: [
- MarkdownService,
- LinkifierService,
VideoCommentService
]
})
+++ /dev/null
-export * from './markdown.service'
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 320
+const LAST_MIGRATION_VERSION = 325
// ---------------------------------------------------------------------------
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
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction,
+ queryInterface: Sequelize.QueryInterface,
+ sequelize: Sequelize.Sequelize
+}): Promise<void> {
+
+ {
+ 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
+}
-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 {
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'
export class VideoAbuseModel extends Model<VideoAbuseModel> {
@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)
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 })
})
})
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)
})