1// Function to sanitize captions and prevent XSS 2function sanitizeElptCaption(caption) { 3 if (typeof caption !== 'string') return caption; 4 5 // Remove any HTML to prevent XSS 6 var tempDiv = jQuery('<div>'); 7 tempDiv.text(caption); 8 return tempDiv.html(); // Returns escaped text 9} 10 11// Safe function to decode HTML entities without executing JavaScript 12function safeDecodeHtml(str) { 13 if (typeof str !== 'string') return str; 14 15 // Check if it contains dangerous patterns before decoding 16 var dangerousPatterns = [ 17 /<script/i, 18 /<img[^&]*onerror/i, 19 /<[^&]*on\w+=/i, 20 /javascript:/i, 21 /<iframe/i, 22 /<object/i, 23 /<embed/i 24 ]; 25 26 for (var i = 0; i < dangerousPatterns.length; i++) { 27 if (dangerousPatterns[i].test(str)) { 28 // If it contains dangerous patterns, return only text without decoding 29 return str.replace(/&[#\w]+;/g, '').replace(/[<>]/g, ''); 30 } 31 } 32 33 // If it doesn't contain dangerous patterns, decode safely 34 var txt = document.createElement('textarea'); 35 txt.innerHTML = str; 36 var decoded = txt.value; 37 38 // Remove any remaining HTML tags as an extra security measure 39 decoded = decoded.replace(/<[^>]*>/g, ''); 40 41 return decoded; 42} 43 44// Function to sanitize all images in the DOM 45function sanitizeAllImages() { 46 jQuery('.elpt-portfolio img[title], a.elpt-portfolio-lightbox img[title]').each(function() { 47 var $img = jQuery(this); 48 var originalTitle = $img.attr('title'); 49 if (originalTitle) { 50 // Decode HTML entities safely, then sanitize 51 var decodedTitle = safeDecodeHtml(originalTitle); 52 var sanitizedTitle = sanitizeElptCaption(decodedTitle); 53 $img.attr('title', sanitizedTitle); 54 } 55 }); 56} 57 58// Execute sanitization immediately 59jQuery(document).ready(function() { 60 sanitizeAllImages(); 61}); 62 63jQuery(window).on('load', function () { 64 // Sanitize again on load 65 sanitizeAllImages(); 66 67 if (jQuery(".elpt-portfolio-content").length) { 68 69 // Process video links with data attributes 70 jQuery('a.elpt-portfolio-video-lightbox').each(function() { 71 var videoUrl = jQuery(this).attr('data-video'); 72 if (videoUrl) { 73 // Modify link behavior to open video in lightbox 74 jQuery(this).on('click', function(e) { 75 e.preventDefault(); 76 77 // Open lightbox with video 78 if (videoUrl.indexOf('youtube.com') > -1 || videoUrl.indexOf('youtu.be') > -1) { 79 // Youtube video 80 var videoId = getYoutubeId(videoUrl); 81 if (videoId) { 82 // Usar o lightbox do SimpleLightbox 83 var embedUrl = 'https://www.youtube.com/embed/' + videoId + '?autoplay=1&rel=0'; 84 openVideoLightbox(embedUrl); 85 } 86 } else if (videoUrl.indexOf('vimeo.com') > -1) { 87 // Vimeo video 88 var videoId = getVimeoId(videoUrl); 89 if (videoId) { 90 // Usar o lightbox do SimpleLightbox 91 var embedUrl = 'https://player.vimeo.com/video/' + videoId + '?autoplay=1&title=0&byline=0&portrait=0'; 92 openVideoLightbox(embedUrl); 93 } 94 } 95 96 return false; 97 }); 98 } 99 }); 100 101 // Prevent Elementor Lightbox from being triggered for our items 102 jQuery('a.elpt-portfolio-lightbox, a.elpt-portfolio-video-lightbox').each(function() { 103 // Remove any class that might trigger Elementor's lightbox 104 jQuery(this).removeClass('elementor-clickable'); 105 // Add attribute that prevents Elementor from opening its lightbox 106 jQuery(this).attr('data-elementor-open-lightbox', 'n
106o'); 107 108 // Prevent event propagation to avoid Elementor capturing it (except for videos that already have handlers) 109 if (!jQuery(this).hasClass('elpt-portfolio-video-lightbox') || !jQuery(this).attr('data-video')) { 110 jQuery(this).on('click', function(e) { 111 e.stopPropagation(); 112 }); 113 } 114 }); 115 116 // Sanitize again before initializing lightbox 117 sanitizeAllImages(); 118 119 // Initialize lightbox for images with safe configuration 120 var lightboxInstance = jQuery('a.elpt-portfolio-lightbox').simpleLightbox({ 121 captions: true, 122 disableScroll: false, 123 rel: true, 124 // Hook to sanitize captions in real time 125 additionalHtml: false 126 }); 127 128 // Override SimpleLightbox library to use text instead of HTML 129 // Intercepts when captions are added 130 var originalAppendTo = jQuery.fn.appendTo; 131 jQuery.fn.appendTo = function(selector) { 132 var result = originalAppendTo.apply(this, arguments); 133 134 // If it's a caption being added 135 if (this.hasClass && this.hasClass('sl-caption')) { 136 var captionContent = this.html(); 137 if (captionContent) { 138 var decodedContent = safeDecodeHtml(captionContent); 139 var sanitizedContent = sanitizeElptCaption(decodedContent); 140 this.text(sanitizedContent); // Use text() instead of html() 141 } 142 } 143 144 return result; 145 }; 146 147 // Intercept and sanitize captions dynamically 148 // Observe DOM changes to sanitize new captions 149 var observer = new MutationObserver(function(mutations) { 150 mutations.forEach(function(mutation) { 151 if (mutation.addedNodes) { 152 mutation.addedNodes.forEach(function(node) { 153 if (node.nodeType === 1) { // Element node 154 var $node = jQuery(node); 155 if ($node.hasClass('sl-caption') || $node.find('.sl-caption').length > 0) { 156 var $caption = $node.hasClass('sl-caption') ? $node : $node.find('.sl-caption'); 157 var captionHtml = $caption.html(); 158 if (captionHtml) { 159 var decodedCaption = safeDecodeHtml(captionHtml); 160 var sanitizedCaption = sanitizeElptCaption(decodedCaption); 161 $caption.text(sanitizedCaption); // Use .text() instead of .html() 162 } 163 } 164 } 165 }); 166 } 167 }); 168 }); 169 170 // Observe changes in the body 171 observer.observe(document.body, { 172 childList: true, 173 subtree: true 174 }); 175 } 176 177 // Function to get YouTube video ID from URL 178 function getYoutubeId(url) { 179 var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/; 180 var match = url.match(regExp); 181 return (match && match[7].length == 11) ? match[7] : false; 182 } 183 184 // Function to get Vimeo video ID from URL 185 function getVimeoId(url) { 186 var regExp = /^.*(vimeo\.com\/)((channels\/[A-z]+\/)|(groups\/[A-z]+\/videos\/))?([0-9]+)/; 187 var match = url.match(regExp); 188 return match ? match[5] : false; 189 } 190 191 // Function to open lightbox with embedded video 192 function openVideoLightbox(embedUrl) { 193 // Create a dark overlay 194 var overlay = jQuery('<div id="elpt-video-overlay"></div>') 195 .css({ 196 'position': 'fixed', 197 'top': 0, 198 'left': 0, 199 'width': '100%', 200 'height': '100%', 201 'background-color': 'rgba(0,0,0,0.9)', 202 'z-index': 9999, 203 'display': 'flex', 204 'justify-content': 'center', 205 'align-items': 'center' 206 }) 207 .appendTo('body'); 208 209 // Create video container (responsive size computed in sizeVideoContainer) 210 var videoContainer = jQuery('<div id="elpt-video-container"></div>') 211 .css({ 212 'position': 'relative', 213 'box-sizing': 'border-box' 214 }) 215 .appendTo(overlay); 216 217 // Create video iframe 218 var iframe = jQuery('<iframe></iframe>') 219 .attr({ 220 'src': embedUrl, 221 'frameborder': '0', 222 'allowfullscreen': 'true',
223 'allow': 'autoplay; fullscreen' 224 }) 225 .css({ 226 'position': 'absolute', 227 'top': 0, 228 'left': 0, 229 'width': '100%', 230 'height': '100%' 231 }) 232 .appendTo(videoContainer); 233 234 // Size the container to fit the viewport while keeping a 16:9 ratio. 235 // Constrained by both width (90vw, max 900px) and height (90vh) so it 236 // never overflows on mobile or with vertical videos. Recomputed on resize. 237 function sizeVideoContainer() { 238 var maxWidth = Math.min(window.innerWidth * 0.9, 900); 239 var maxHeight = window.innerHeight * 0.9; 240 var width = maxWidth; 241 var height = width * 9 / 16; 242 if (height > maxHeight) { 243 height = maxHeight; 244 width = height * 16 / 9; 245 } 246 videoContainer.css({ 247 'width': Math.round(width) + 'px', 248 'height': Math.round(height) + 'px' 249 }); 250 } 251 sizeVideoContainer(); 252 253 // Create close button, fixed to the viewport so it is always visible/tappable 254 var closeButton = jQuery('<div id="elpt-video-close">Ã</div>') 255 .css({ 256 'position': 'fixed', 257 'top': '12px', 258 'right': '16px', 259 'width': '44px', 260 'height': '44px', 261 'line-height': '44px', 262 'text-align': 'center', 263 'color': 'white', 264 'font-size': '34px', 265 'cursor': 'pointer', 266 'z-index': 10000 267 }) 268 .appendTo(overlay); 269 270 function closeVideoLightbox() { 271 overlay.remove(); 272 jQuery(window).off('resize.elptVideo orientationchange.elptVideo'); 273 jQuery(document).off('keydown.elptVideo'); 274 } 275 276 // Add click event to close lightbox 277 closeButton.on('click', closeVideoLightbox); 278 279 // Close lightbox when clicking outside video 280 overlay.on('click', function(e) { 281 if (e.target === this) { 282 closeVideoLightbox(); 283 } 284 }); 285 286 // Close lightbox when pressing ESC 287 jQuery(document).on('keydown.elptVideo', function(e) { 288 if (e.keyCode === 27) { // ESC key 289 closeVideoLightbox(); 290 } 291 }); 292 293 // Recompute size on viewport changes (resize / device rotation) 294 jQuery(window).on('resize.elptVideo orientationchange.elptVideo', sizeVideoContainer); 295 } 296});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.