| @@ -20,8 +20,35 @@ | ||
| 20 | 20 | const widgetMode = $wrapper.data('widget-mode'); |
| 21 | 21 | const isPro = widgetMode === 'custom_cpt'; |
| 22 | 22 | |
| 23 | 23 | const gridHandler = { |
| 24 | + // Sanitize HTML using DOMPurify for XSS protection | |
| 25 | + sanitizeHtml(value) { | |
| 26 | + if (typeof DOMPurify !== 'undefined') { | |
| 27 | + return DOMPurify.sanitize(String(value), {ALLOWED_TAGS: [], ALLOWED_ATTR: []}); | |
| 28 | + } | |
| 29 | + // Fallback | |
| 30 | + return String(value) | |
| 31 | + .replace(/&/g, "&") | |
| 32 | + .replace(/</g, "<") | |
| 33 | + .replace(/>/g, ">") | |
| 34 | + .replace(/"/g, """) | |
| 35 | + .replace(/'/g, "'"); | |
| 36 | + }, | |
| 37 | + | |
| 38 | + normalizeSafeUrl(rawUrl, allowedProtocols = ["http:", "https:"]) { | |
| 39 | + if (!rawUrl || typeof rawUrl !== "string") return null; | |
| 40 | + try { | |
| 41 | + const url = new URL(rawUrl, window.location.href); | |
| 42 | + if (allowedProtocols.includes(url.protocol)) { | |
| 43 | + return url.href; | |
| 44 | + } | |
| 45 | + } catch (e) { | |
| 46 | + // ignore | |
| 47 | + } | |
| 48 | + return null; | |
| 49 | + }, | |
| 50 | + | |
| 24 | 51 | init() { |
| 25 | 52 | this.wrapper = $scope.find('.king-addons-dpg-wrapper'); |
| 26 | 53 | this.grid = $scope.find('.king-addons-dpg-grid'); |
| 27 | 54 | this.filterBar = $scope.find('.king-addons-dpg-filter-bar'); |
| @@ -123,10 +150,12 @@ | ||
| 123 | 150 | const postLink = $(e.currentTarget).find('.king-addons-dpg-title a'); |
| 124 | 151 | if (postLink.length > 0) { |
| 125 | 152 | const postUrl = postLink.attr('href'); |
| 126 | 153 | if (postUrl) { |
| 127 | - // Navigate to post | |
| 128 | - window.location.href = postUrl; | |
| 154 | + const safePostUrl = this.normalizeSafeUrl(postUrl); | |
| 155 | + if (safePostUrl) { | |
| 156 | + window.location.href = safePostUrl; | |
| 157 | + } | |
| 129 | 158 | } |
| 130 | 159 | } |
| 131 | 160 | }); |
| 132 | 161 | |
| @@ -163,9 +192,9 @@ | ||
| 163 | 192 | const url = $button.data('url'); |
| 164 | 193 | const title = $button.data('title') || ''; |
| 165 | 194 | |
| 166 | 195 | if (!url) { |
| 167 | - console.warn('No URL provided for action button'); | |
| 196 | + // console.warn('No URL provided for action button'); | |
| 168 | 197 | return; |
| 169 | 198 | } |
| 170 | 199 | |
| 171 | 200 | switch (action) { |
| @@ -183,8 +212,13 @@ | ||
| 183 | 212 | }); |
| 184 | 213 | }, |
| 185 | 214 | |
| 186 | 215 | openImageLightbox(url, title) { |
| 216 | + const safeUrl = this.normalizeSafeUrl(url); | |
| 217 | + if (!safeUrl) { | |
| 218 | + return; | |
| 219 | + } | |
| 220 | + | |
| 187 | 221 | // Prevent multiple lightboxes from opening simultaneously |
| 188 | 222 | if ($('.lg-backdrop, .lg-outer, .king-addons-dpg-lightbox-temp, [data-lg-uid]').length > 0) { |
| 189 | 223 | return; |
| 190 | 224 | } |
| @@ -198,10 +232,10 @@ | ||
| 198 | 232 | $('.king-addons-dpg-lightbox-temp').remove(); |
| 199 | 233 | |
| 200 | 234 | // Create array with single image item for LightGallery |
| 201 | 235 | const galleryItems = [{ |
| 202 | - src: url, | |
| 203 | - subHtml: title || '' | |
| 236 | + src: safeUrl, | |
| 237 | + subHtml: title ? '<div>' + this.sanitizeHtml(title) + '</div>' : '' | |
| 204 | 238 | }]; |
| 205 | 239 | |
| 206 | 240 | // Initialize LightGallery directly with dynamic gallery |
| 207 | 241 | if (typeof $.fn.lightGallery !== 'undefined') { |
| @@ -230,10 +264,10 @@ | ||
| 230 | 264 | $tempDiv.remove(); |
| 231 | 265 | }, 100); |
| 232 | 266 | }); |
| 233 | 267 | } else { |
| 234 | - console.error('LightGallery not available'); | |
| 235 | - window.open(url, '_blank'); | |
| 268 | + // console.error('LightGallery not available'); | |
| 269 | + window.open(safeUrl, '_blank'); | |
| 236 | 270 | } |
| 237 | 271 | }, |
| 238 | 272 | |
| 239 | 273 | openVideoLightbox(url, title) { |
| @@ -238,10 +272,13 @@ | ||
| 238 | 272 | |
| 239 | 273 | openVideoLightbox(url, title) { |
| 240 | 274 | // Check if LightGallery is available |
| 241 | 275 | if (typeof $.fn.lightGallery === 'undefined') { |
| 242 | - console.warn('LightGallery not loaded, opening video in new tab'); | |
| 243 | - window.open(url, '_blank'); | |
| 276 | + // console.warn('LightGallery not loaded, opening video in new tab'); | |
| 277 | + const safeUrl = this.normalizeSafeUrl(url); | |
| 278 | + if (safeUrl) { | |
| 279 | + window.open(safeUrl, '_blank'); | |
| 280 | + } | |
| 244 | 281 | return; |
| 245 | 282 | } |
| 246 | 283 | |
| 247 | 284 | // For old LightGallery v1.6.12, let's create a manual video popup |
| @@ -248,13 +285,18 @@ | ||
| 248 | 285 | this.createYouTubePopup(url, title); |
| 249 | 286 | }, |
| 250 | 287 | |
| 251 | 288 | createYouTubePopup(url, title) { |
| 289 | + const safeUrl = this.normalizeSafeUrl(url); | |
| 290 | + if (!safeUrl) { | |
| 291 | + return; | |
| 292 | + } | |
| 293 | + | |
| 252 | 294 | // Process YouTube URL to get video ID |
| 253 | - const videoId = this.getYouTubeVideoId(url); | |
| 295 | + const videoId = this.getYouTubeVideoId(safeUrl); | |
| 254 | 296 | if (!videoId) { |
| 255 | - console.warn('Invalid YouTube URL'); | |
| 256 | - window.open(url, '_blank'); | |
| 297 | + // console.warn('Invalid YouTube URL'); | |
| 298 | + window.open(safeUrl, '_blank'); | |
| 257 | 299 | return; |
| 258 | 300 | } |
| 259 | 301 | |
| 260 | 302 | // Create manual video popup similar to LightGallery structure |
| @@ -416,9 +458,9 @@ | ||
| 416 | 458 | success: (response) => { |
| 417 | 459 | this.handleFilterResponse(response); |
| 418 | 460 | }, |
| 419 | 461 | error: (xhr, status, error) => { |
| 420 | - console.error('Dynamic Posts Grid AJAX Error:', error); | |
| 462 | + // console.error('Dynamic Posts Grid AJAX Error:', error); | |
| 421 | 463 | this.hideLoading(); |
| 422 | 464 | this.isLoading = false; |
| 423 | 465 | } |
| 424 | 466 | }); |
| @@ -459,9 +501,9 @@ | ||
| 459 | 501 | success: (response) => { |
| 460 | 502 | this.handleLoadMoreResponse(response); |
| 461 | 503 | }, |
| 462 | 504 | error: (xhr, status, error) => { |
| 463 | - console.error('Dynamic Posts Grid Load More Error:', error); | |
| 505 | + // console.error('Dynamic Posts Grid Load More Error:', error); | |
| 464 | 506 | this.hideLoading(); |
| 465 | 507 | this.isLoading = false; |
| 466 | 508 | this.currentPage--; // Revert page increment on error |
| 467 | 509 | } |
| @@ -559,11 +601,20 @@ | ||
| 559 | 601 | if (!conf) return; |
| 560 | 602 | |
| 561 | 603 | const $iconWrap = $card.find('.king-addons-dpg-icon'); |
| 562 | 604 | if (conf.icon_type === 'image' && conf.image_url) { |
| 563 | - $iconWrap.html('<img src="' + conf.image_url + '" alt="' + postType + '" />'); | |
| 605 | + const safeImgUrl = this.normalizeSafeUrl(conf.image_url); | |
| 606 | + if (!safeImgUrl) return; | |
| 607 | + $iconWrap.empty().append( | |
| 608 | + $("<img />", { | |
| 609 | + src: safeImgUrl, | |
| 610 | + alt: String(postType || ""), | |
| 611 | + }) | |
| 612 | + ); | |
| 564 | 613 | } else if (conf.icon_class) { |
| 565 | - $iconWrap.html('<i class="' + conf.icon_class + '"></i>'); | |
| 614 | + $iconWrap.empty().append( | |
| 615 | + $("<i />").addClass(String(conf.icon_class || "")) | |
| 616 | + ); | |
| 566 | 617 | } |
| 567 | 618 | }); |
| 568 | 619 | }, |
| 569 | 620 | |