| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389 | 
							- import _sanitizeXss from 'xss';
 
- const ASIS = 'asis';
 
- const sanitizeXss = (input, options) => {
 
-   const defaultAllowedIframeSrc = /^(https:){0,1}\/\/.*?(youtube|vimeo|dailymotion|youku)/i;
 
-   const allowedIframeSrcRegex = (function() {
 
-     let reg = defaultAllowedIframeSrc;
 
-     const SAFE_IFRAME_SRC_PATTERN =
 
-       Meteor.settings.public.SAFE_IFRAME_SRC_PATTERN;
 
-     try {
 
-       if (SAFE_IFRAME_SRC_PATTERN !== undefined) {
 
-         reg = new RegExp(SAFE_IFRAME_SRC_PATTERN, 'i');
 
-       }
 
-     } catch (e) {
 
-       /*eslint no-console: ["error", { allow: ["warn", "error"] }] */
 
-       console.error('Wrong pattern specified', SAFE_IFRAM_SRC_PATTERN, e);
 
-     }
 
-     return reg;
 
-   })();
 
-   const targetWindow = '_blank';
 
-   const getHtmlDOM = html => {
 
-     const i = document.createElement('i');
 
-     i.innerHTML = html;
 
-     return i.firstChild;
 
-   };
 
-   options = {
 
-     onTag(tag, html, options) {
 
-       const htmlDOM = getHtmlDOM(html);
 
-       const getAttr = attr => {
 
-         return htmlDOM && attr && htmlDOM.getAttribute(attr);
 
-       };
 
-       if (tag === 'iframe') {
 
-         const clipCls = 'note-vide-clip';
 
-         if (!options.isClosing) {
 
-           const iframeCls = getAttr('class');
 
-           let safe = iframeCls.indexOf(clipCls) > -1;
 
-           const src = getAttr('src');
 
-           if (allowedIframeSrcRegex.exec(src)) {
 
-             safe = true;
 
-           }
 
-           if (safe)
 
-             return `<iframe src='${src}' class="${clipCls}" width=100% height=auto allowfullscreen></iframe>`;
 
-         } else {
 
-           // remove </iframe> tag
 
-           return '';
 
-         }
 
-       } else if (tag === 'a') {
 
-         if (!options.isClosing) {
 
-           if (getAttr(ASIS) === 'true') {
 
-             // if has a ASIS attribute, don't do anything, it's a member id
 
-             return html;
 
-           } else {
 
-             const href = getAttr('href');
 
-             if (href.match(/^((http(s){0,1}:){0,1}\/\/|\/)/)) {
 
-               // a valid url
 
-               return `<a href=${href} target=${targetWindow}>`;
 
-             }
 
-           }
 
-         }
 
-       } else if (tag === 'img') {
 
-         if (!options.isClosing) {
 
-           const src = getAttr('src');
 
-           if (src) {
 
-             return `<a href='${src}' class='swipebox'><img src='${src}' class="attachment-image-preview mCS_img_loaded"></a>`;
 
-           }
 
-         }
 
-       }
 
-       return undefined;
 
-     },
 
-     onTagAttr(tag, name, value) {
 
-       if (tag === 'img' && name === 'src') {
 
-         if (value && value.substr(0, 5) === 'data:') {
 
-           // allow image with dataURI src
 
-           return `${name}='${value}'`;
 
-         }
 
-       } else if (tag === 'a' && name === 'target') {
 
-         return `${name}='${targetWindow}'`; // always change a href target to a new window
 
-       }
 
-       return undefined;
 
-     },
 
-     ...options,
 
-   };
 
-   return _sanitizeXss(input, options);
 
- };
 
- Template.editor.onRendered(() => {
 
-   const textareaSelector = 'textarea';
 
-   const mentions = [
 
-     // User mentions
 
-     {
 
-       match: /\B@([\w.]*)$/,
 
-       search(term, callback) {
 
-         const currentBoard = Boards.findOne(Session.get('currentBoard'));
 
-         callback(
 
-           currentBoard
 
-             .activeMembers()
 
-             .map(member => {
 
-               const username = Users.findOne(member.userId).username;
 
-               return username.includes(term) ? username : null;
 
-             })
 
-             .filter(Boolean),
 
-         );
 
-       },
 
-       template(value) {
 
-         return value;
 
-       },
 
-       replace(username) {
 
-         return `@${username} `;
 
-       },
 
-       index: 1,
 
-     },
 
-   ];
 
-   const enableTextarea = function() {
 
-     const $textarea = this.$(textareaSelector);
 
-     autosize($textarea);
 
-     $textarea.escapeableTextComplete(mentions);
 
-   };
 
-   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', 'help']],
 
-         ];
 
-     const cleanPastedHTML = sanitizeXss;
 
-     const editor = '.editor';
 
-     const selectors = [
 
-       `.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() {
 
-                 // resetCommentInput has been called
 
-                 if (!this.value) {
 
-                   const sn = getSummernote(this);
 
-                   sn && sn.summernote('reset');
 
-                   object && object.editingArea.find('.note-placeholder').show();
 
-                 }
 
-               });
 
-               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 = Cards.findOne(Session.get('currentCard'));
 
-                 const MAX_IMAGE_PIXEL = Utils.MAX_IMAGE_PIXEL;
 
-                 const COMPRESS_RATIO = Utils.IMAGE_COMPRESS_RATIO;
 
-                 const insertImage = src => {
 
-                   const img = document.createElement('img');
 
-                   img.src = src;
 
-                   img.setAttribute('width', '100%');
 
-                   $summernote.summernote('insertNode', img);
 
-                 };
 
-                 const processData = function(fileObj) {
 
-                   Utils.processUploadedAttachment(
 
-                     currentCard,
 
-                     fileObj,
 
-                     attachment => {
 
-                       if (
 
-                         attachment &&
 
-                         attachment._id &&
 
-                         attachment.isImage()
 
-                       ) {
 
-                         attachment.one('uploaded', function() {
 
-                           const maxTry = 3;
 
-                           const checkItvl = 500;
 
-                           let retry = 0;
 
-                           const checkUrl = function() {
 
-                             // even though uploaded event fired, attachment.url() is still null somehow //TODO
 
-                             const url = attachment.url();
 
-                             if (url) {
 
-                               insertImage(
 
-                                 `${location.protocol}//${location.host}${url}`,
 
-                               );
 
-                             } else {
 
-                               retry++;
 
-                               if (retry < maxTry) {
 
-                                 setTimeout(checkUrl, checkItvl);
 
-                               }
 
-                             }
 
