1/** 2 * Leaky Paywall Restriction Check 3 * 4 * Optimized replacement for leaky-paywall-cookie.js 5 * Uses REST API instead of admin-ajax.php for better performance. 6 * 7 * @package Leaky Paywall 8 * @since 4.23.0 9 */ 10 11(function () { 12 'use strict'; 13 14 var config = window.leaky_paywall_check_config || {}; 15 var STORAGE_KEY = 'lp_viewed_content'; 16 17 /** 18 * Dispatch a custom event for other plugins to listen to 19 * 20 * @param {string} eventName 21 * @param {Object} detail 22 */ 23 function dispatchPaywallEvent(eventName, detail) { 24 var event = new CustomEvent(eventName, { 25 detail: detail, 26 bubbles: true, 27 cancelable: true 28 }); 29 30 document.dispatchEvent(event); 31 } 32 33 /** 34 * Get post ID from body classes 35 * 36 * @returns {number|null} 37 */ 38 function getPostIdFromBody() { 39 var body = document.body; 40 if (!body) { 41 return null; 42 } 43 44 var classes = body.className.split(/\s+/); 45 var postId = null; 46 47 for (var i = 0; i < classes.length; i++) { 48 var cls = classes[i]; 49 50 // Match postid-123 pattern 51 if (cls.indexOf('postid-') === 0) { 52 postId = parseInt(cls.substring(7), 10); 53 if (postId > 0) { 54 return postId; 55 } 56 } 57 58 // Match page-id-123 pattern 59 if (cls.indexOf('page-id-') === 0) { 60 postId = parseInt(cls.substring(8), 10); 61 if (postId > 0) { 62 return postId; 63 } 64 } 65 } 66 67 return null; 68 } 69 70 /** 71 * Get viewed content from localStorage 72 * 73 * @returns {Object} 74 */ 75 function getViewedContent() { 76 try { 77 var stored = localStorage.getItem(STORAGE_KEY); 78 if (stored) { 79 return JSON.parse(stored); 80 } 81 } catch (e) { 82 // localStorage not available or invalid JSON 83 } 84 return {}; 85 } 86 87 /** 88 * Save viewed content to localStorage 89 * 90 * @param {Object} content 91 */ 92 function saveViewedContent(content) { 93 try { 94 localStorage.setItem(STORAGE_KEY, JSON.stringify(content)); 95 } catch (e) { 96 // localStorage not available 97 } 98 } 99 100 /** 101 * Update viewed content with new post 102 * 103 * @param {number} postId 104 * @param {string} postType 105 * @param {number} expiration 106 */ 107 function updateViewedContent(postId, postType, expiration) { 108 var content = getViewedContent(); 109 110 if (!content[postType]) { 111 content[postType] = {}; 112 } 113 114 content[postType][postId] = expiration; 115 saveViewedContent(content); 116 } 117 118 /** 119 * Clean expired entries from viewed content 120 */ 121 function cleanExpiredContent() { 122 var content = getViewedContent(); 123 var now = Math.floor(Date.now() / 1000); 124 var hasChanges = false; 125 126 for (var postType in content) { 127 if (content.hasOwnProperty(postType)) { 128 for (var postId in content[postType]) { 129 if (content[postType].hasOwnProperty(postId)) { 130 if (content[postType][postId] < now) { 131 delete content[postType][postId]; 132 hasChanges = true; 133 } 134 } 135 } 136 137 // Remove empty post type objects 138 if (Object.keys(content[postType]).length === 0) { 139 delete content[postType]; 140 hasChanges = true; 141 } 142 } 143 } 144 145 if (hasChanges) { 146 saveViewedContent(content); 147 } 148 149 return content; 150 } 151 152 /** 153 * Get content containers based on post type 154 * 155 * @param {number} postId 156 * @returns {NodeList} 157 */ 158 function getContentContainers(postId) { 159 var body = document.body; 160 var containerSetting; 161 162 // Check if this is a page or post 163 if (body.classList.contains('page-id-' + postId)) { 164 containerSetting = config.page_container || ''; 165 } else { 166 containerSetting = config.post_container || ''; 167 } 168 169 if (!containerSetting) { 170 return []; 171 } 172 173 // Handle comma-separated selectors 174 var selectors = containerSetting.split(',').map(function (s) { 175 return s.trim(); 176 }).filter(function (s) { 177 return s.length > 0; 178 }); 179 180 var containers = []; 181 selectors.forEach(function (selector) { 182 var elements = document.querySelectorAll(selector); 183 for (var i = 0; i < elements.length; i++) { 184 containers.push(elements[i]); 185 } 186 }); 187 188 return containers; 189 } 190 191 /** 192 * Extract lead-in content from container 193 * 194 * @param {Element} container 195 * @param {number} numElements 196 * @returns {string} 197 */ 198 function getLeadInContent(container, numElements) { 199 if (!numElements || numElements <= 0) { 200 return ''; 201 } 202 203 var children = container.children; 204 var leadIn = ''; 205 206 for (var i = 0; i < Math.min(numElements, children.length); i++) { 207 leadIn += children[i].outerHTML; 208 } 209 210 return leadIn; 211 } 212 213 /** 214 * Display the paywall content 215 * 216 * @param {string} nagContent 217 * @param {number} postId 218 */ 219 function displayPaywall(nagContent, postId) { 220 var containers = getContentContainers(postId); 221 var leadInElements = parseInt(config.lead_in_elements, 10) || 0; 222
223 containers.forEach(function (container, index) { 224 var leadIn = getLeadInContent(container, leadInElements); 225 226 if (containers.length > 1 && index > 0) { 227 // For multiple containers, clear all but the first 228 container.innerHTML = ''; 229 } else { 230 container.innerHTML = leadIn + nagContent; 231 } 232 container.style.visibility = 'visible'; 233 }); 234 } 235 236 /** 237 * Show the content (no paywall needed) 238 * 239 * @param {number} postId 240 */ 241 function showContent(postId) { 242 var containers = getContentContainers(postId); 243 244 containers.forEach(function (container) { 245 container.style.visibility = 'visible'; 246 }); 247 } 248 249 /** 250 * Check restrictions via REST API 251 * 252 * @param {number} postId 253 */ 254 function checkRestrictions(postId) { 255 // Clean expired content first 256 var viewedContent = cleanExpiredContent(); 257 258 var restUrl = config.rest_url || '/wp-json/leaky-paywall/v1/check-restrictions'; 259 260 // Build request 261 var requestBody = { 262 post_id: postId, 263 viewed_content: viewedContent 264 }; 265 266 var headers = { 267 'Content-Type': 'application/json' 268 }; 269 270 // Only send the nonce for logged-in users. For anonymous visitors, 271 // omitting the header avoids 403 errors when a page cache serves a stale nonce. 272 if ( config.nonce && document.body.classList.contains('logged-in') ) { 273 headers['X-WP-Nonce'] = config.nonce; 274 } 275 276 fetch(restUrl, { 277 method: 'POST', 278 headers: headers, 279 body: JSON.stringify(requestBody), 280 credentials: 'same-origin' 281 }) 282 .then(function (response) { 283 if (!response.ok) { 284 throw new Error('Network response was not ok'); 285 } 286 return response.json(); 287 }) 288 .then(function (data) { 289 if (data.show_paywall && data.nag_content) { 290 // Record the post where the paywall was shown so a later signup is 291 // attributed to it even if the visitor didn't click a link in the nag. 292 if (postId > 0) { 293 document.cookie = 'lp_nag_loc=' + postId + ';path=/'; 294 } 295 296 // If List Builder is active and handles this nag type, 297 // skip the in-content nag â the slider will handle display via the event. 298 var lb = window.LP_LIST_BUILDER; 299 var lbHandlesNag = lb && ( 300 data.nag_type === 'subscribe' || 301 (data.nag_type === 'upgrade' && lb.upgradeEnabled) 302 ); 303 304 if (lbHandlesNag) { 305 showContent(postId); 306 } else { 307 displayPaywall(data.nag_content, postId); 308 } 309 310 // Dispatch event for other plugins to hook into 311 dispatchPaywallEvent('leaky_paywall_shown', { 312 postId: postId, 313 response: data 314 }); 315 } else { 316 showContent(postId); 317 318 // Only update viewed content when access is granted. 319 if (data.viewed_content) { 320 saveViewedContent(data.viewed_content); 321 } 322 323 // Dispatch event for content access 324 dispatchPaywallEvent('leaky_paywall_access_granted', { 325 postId: postId, 326 response: data 327 }); 328 } 329 }) 330 .catch(function (error) { 331 // On error, show content to avoid blocking users 332 console.error('Leaky Paywall check failed:', error); 333 showContent(postId); 334 }); 335 } 336 337 /** 338 * Initialize 339 */ 340 function init() { 341 var postId = getPostIdFromBody(); 342 343 if (!postId) { 344 return; 345 } 346 347 checkRestrictions(postId); 348 } 349 350 // Run on DOM ready 351 if (document.readyState === 'loading') { 352 document.addEventListener('DOMContentLoaded', init); 353 } else { 354 init(); 355 } 356 357 // Expose for external use if needed 358 window.LeakyPaywallCheck = { 359 checkRestrictions: checkRestrictions, 360 getViewedContent: getViewedContent, 361 updateViewedContent: updateViewedContent, 362 cleanExpiredContent: cleanExpiredContent 363 }; 364 365})();
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.