diff --git a/css/custom_linehighlighting.css b/css/custom_linehighlighting.css new file mode 100644 index 00000000..3541f4dc --- /dev/null +++ b/css/custom_linehighlighting.css @@ -0,0 +1,99 @@ +li.line-selected, +li.line-selected-extra, +li.line-highlighted { + background: #FFF8C5 !important; + position: relative !important; +} + +li .highlighter-selected { + position: absolute; + left: -52px; + display: none; + color: #5CB85C; +} + +li .highlighter-controls { + position: absolute; + left: -35px; + display: none; +} + +li.line-selected .highlighter-selected { + display: inline-flex; +} + +li.line-selected-extra:before { + display: inline-flex; + width: 3px; + height: 19px; + background: #FF5500; + content: ""; + left: -52px; + position: absolute; +} + +li.line-selected .highlighter-selected .left-line { + display: inline-flex; + width: 3px; + height: 19px; + background: #FF5500; +} + +li.line-highlighted .highlighter-controls { + display: inline-flex; + z-index: 999; +} + +li.line-highlighted .highlighter-controls .hc-toggle { + color: #333; + background-color: #f5f5f5; + border: 1px solid #ccc; + padding: 2px 8px; + border-radius: 3px; + display: inline-flex; + cursor: pointer; +} + +li.line-highlighted .highlighter-controls .hc-toggle:hover { + background-color: #E4E4E4; +} + +li.line-highlighted .highlighter-controls .hc-dropdown { + display: none; + position: absolute; + color: #333; + background-color: #f5f5f5; + border: 1px solid #ccc; + padding: 2px 0; + border-radius: 3px; + left: 35px; +} + +li.line-highlighted .highlighter-controls .hc-dropdown ul { + display: flex; + flex-direction: column; + padding: 0px; + margin: 0px; + list-style: none; +} + +li.line-highlighted .highlighter-controls .hc-dropdown ul li { + display: inline-flex; + cursor: pointer; + white-space: nowrap; + width: 100%; + padding: 2px 8px; +} + +li.line-highlighted .highlighter-controls .hc-dropdown ul li:hover { + background-color: #E4E4E4; +} + +li.line-highlighted .highlighter-controls.hc-open .hc-dropdown { + display: inline-flex; +} + +li.line-highlighted .highlighter-controls .hc-dropdown ul li.extend-line, +li.line-highlighted .highlighter-controls .hc-dropdown ul li.unselect-line { + display: none; +} \ No newline at end of file diff --git a/js/privatebin.js b/js/privatebin.js index 20e8c5e8..4f2fece1 100644 --- a/js/privatebin.js +++ b/js/privatebin.js @@ -13,19 +13,24 @@ // global Base64, DOMPurify, FileReader, RawDeflate, history, navigator, prettyPrint, prettyPrintOne, showdown, kjua +var globalScrollPosition; +var globalSecondPositionMultiple; +var selectedLine; +var allowedReset = false; + jQuery.fn.draghover = function() { 'use strict'; return this.each(function() { let collection = $(), self = $(this); - + self.on('dragenter', function(e) { if (collection.length === 0) { self.trigger('draghoverstart'); } collection = collection.add(e.target); }); - + self.on('dragleave drop', function(e) { collection = collection.not(e.target); if (collection.length === 0) { @@ -42,6 +47,187 @@ jQuery(document).ready(function() { $.PrivateBin.Controller.init(); }); +function parse_query_string(query) { + var vars = query.split("&"); + var query_string = {}; + for (var i = 0; i < vars.length; i++) { + var pair = vars[i].split("="); + var key = decodeURIComponent(pair[0]); + var value = decodeURIComponent(pair[1]); + // If first entry with this name + if (typeof query_string[key] === "undefined") { + query_string[key] = decodeURIComponent(value); + // If second entry with this name + } else if (typeof query_string[key] === "string") { + var arr = [query_string[key], decodeURIComponent(value)]; + query_string[key] = arr; + // If third or later entry with this name + } else { + query_string[key].push(decodeURIComponent(value)); + } + } + return query_string; +} + +function markLines(a, b) { + for (var i = a+1; i < b+1; i++) { + $(".linenums").children().eq(i).addClass("line-selected-extra"); + } +} + +function selectLinesBetween() { + var a = globalScrollPosition < globalSecondPositionMultiple ? globalScrollPosition : globalSecondPositionMultiple; + var b = globalScrollPosition > globalSecondPositionMultiple ? globalScrollPosition : globalSecondPositionMultiple; + + globalScrollPosition = a; + globalSecondPositionMultiple = b; + + allowedReset = false; + $(".linenums").children().eq(globalScrollPosition).find(".select-line").click(); + allowedReset = true; + + markLines(a, b); +} + +function attachLineHighlighter() { + var optsOpen = false; + + if ($("#prettyprint").length < 1 || $("#prettyprint").find("li").length < 1) return; + + $("#prettyprint").find("li").append(` +
+ `.trim()); + + setTimeout(function(){ + $("#prettyprint").find("li").hover(function () { + if (optsOpen) return; + + $(".line-highlighted").removeClass("line-highlighted"); + $(this).addClass("line-highlighted"); + }); + + $(".hc-toggle").click(function(e) { + e.stopPropagation(); + + optsOpen = true; + $(this).parent().addClass("hc-open"); + + if (!globalScrollPosition) $(this).find(".extend-line").hide(); + else { + var selectS = $(".linenums").children().eq(globalScrollPosition); + selectS.find(".extend-line").hide(); + } + }); + + $(".hc-toggle").one("click", function() { + $(".extend-line").show(); + + var qs = parse_query_string(window.location.href); + if (!globalScrollPosition && !qs.s) $(".extend-line").hide(); + }); + + $(document).on('click', function(e) { + optsOpen = false; + $(".highlighter-controls").removeClass("hc-open"); + }); + + $(".extend-line").click(function() { + globalSecondPositionMultiple = $(this).parents("li").index(); + + if (!globalScrollPosition) { + var qs = parse_query_string(window.location.href); + globalScrollPosition = Number(qs.s); + } + + selectLinesBetween(); + + var freshURL = window.location.href.split('&')[0]; + + if (!freshURL.includes("#")) return; + + var URLA = ""; + if (globalScrollPosition && globalScrollPosition!="") URLA+=`&s=${globalScrollPosition+1}`; + if (globalSecondPositionMultiple && globalSecondPositionMultiple!="") URLA+=`&e=${globalSecondPositionMultiple+1}`; + + window.history.pushState('page2', 'Title', freshURL + URLA); + }); + + $(".select-line").click(function() { + $(".select-line").show(); + $(".extend-line").show(); + $(".unselect-line").hide(); + + $(this).hide(); + $(this).siblings(".extend-line").hide(); + $(this).siblings(".unselect-line").show(); + + globalScrollPosition = $(this).parents("li").index(); + if (allowedReset) globalSecondPositionMultiple = null; + + $(".line-selected").removeClass("line-selected"); + $(".line-selected-extra").removeClass("line-selected-extra"); + + $(this).parents("li").addClass("line-selected"); + + optsOpen = false; + $(".highlighter-controls").removeClass("hc-open"); + + var freshURL = window.location.href.split('&')[0]; + + if (!freshURL.includes("#")) return; + + var URLA = ""; + if (globalScrollPosition && globalScrollPosition!="") URLA+=`&s=${globalScrollPosition+1}`; + if (globalSecondPositionMultiple && globalSecondPositionMultiple!="") URLA+=`&e=${globalSecondPositionMultiple+1}`; + + window.history.pushState('page2', 'Title', freshURL + URLA); + }); + + $(".unselect-line").click(function() { + $(this).hide(); + $(this).siblings(".select-line").show(); + $(".extend-line").hide(); + + globalScrollPosition = null; + globalSecondPositionMultiple = null; + + $(".line-selected").removeClass("line-selected"); + $(".line-selected-extra").removeClass("line-selected-extra"); + + optsOpen = false; + $(".highlighter-controls").removeClass("hc-open"); + + var freshURL = window.location.href.split('&')[0]; + window.history.pushState('page2', 'Title', freshURL); + }); + + $(document).ready(function() { + var qs = parse_query_string(window.location.href); + + if (qs.s && qs.s != "") { + $(".line-selected").removeClass("line-selected"); + $(".line-selected-extra").removeClass("line-selected-extra"); + + selectedLine = $(".linenums").children().eq(qs.s) + selectedLine.addClass("line-selected"); + + $(".line-selected").find(".hc-toggle").one('click', function(event) { + selectedLine.find(".unselect-line").show(); + selectedLine.find(".select-line").hide(); + }); + + if (qs.e && qs.e != "") { + markLines(Number(qs.s)-2, Number(qs.e)-1); + } + + $([document.documentElement, document.body]).animate({ + scrollTop: selectedLine.offset().top - 50 + }, 500); + } + }) + }, 150); +} + jQuery.PrivateBin = (function($, RawDeflate) { 'use strict'; @@ -243,18 +429,6 @@ jQuery.PrivateBin = (function($, RawDeflate) { */ const day = 86400; - /** - * number of seconds in a week - * - * = 60 * 60 * 24 * 7 seconds - * - * @name Helper.week - * @private - * @enum {number} - * @readonly - */ - const week = 604800; - /** * number of seconds in a month (30 days, an approximation) * @@ -338,7 +512,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { */ me.durationToSeconds = function(duration) { - let pieces = duration.split(/(\D+)/), + let pieces = duration.split(/\d+/), factor = pieces[0] || 0, timespan = pieces[1] || pieces[0]; switch (timespan) @@ -349,8 +523,6 @@ jQuery.PrivateBin = (function($, RawDeflate) { return factor * hour; case 'day': return factor * day; - case 'week': - return factor * week; case 'month': return factor * month; case 'year': @@ -405,11 +577,9 @@ jQuery.PrivateBin = (function($, RawDeflate) { me.urls2links = function(element) { element.html( - DOMPurify.sanitize( - element.html().replace( - /(((https?|ftp):\/\/[\w?!=&.\/-;#@~%+*-]+(?![\w\s?!&.\/;#~%"=-]>))|((magnet):[\w?=&.\/-;#@~%+*-]+))/ig, - '$1' - ) + element.html().replace( + /(((https?|ftp):\/\/[\w?!=&.\/-;#@~%+*-]+(?![\w\s?!&.\/;#~%"=-]>))|((magnet):[\w?=&.\/-;#@~%+*-]+))/ig, + '$1' ) ); }; @@ -535,7 +705,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { /** * calculate expiration date given initial date and expiration period - * + * * @name Helper.calculateExpirationDate * @function * @param {Date} initialDate - may not be empty @@ -548,7 +718,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { if (typeof expirationDisplayStringOrSecondsToExpire === 'string') { secondsToExpiration = me.durationToSeconds(expirationDisplayStringOrSecondsToExpire); } - + if (typeof secondsToExpiration !== 'number') { throw new Error('Cannot calculate expiration date.'); } @@ -601,7 +771,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { * @prop {string[]} * @readonly */ - const supportedLanguages = ['bg', 'cs', 'de', 'es', 'fr', 'he', 'hu', 'it', 'lt', 'no', 'nl', 'pl', 'pt', 'oc', 'ru', 'sl', 'uk', 'zh']; + const supportedLanguages = ['bg', 'cs', 'de', 'es', 'fr', 'it', 'hu', 'no', 'nl', 'pl', 'pt', 'oc', 'ru', 'sl', 'uk', 'zh']; /** * built in language @@ -782,10 +952,6 @@ jQuery.PrivateBin = (function($, RawDeflate) { case 'oc': case 'zh': return n > 1 ? 1 : 0; - case 'he': - return n === 1 ? 0 : (n === 2 ? 1 : ((n < 0 || n > 10) && (n % 10 === 0) ? 2 : 3)); - case 'lt': - return n % 10 === 1 && n % 100 !== 11 ? 0 : ((n % 10 >= 2 && n % 100 < 10 || n % 100 >= 20) ? 1 : 2); case 'pl': return n === 1 ? 0 : (n % 10 >= 2 && n %10 <=4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2); case 'ru': @@ -1995,11 +2161,19 @@ jQuery.PrivateBin = (function($, RawDeflate) { return a.length - b.length; })[0]; if (typeof shortUrl === 'string' && shortUrl.length > 0) { + var URLA = ""; + if (globalScrollPosition && globalScrollPosition!="") URLA+=`&s=${globalScrollPosition+1}`; + if (globalSecondPositionMultiple && globalSecondPositionMultiple!="") URLA+=`&e=${globalSecondPositionMultiple+1}`; + + I18n._( + $('#pastelink'), + `Your paste is %s${URLA} (Hit [Ctrl]+[c] to copy)`, + shortUrl, shortUrl + ); // we disable the button to avoid calling shortener again $shortenButton.addClass('buttondisabled'); - // update link - $pasteUrl.text(shortUrl); - $pasteUrl.prop('href', shortUrl); + // save newly created element + $pasteUrl = $('#pasteurl'); // we pre-select the link so that the user only has to [Ctrl]+[c] the link Helper.selectText($pasteUrl[0]); return; @@ -2049,9 +2223,13 @@ jQuery.PrivateBin = (function($, RawDeflate) { */ me.createPasteNotification = function(url, deleteUrl) { + var URLA = ""; + if (globalScrollPosition && globalScrollPosition!="") URLA+=`&s=${globalScrollPosition+1}`; + if (globalSecondPositionMultiple && globalSecondPositionMultiple!="") URLA+=`&e=${globalSecondPositionMultiple+1}`; + I18n._( $('#pastelink'), - 'Your paste is %s (Hit [Ctrl]+[c] to copy)', + `Your paste is %s${URLA} (Hit [Ctrl]+[c] to copy)`, url, url ); // save newly created element @@ -2420,7 +2598,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { /** * hides the Editor * - * @name Editor.hide + * @name Editor.reset * @function */ me.hide = function() @@ -2550,6 +2728,8 @@ jQuery.PrivateBin = (function($, RawDeflate) { Helper.htmlEntities(text), null, true ) ); + + attachLineHighlighter(); } else { // = 'plaintext' $prettyPrint.text(text); @@ -2767,8 +2947,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { // extract mediaType const mediaType = attachmentData.substring(5, mediaTypeEnd); // extract data and convert to binary - const rawData = attachmentData.substring(base64Start); - const decodedData = rawData.length > 0 ? atob(rawData) : ''; + const decodedData = atob(attachmentData.substring(base64Start)); // Transform into a Blob const buf = new Uint8Array(decodedData.length); @@ -3127,15 +3306,19 @@ jQuery.PrivateBin = (function($, RawDeflate) { */ function addClipboardEventHandler() { $(document).on('paste', function (event) { + if (TopNav.isAttachmentReadonly()) { + event.stopPropagation(); + event.preventDefault(); + return false; + } const items = (event.clipboardData || event.originalEvent.clipboardData).items; - const lastItem = items[items.length - 1]; - if (lastItem.kind === 'file') { - if (TopNav.isAttachmentReadonly()) { - event.stopPropagation(); - event.preventDefault(); - return false; - } else { - readFileData(lastItem.getAsFile()); + for (let i = 0; i < items.length; ++i) { + if (items[i].kind === 'file') { + //Clear the file input: + $fileInput.wrap('
').closest('form').get(0).reset(); + $fileInput.unwrap(); + + readFileData(items[i].getAsFile()); } } }); @@ -3614,20 +3797,6 @@ jQuery.PrivateBin = (function($, RawDeflate) { } } - - /** - * Clear the attachment input in the top navigation. - * - * @name TopNav.clearAttachmentInput - * @function - */ - function clearAttachmentInput() - { - // hide UI for selected files - // our up-to-date jQuery can handle it :) - $fileWrap.find('input').val(''); - } - /** * return raw text * @@ -3722,7 +3891,9 @@ jQuery.PrivateBin = (function($, RawDeflate) { // in any case, remove saved attachment data AttachmentViewer.removeAttachmentData(); - clearAttachmentInput(); + // hide UI for selected files + // our up-to-date jQuery can handle it :) + $fileWrap.find('input').val(''); AttachmentViewer.clearDragAndDrop(); // pevent '#' from appearing in the URL @@ -3747,11 +3918,11 @@ jQuery.PrivateBin = (function($, RawDeflate) { /** * Template Email body. - * + * * @name TopNav.templateEmailBody - * @private - * @param {string} expirationDateString - * @param {bool} isBurnafterreading + * @private + * @param {string} expirationDateString + * @param {bool} isBurnafterreading */ function templateEmailBody(expirationDateString, isBurnafterreading) { @@ -3765,12 +3936,8 @@ jQuery.PrivateBin = (function($, RawDeflate) { if (expirationDateString !== null) { emailBody += EOL; emailBody += BULLET; - // avoid DOMPurify mess with forward slash in expirationDateString - emailBody += Helper.sprintf( - I18n._( - 'This link will expire after %s.', - '%s' - ), + emailBody += I18n._( + 'This link will expire after %s.', expirationDateString ); } @@ -3793,10 +3960,10 @@ jQuery.PrivateBin = (function($, RawDeflate) { /** * Trigger Email send. - * + * * @name TopNav.triggerEmailSend - * @private - * @param {string} emailBody + * @private + * @param {string} emailBody */ function triggerEmailSend(emailBody) { @@ -4009,7 +4176,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { /** * show the "email" button - * + * * @name TopNav.showEmailbutton * @function * @param {int|undefined} optionalRemainingTimeInSeconds @@ -4037,7 +4204,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { /** * hide the "email" button - * + * * @name TopNav.hideEmailButton * @function */ @@ -4071,7 +4238,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { /** * only hides the qr code button - * + * * @name TopNav.hideQrCodeButton * @function */ @@ -4082,7 +4249,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { /** * hide all irrelevant buttons when viewing burn after reading paste - * + * * @name TopNav.hideBurnAfterReadingButtons * @function */ @@ -4118,7 +4285,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { /** * hides the custom attachment - * + * * @name TopNav.hideCustomAttachment * @function */ @@ -4141,24 +4308,6 @@ jQuery.PrivateBin = (function($, RawDeflate) { } }; - /** - * Reset the top navigation back to it's default values. - * - * @name TopNav.resetInput - * @function - */ - me.resetInput = function() - { - clearAttachmentInput(); - - $openDiscussion.prop('checked', false); - $burnAfterReading.prop('checked', false); - $openDiscussionOption.removeClass('buttondisabled'); - $burnAfterReadingOption.removeClass('buttondisabled'); - - // TODO: reset expiration time - }; - /** * returns the currently set expiration time * @@ -4260,7 +4409,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { /** * Highlight file upload - * + * * @name TopNav.highlightFileupload * @function */ @@ -4279,7 +4428,7 @@ jQuery.PrivateBin = (function($, RawDeflate) { /** * set the format on bootstrap templates in dropdown programmatically - * + * * @name TopNav.setFormat * @function */ @@ -4290,14 +4439,14 @@ jQuery.PrivateBin = (function($, RawDeflate) { /** * returns if attachment dropdown is readonly, not editable - * + * * @name TopNav.isAttachmentReadonly * @function * @return {bool} */ me.isAttachmentReadonly = function() { - return !createButtonsDisplayed || $attach.hasClass('hidden'); + return createButtonsDisplayed && $attach.hasClass('hidden'); } /** @@ -5180,7 +5329,6 @@ jQuery.PrivateBin = (function($, RawDeflate) { Editor.show(); Editor.focusInput(); AttachmentViewer.removeAttachment(); - TopNav.resetInput(); TopNav.showCreateButtons(); @@ -5345,23 +5493,6 @@ jQuery.PrivateBin = (function($, RawDeflate) { SAFE_FOR_JQUERY: true }); - // Add a hook to make all links open a new window - DOMPurify.addHook('afterSanitizeAttributes', function(node) { - // set all elements owning target to target=_blank - if ('target' in node && node.id !== 'pasteurl') { - node.setAttribute('target', '_blank'); - } - // set non-HTML/MathML links to xlink:show=new - if (!node.hasAttribute('target') - && (node.hasAttribute('xlink:href') - || node.hasAttribute('href'))) { - node.setAttribute('xlink:show', 'new'); - } - if ('rel' in node) { - node.setAttribute('rel', 'nofollow noopener noreferrer'); - } - }); - // center all modals $('.modal').on('show.bs.modal', function(e) { $(e.target).css({ @@ -5393,12 +5524,6 @@ jQuery.PrivateBin = (function($, RawDeflate) { } me.initZ(); - // if delete token is passed (i.e. paste has been deleted by this - // access), there is nothing more to do - if (Model.hasDeleteToken()) { - return; - } - // check whether existing paste needs to be shown try { Model.getPasteId(); @@ -5407,10 +5532,11 @@ jQuery.PrivateBin = (function($, RawDeflate) { return me.newPaste(); } - // always reload on back button to invalidate cache(protect burn after read paste) - window.addEventListener('popstate', () => { - window.location.reload(); - }); + // if delete token is passed (i.e. paste has been deleted by this + // access), there is nothing more to do + if (Model.hasDeleteToken()) { + return; + } // display an existing paste return me.showPaste(); diff --git a/tpl/bootstrap.php b/tpl/bootstrap.php index e171e9ef..cfe71550 100644 --- a/tpl/bootstrap.php +++ b/tpl/bootstrap.php @@ -13,7 +13,7 @@ $isPage = substr($template, -5) === '-page'; <?php echo I18n::_($NAME); ?> - + - - - - - + + + + - + - - - - - - - - - - - - +