2020-09-16 14:52:00 -05:00
import Attachments from '/models/attachments' ;
2021-08-03 23:35:12 +02:00
const specialHandles = [
{ userId : 'board_members' , username : 'board_members' } ,
{ userId : 'card_members' , username : 'card_members' }
] ;
const specialHandleNames = specialHandles . map ( m => m . username ) ;
2021-11-19 00:29:56 +01:00
BlazeComponent . extendComponent ( {
onRendered ( ) {
const textareaSelector = 'textarea' ;
const mentions = [
// User mentions
{
2022-01-02 18:44:28 +01:00
match : /\B@([\w.-]*)$/ ,
2021-11-19 00:29:56 +01:00
search ( term , callback ) {
const currentBoard = Boards . findOne ( Session . get ( 'currentBoard' ) ) ;
callback (
_ . union (
currentBoard
. activeMembers ( )
. map ( member => {
const user = Users . findOne ( member . userId ) ;
const username = user . username ;
2022-01-02 18:44:28 +01:00
const fullName = user . profile && user . profile !== undefined && user . profile . fullname ? user . profile . fullname : "" ;
2022-01-02 19:35:47 +01:00
return username . includes ( term ) || fullName . includes ( term ) ? user : null ;
2021-11-19 00:29:56 +01:00
} )
2022-01-02 19:35:47 +01:00
. filter ( Boolean ) , [ ... specialHandles ] )
2021-11-19 00:29:56 +01:00
) ;
} ,
2022-01-02 19:35:47 +01:00
template ( user ) {
if ( user . profile && user . profile . fullname ) {
return ( user . profile . fullname + " (" + user . username + ")" ) ;
}
return user . username ;
2021-11-19 00:29:56 +01:00
} ,
2022-01-02 19:35:47 +01:00
replace ( user ) {
if ( user . profile && user . profile . fullname ) {
return ` @ ${ user . username } ( ${ user . profile . fullname } ) ` ;
}
return ` @ ${ user . username } ` ;
2021-11-19 00:29:56 +01:00
} ,
index : 1 ,
Renaissance
_,,ad8888888888bba,_
,ad88888I888888888888888ba,
,88888888I88888888888888888888a,
,d888888888I8888888888888888888888b,
d88888PP"""" ""YY88888888888888888888b,
,d88"'__,,--------,,,,.;ZZZY8888888888888,
,8IIl'" ;;l"ZZZIII8888888888,
,I88l;' ;lZZZZZ888III8888888,
,II88Zl;. ;llZZZZZ888888I888888,
,II888Zl;. .;;;;;lllZZZ888888I8888b
,II8888Z;; `;;;;;''llZZ8888888I8888,
II88888Z;' .;lZZZ8888888I888b
II88888Z; _,aaa, .,aaaaa,__.l;llZZZ88888888I888
II88888IZZZZZZZZZ, .ZZZZZZZZZZZZZZ;llZZ88888888I888,
II88888IZZ<'(@@>Z| |ZZZ<'(@@>ZZZZ;;llZZ888888888I88I
,II88888; `""" ;| |ZZ; `""" ;;llZ8888888888I888
II888888l `;; .;llZZ8888888888I888,
,II888888Z; ;;; .;;llZZZ8888888888I888I
III888888Zl; .., `;; ,;;lllZZZ88888888888I888
II88888888Z;;...;(_ _) ,;;;llZZZZ88888888888I888,
II88888888Zl;;;;;' `--'Z;. .,;;;;llZZZZ88888888888I888b
]I888888888Z;;;;' ";llllll;..;;;lllZZZZ88888888888I8888,
II888888888Zl.;;"Y88bd888P";;,..;lllZZZZZ88888888888I8888I
II8888888888Zl;.; `"PPP";;;,..;lllZZZZZZZ88888888888I88888
II888888888888Zl;;. `;;;l;;;;lllZZZZZZZZW88888888888I88888
`II8888888888888Zl;. ,;;lllZZZZZZZZWMZ88888888888I88888
II8888888888888888ZbaalllZZZZZZZZZWWMZZZ8888888888I888888,
`II88888888888888888b"WWZZZZZWWWMMZZZZZZI888888888I888888b
`II88888888888888888;ZZMMMMMMZZZZZZZZllI888888888I8888888
`II8888888888888888 `;lZZZZZZZZZZZlllll888888888I8888888,
II8888888888888888, `;lllZZZZllllll;;.Y88888888I8888888b,
,II8888888888888888b .;;lllllll;;;.;..88888888I88888888b,
II888888888888888PZI;. .`;;;.;;;..; ...88888888I8888888888,
II888888888888PZ;;';;. ;. .;. .;. .. Y8888888I88888888888b,
,II888888888PZ;;' `8888888I8888888888888b,
II888888888' 888888I8888888888888888
,II888888888 ,888888I8888888888888888
,d88888888888 d888888I8888888888ZZZZZZ
,ad888888888888I 8888888I8888ZZZZZZZZZZZZ
888888888888888' 888888IZZZZZZZZZZZZZZZZZ
8888888888P'8P' Y888ZZZZZZZZZZZZZZZZZZZZ
888888888, " ,ZZZZZZZZZZZZZZZZZZZZZZZ
8888888888, ,ZZZZZZZZZZZZZZZZZZZZZZZZZZ
888888888888a, _ ,ZZZZZZZZZZZZZZZZZZZZ88888888
888888888888888ba,_d' ,ZZZZZZZZZZZZZZZZZ8888888888888
8888888888888888888888bbbaaa,,,______,ZZZZZZZZZZZZZZZ88888888888888888
88888888888888888888888888888888888ZZZZZZZZZZZZZZZ88888888888888888888
8888888888888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888
888888888888888888888888888888888ZZZZZZZZZZZZZZ88888888888888888888888
8888888888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888888
88888888888888888888888888888ZZZZZZZZZZZZZZ888888888888888888888888888
8888888888888888888888888888ZZZZZZZZZZZZZZ88888888888888888 Normand 8
88888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888 Veilleux 8
8888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888888888888
2015-05-12 19:20:58 +02:00
} ,
2021-11-19 00:29:56 +01:00
] ;
const enableTextarea = function ( ) {
const $textarea = this . $ ( textareaSelector ) ;
autosize ( $textarea ) ;
$textarea . escapeableTextComplete ( mentions ) ;
2020-03-23 22:29:20 +02:00
} ;
2021-11-19 00:29:56 +01:00
if ( Meteor . settings . public . RICHER _CARD _COMMENT _EDITOR !== false ) {
const isSmall = Utils . isMiniScreen ( ) ;
const toolbar = isSmall
? [
[ 'view' , [ 'fullscreen' ] ] ,
[ 'table' , [ 'table' ] ] ,
[ 'font' , [ 'bold' , 'underline' ] ] ,
//['fontsize', ['fontsize']],
[ 'color' , [ 'color' ] ] ,
]
: [
[ 'style' , [ 'style' ] ] ,
[ 'font' , [ 'bold' , 'underline' , 'clear' ] ] ,
[ 'fontsize' , [ 'fontsize' ] ] ,
[ 'fontname' , [ 'fontname' ] ] ,
[ 'color' , [ 'color' ] ] ,
[ 'para' , [ 'ul' , 'ol' , 'paragraph' ] ] ,
[ 'table' , [ 'table' ] ] ,
//['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled
[ 'insert' , [ 'link' ] ] , //, 'picture']], // modal popup has issue somehow :(
[ 'view' , [ 'fullscreen' , 'codeview' , 'help' ] ] ,
] ;
const cleanPastedHTML = function ( input ) {
const badTags = [
'style' ,
'script' ,
'applet' ,
'embed' ,
'noframes' ,
'noscript' ,
'meta' ,
'link' ,
'button' ,
'form' ,
] . join ( '|' ) ;
const badPatterns = new RegExp (
` (?: ${ [
` <( ${ badTags } )s*[^>][ \\ s \\ S]*?< \\ / \\ 1> ` ,
` <( ${ badTags } )[^>]*? \\ /> ` ,
] . join ( '|' ) } ) ` ,
'gi' ,
) ;
let output = input ;
// remove bad Tags
output = output . replace ( badPatterns , '' ) ;
// remove attributes ' style="..."'
const badAttributes = new RegExp (
` (?: ${ [
'on\\S+=([\'"]?).*?\\1' ,
'href=([\'"]?)javascript:.*?\\2' ,
'style=([\'"]?).*?\\3' ,
'target=\\S+' ,
] . join ( '|' ) } ) ` ,
'gi' ,
) ;
output = output . replace ( badAttributes , '' ) ;
output = output . replace ( /(<a )/gi , '$1target=_ ' ) ; // always to new target
return output ;
2019-07-22 23:33:44 -04:00
} ;
2021-11-19 00:29:56 +01:00
const editor = '.editor' ;
const selectors = [
` .js-new-description-form ${ editor } ` ,
` .js-new-comment-form ${ editor } ` ,
` .js-edit-comment ${ editor } ` ,
] . join ( ',' ) ; // only new comment and edit comment
const inputs = $ ( selectors ) ;
if ( inputs . length === 0 ) {
// only enable richereditor to new comment or edit comment no others
enableTextarea ( ) ;
} else {
const placeholder = inputs . attr ( 'placeholder' ) || '' ;
const mSummernotes = [ ] ;
const getSummernote = function ( input ) {
const idx = inputs . index ( input ) ;
if ( idx > - 1 ) {
return mSummernotes [ idx ] ;
}
return undefined ;
} ;
inputs . each ( function ( idx , input ) {
mSummernotes [ idx ] = $ ( input ) . summernote ( {
placeholder ,
callbacks : {
onInit ( object ) {
const originalInput = this ;
$ ( originalInput ) . on ( 'submitted' , function ( ) {
// when comment is submitted, the original textarea will be set to '', so shall we
if ( ! this . value ) {
const sn = getSummernote ( this ) ;
sn && sn . summernote ( 'code' , '' ) ;
}
2019-07-22 23:33:44 -04:00
} ) ;
2021-11-19 00:29:56 +01:00
const jEditor = object && object . editable ;
const toolbar = object && object . toolbar ;
if ( jEditor !== undefined ) {
jEditor . escapeableTextComplete ( mentions ) ;
}
if ( toolbar !== undefined ) {
const fBtn = toolbar . find ( '.btn-fullscreen' ) ;
fBtn . on ( 'click' , function ( ) {
const $this = $ ( this ) ,
isActive = $this . hasClass ( 'active' ) ;
$ ( '.minicards,#header-quick-access' ) . toggle ( ! isActive ) ; // mini card is still showing when editor is in fullscreen mode, we hide here manually
} ) ;
}
} ,
onImageUpload ( files ) {
const $summernote = getSummernote ( this ) ;
if ( files && files . length > 0 ) {
const image = files [ 0 ] ;
const currentCard = Utils . getCurrentCard ( ) ;
const MAX _IMAGE _PIXEL = Utils . MAX _IMAGE _PIXEL ;
const COMPRESS _RATIO = Utils . IMAGE _COMPRESS _RATIO ;
2020-09-13 22:17:58 -05:00
const processUpload = function ( file ) {
const uploader = Attachments . insert (
{
file ,
2020-09-14 01:07:17 -05:00
meta : Utils . getCommonAttachmentMetaFrom ( card ) ,
2020-09-13 22:17:58 -05:00
chunkSize : 'dynamic' ,
2021-11-19 00:29:56 +01:00
} ,
2020-09-13 22:17:58 -05:00
false ,
2021-11-19 00:29:56 +01:00
) ;
2020-09-14 01:07:17 -05:00
uploader . on ( 'uploaded' , ( error , fileRef ) => {
2020-09-13 22:17:58 -05:00
if ( ! error ) {
2020-09-14 01:07:17 -05:00
if ( fileRef . isImage ) {
const img = document . createElement ( 'img' ) ;
img . src = fileRef . link ( ) ;
img . setAttribute ( 'width' , '100%' ) ;
$summernote . summernote ( 'insertNode' , img ) ;
2020-09-13 22:17:58 -05:00
}
}
} ) ;
uploader . start ( ) ;
2021-11-19 00:29:56 +01:00
} ;
if ( MAX _IMAGE _PIXEL ) {
const reader = new FileReader ( ) ;
reader . onload = function ( e ) {
const dataurl = e && e . target && e . target . result ;
if ( dataurl !== undefined ) {
// need to shrink image
Utils . shrinkImage ( {
dataurl ,
maxSize : MAX _IMAGE _PIXEL ,
ratio : COMPRESS _RATIO ,
toBlob : true ,
callback ( blob ) {
if ( blob !== false ) {
blob . name = image . name ;
2020-09-13 22:17:58 -05:00
processUpload ( blob ) ;
2020-05-25 17:54:51 +03:00
}
2021-11-19 00:29:56 +01:00
} ,
2020-05-25 17:54:51 +03:00
} ) ;
2019-08-10 21:21:42 -04:00
}
2021-11-19 00:29:56 +01:00
} ;
reader . readAsDataURL ( image ) ;
} else {
2020-09-13 22:17:58 -05:00
processUpload ( image ) ;
2021-11-19 00:29:56 +01:00
}
2019-08-10 21:21:42 -04:00
}
2021-11-19 00:29:56 +01:00
} ,
onPaste ( e ) {
var clipboardData = e . clipboardData ;
var pastedData = clipboardData . getData ( 'Text' ) ;
2021-04-26 23:02:01 -07:00
2021-11-19 00:29:56 +01:00
//if pasted data is an image, exit
if ( ! pastedData . length ) {
e . preventDefault ( ) ;
return ;
}
2021-04-26 23:02:01 -07:00
2021-11-19 00:29:56 +01:00
// clear up unwanted tag info when user pasted in text
const thisNote = this ;
const updatePastedText = function ( object ) {
const someNote = getSummernote ( object ) ;
// Fix Pasting text into a card is adding a line before and after
// (and multiplies by pasting more) by changing paste "p" to "br".
// Fixes https://github.com/wekan/wekan/2890 .
// == Fix Start ==
someNote . execCommand ( 'defaultParagraphSeparator' , false , 'br' ) ;
// == Fix End ==
const original = someNote . summernote ( 'code' ) ;
const cleaned = cleanPastedHTML ( original ) ; //this is where to call whatever clean function you want. I have mine in a different file, called CleanPastedHTML.
someNote . summernote ( 'code' , '' ) ; //clear original
someNote . summernote ( 'pasteHTML' , cleaned ) ; //this sets the displayed content editor to the cleaned pasted code.
} ;
setTimeout ( function ( ) {
//this kinda sucks, but if you don't do a setTimeout,
//the function is called before the text is really pasted.
updatePastedText ( thisNote ) ;
} , 10 ) ;
} ,
} ,
dialogsInBody : true ,
spellCheck : true ,
disableGrammar : false ,
disableDragAndDrop : false ,
toolbar ,
popover : {
image : [
[ 'imagesize' , [ 'imageSize100' , 'imageSize50' , 'imageSize25' ] ] ,
[ 'float' , [ 'floatLeft' , 'floatRight' , 'floatNone' ] ] ,
[ 'remove' , [ 'removeMedia' ] ] ,
] ,
link : [ [ 'link' , [ 'linkDialogShow' , 'unlink' ] ] ] ,
table : [
[ 'add' , [ 'addRowDown' , 'addRowUp' , 'addColLeft' , 'addColRight' ] ] ,
[ 'delete' , [ 'deleteRow' , 'deleteCol' , 'deleteTable' ] ] ,
] ,
air : [
[ 'color' , [ 'color' ] ] ,
[ 'font' , [ 'bold' , 'underline' , 'clear' ] ] ,
] ,
2021-04-29 13:26:49 +03:00
} ,
2021-11-19 00:29:56 +01:00
height : 200 ,
} ) ;
2019-07-22 23:33:44 -04:00
} ) ;
2021-11-19 00:29:56 +01:00
}
} else {
enableTextarea ( ) ;
2019-07-22 23:33:44 -04:00
}
2021-11-19 00:29:56 +01:00
} ,
events ( ) {
return [
{
2021-11-19 11:22:49 +01:00
'click a.fa.fa-copy' ( event ) {
2021-11-19 00:29:56 +01:00
const $editor = this . $ ( 'textarea.editor' ) ;
const promise = Utils . copyTextToClipboard ( $editor [ 0 ] . value ) ;
2021-11-19 12:07:42 +01:00
const $tooltip = this . $ ( '.copied-tooltip' ) ;
Utils . showCopied ( promise , $tooltip ) ;
2021-11-19 00:29:56 +01:00
} ,
}
]
2019-07-22 13:53:37 -04:00
}
2021-11-19 00:29:56 +01:00
} ) . register ( 'editor' ) ;
2015-05-26 20:30:01 +02:00
2021-05-07 02:13:20 +03:00
import DOMPurify from 'dompurify' ;
2020-03-23 22:29:20 +02:00
2020-11-10 18:03:17 -03:00
// Additional safeAttrValue function to allow for other specific protocols
// See https://github.com/leizongmin/js-xss/issues/52#issuecomment-241354114
2021-05-07 02:13:20 +03:00
/ *
2020-11-10 18:03:17 -03:00
function mySafeAttrValue ( tag , name , value , cssFilter ) {
// only when the tag is 'a' and attribute is 'href'
// then use your custom function
if ( tag === 'a' && name === 'href' ) {
// only filter the value if starts with 'cbthunderlink:' or 'aodroplink'
2020-11-29 04:19:28 +02:00
if (
/^thunderlink:/gi . test ( value ) ||
/^cbthunderlink:/gi . test ( value ) ||
2021-01-13 00:02:17 +02:00
/^aodroplink:/gi . test ( value ) ||
/^onenote:/gi . test ( value ) ||
/^file:/gi . test ( value ) ||
2021-01-26 13:54:22 +01:00
/^abasurl:/gi . test ( value ) ||
/^conisio:/gi . test ( value ) ||
2021-01-13 00:02:17 +02:00
/^mailspring:/gi . test ( value )
2020-11-29 04:19:28 +02:00
) {
2020-11-10 18:03:17 -03:00
return value ;
2020-11-29 04:19:28 +02:00
} else {
2020-11-10 22:01:04 -03:00
// use the default safeAttrValue function to process all non cbthunderlinks
return sanitizeXss . safeAttrValue ( tag , name , value , cssFilter ) ;
}
2020-11-10 18:03:17 -03:00
} else {
// use the default safeAttrValue function to process it
return sanitizeXss . safeAttrValue ( tag , name , value , cssFilter ) ;
}
2020-11-29 04:19:28 +02:00
}
2021-05-07 02:13:20 +03:00
* /
2020-11-10 18:03:17 -03:00
2015-09-06 23:42:52 +02:00
// XXX I believe we should compute a HTML rendered field on the server that
2018-02-12 23:52:22 +02:00
// would handle markdown and user mentions. We can simply have two
2015-09-06 23:42:52 +02:00
// fields, one source, and one compiled version (in HTML) and send only the
// compiled version to most users -- who don't need to edit.
// In the meantime, all the transformation are done on the client using the
// Blaze API.
2019-06-28 12:52:09 -05:00
const at = HTML . CharRef ( { html : '@' , str : '@' } ) ;
Blaze . Template . registerHelper (
'mentions' ,
new Template ( 'mentions' , function ( ) {
const view = this ;
let content = Blaze . toHTML ( view . templateContentBlock ) ;
const currentBoard = Boards . findOne ( Session . get ( 'currentBoard' ) ) ;
2020-11-29 04:19:28 +02:00
if ( ! currentBoard )
2021-05-07 02:13:20 +03:00
return HTML . Raw (
DOMPurify . sanitize ( content , { ALLOW _UNKNOWN _PROTOCOLS : true } ) ,
) ;
2021-08-03 23:35:12 +02:00
const knowedUsers = _ . union ( currentBoard . members . map ( member => {
2019-06-28 12:52:09 -05:00
const u = Users . findOne ( member . userId ) ;
if ( u ) {
member . username = u . username ;
}
return member ;
2021-08-03 23:35:12 +02:00
} ) , [ ... specialHandles ] ) ;
2022-01-02 18:44:28 +01:00
const mentionRegex = /\B@([\w.-]*)/gi ;
2015-09-06 23:42:52 +02:00
2019-06-28 12:52:09 -05:00
let currentMention ;
while ( ( currentMention = mentionRegex . exec ( content ) ) !== null ) {
2019-09-19 15:16:48 -04:00
const [ fullMention , quoteduser , simple ] = currentMention ;
const username = quoteduser || simple ;
2019-06-28 12:52:09 -05:00
const knowedUser = _ . findWhere ( knowedUsers , { username } ) ;
if ( ! knowedUser ) {
continue ;
}
2015-09-06 23:42:52 +02:00
2019-06-28 12:52:09 -05:00
const linkValue = [ ' ' , at , knowedUser . username ] ;
let linkClass = 'atMention js-open-member' ;
if ( knowedUser . userId === Meteor . userId ( ) ) {
linkClass += ' me' ;
}
2020-03-24 20:39:49 +02:00
// This @user mention link generation did open same Wekan
// window in new tab, so now A is changed to U so it's
// underlined and there is no link popup. This way also
// text can be selected more easily.
//const link = HTML.A(
const link = HTML . U (
2019-06-28 12:52:09 -05:00
{
class : linkClass ,
// XXX Hack. Since we stringify this render function result below with
// `Blaze.toHTML` we can't rely on blaze data contexts to pass the
// `userId` to the popup as usual, and we need to store it in the DOM
// using a data attribute.
'data-userId' : knowedUser . userId ,
} ,
linkValue ,
) ;
2015-09-06 23:42:52 +02:00
2019-06-28 12:52:09 -05:00
content = content . replace ( fullMention , Blaze . toHTML ( link ) ) ;
}
2020-03-23 22:29:20 +02:00
2021-05-07 02:13:20 +03:00
return HTML . Raw (
DOMPurify . sanitize ( content , { ALLOW _UNKNOWN _PROTOCOLS : true } ) ,
) ;
2019-06-28 12:52:09 -05:00
} ) ,
) ;
2020-03-23 22:29:20 +02:00
2015-09-01 22:26:48 +02:00
Template . viewer . events ( {
// Viewer sometimes have click-able wrapper around them (for instance to edit
// the corresponding text). Clicking a link shouldn't fire these actions, stop
// we stop these event at the viewer component level.
2019-06-28 12:52:09 -05:00
'click a' ( event , templateInstance ) {
2020-05-03 00:33:15 +02:00
const prevent = true ;
2019-06-28 12:52:09 -05:00
const userId = event . currentTarget . dataset . userid ;
2016-07-11 12:04:42 +02:00
if ( userId ) {
2019-06-28 12:52:09 -05:00
Popup . open ( 'member' ) . call ( { userId } , event , templateInstance ) ;
} else {
const href = event . currentTarget . href ;
2020-03-23 22:29:20 +02:00
if ( href ) {
2016-07-11 12:04:42 +02:00
window . open ( href , '_blank' ) ;
}
2015-09-06 23:42:52 +02:00
}
2019-08-07 23:44:45 -04:00
if ( prevent ) {
event . stopPropagation ( ) ;
// XXX We hijack the build-in browser action because we currently don't have
// `_blank` attributes in viewer links, and the transformer function is
// handled by a third party package that we can't configure easily. Fix that
// by using directly `_blank` attribute in the rendered HTML.
event . preventDefault ( ) ;
}
2015-09-03 23:12:46 +02:00
} ,
2015-09-01 22:26:48 +02:00
} ) ;