-                           };
 
-                           checkUrl();
 
-                         });
 
-                       }
 
-                     },
 
-                   );
 
-                 };
 
-                 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;
 
-                             processData(blob);
 
-                           }
 
-                         },
 
-                       });
 
-                     }
 
-                   };
 
-                   reader.readAsDataURL(image);
 
-                 } else {
 
-                   processData(image);
 
-                 }
 
-               }
 
-             },
 
-             onPaste() {
 
-               // clear up unwanted tag info when user pasted in text
 
-               const thisNote = this;
 
-               const updatePastedText = function(object) {
 
-                 const someNote = getSummernote(object);
 
-                 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('reset'); //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,
 
-           disableDragAndDrop: true,
 
-           toolbar,
 
-           popover: {
 
-             image: [
 
-               [
 
-                 'image',
 
-                 ['resizeFull', 'resizeHalf', 'resizeQuarter', 'resizeNone'],
 
-               ],
 
-               ['float', ['floatLeft', 'floatRight', 'floatNone']],
 
-               ['remove', ['removeMedia']],
 
-             ],
 
-             table: [
 
-               ['add', ['addRowDown', 'addRowUp', 'addColLeft', 'addColRight']],
 
-               ['delete', ['deleteRow', 'deleteCol', 'deleteTable']],
 
-             ],
 
-             air: [
 
-               ['color', ['color']],
 
-               ['font', ['bold', 'underline', 'clear']],
 
-             ],
 
-           },
 
-           height: 200,
 
-         });
 
-       });
 
-     }
 
-   } else {
 
-     enableTextarea();
 
-   }
 
- });
 
- // XXX I believe we should compute a HTML rendered field on the server that
 
- // would handle markdown and user mentions. We can simply have two
 
- // 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.
 
- 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'));
 
-     if (!currentBoard) return HTML.Raw(sanitizeXss(content));
 
-     const knowedUsers = currentBoard.members.map(member => {
 
-       const u = Users.findOne(member.userId);
 
-       if (u) {
 
-         member.username = u.username;
 
-       }
 
-       return member;
 
-     });
 
-     const mentionRegex = /\B@([\w.]*)/gi;
 
-     let currentMention;
 
-     while ((currentMention = mentionRegex.exec(content)) !== null) {
 
-       const [fullMention, username] = currentMention;
 
-       const knowedUser = _.findWhere(knowedUsers, { username });
 
-       if (!knowedUser) {
 
-         continue;
 
-       }
 
-       const linkValue = [' ', at, knowedUser.username];
 
-       let linkClass = 'atMention js-open-member';
 
-       if (knowedUser.userId === Meteor.userId()) {
 
-         linkClass += ' me';
 
-       }
 
-       const link = HTML.A(
 
-         {
 
-           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,
 
-           [ASIS]: 'true',
 
-         },
 
-         linkValue,
 
-       );
 
-       content = content.replace(fullMention, Blaze.toHTML(link));
 
-     }
 
-     return HTML.Raw(sanitizeXss(content));
 
-   }),
 
- );
 
- 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.
 
-   'click a'(event, templateInstance) {
 
-     let prevent = true;
 
-     const userId = event.currentTarget.dataset.userid;
 
-     if (userId) {
 
-       Popup.open('member').call({ userId }, event, templateInstance);
 
-     } else {
 
-       const href = event.currentTarget.href;
 
-       const child = event.currentTarget.firstElementChild;
 
-       if (child && child.tagName === 'IMG') {
 
-         prevent = false;
 
-       } else if (href) {
 
-         window.open(href, '_blank');
 
-       }
 
-     }
 
-     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();
 
-     }
 
-   },
 
- });
 
 
  |