PluginProbe
Passster – Password Protect Pages and Content / 4.3.7
Passster – Password Protect Pages and Content v4.3.7
4.3.17 4.3.16 4.3.15 4.3.14 4.3.12 4.3.13 4.3.11 4.3.10 4.3.9 4.3.8 4.3.7 4.3.6 4.3.5 trunk 3.5.4 3.5.5.2 3.5.5.8 3.5.5.9 4.0 4.1.4 4.2.10 4.2.11 4.2.12 4.2.13 4.2.14 All 49 releases
content-protector / assets / public / passster-public.js

passster-public.js in Passster – Password Protect Pages and Content 4.3.7, at assets/public/passster-public.js

349 lines 10.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 jQuery( document ).ready( function( $ ) {
2 const restUrl = ps_ajax.rest_url + 'passster/v1';
3
4 // Get cookie duration.
5 function getDurationBySettings() {
6 switch ( ps_ajax.cookie_duration_unit ) {
7 case 'days':
8 return parseInt( ps_ajax.cookie_duration );
9 case 'hours':
10 return new Date( new Date().getTime() + ( ps_ajax.cookie_duration * 60 ) * 60 * 1000 );
11 case 'minutes':
12 return new Date( new Date().getTime() + ps_ajax.cookie_duration * 60 * 1000 );
13 default:
14 return parseInt( ps_ajax.cookie_duration );
15 }
16 }
17
18 // Get cache busting URL.
19 function getCacheFriendlyURL() {
20 const pts = 'pts=' + Math.floor( Date.now() / 1000 );
21 const base = location.origin + location.pathname;
22 const search = location.search ? location.search + '&' + pts : '?' + pts;
23 const hash = location.hash || '';
24 return base + search + hash;
25 }
26
27 // Hash password via REST API.
28 async function hashPassword( password ) {
29 try {
30 const response = await fetch( restUrl + '/hash', {
31 method: 'POST',
32 credentials: 'same-origin',
33 headers: { 'Content-Type': 'application/json' },
34 body: JSON.stringify( { password } ),
35 } );
36 const data = await response.json();
37 return data.hash || '';
38 } catch ( e ) {
39 return '';
40 }
41 }
42
43 // Get existing hashes from cookie as array.
44 function getExistingHashes() {
45 const existing = Cookies.get( 'passster' );
46 if ( ! existing ) {
47 return [];
48 }
49 // Support both old (single hash) and new (pipe-separated) format
50 return existing.split( '|' ).filter( h => h.length > 0 );
51 }
52
53 // Set hashed cookie (appends to existing hashes).
54 async function setHashedCookie( password ) {
55 const hash = await hashPassword( password );
56 if ( hash ) {
57 const hashes = getExistingHashes();
58 // Only add if not already present
59 if ( ! hashes.includes( hash ) ) {
60 hashes.push( hash );
61 }
62 Cookies.set( 'passster', hashes.join( '|' ), {
63 expires: getDurationBySettings(),
64 sameSite: 'strict',
65 } );
66 }
67 }
68
69 // Set simple cookie (for captcha) - appends to existing.
70 function setCookie( value ) {
71 const hashes = getExistingHashes();
72 if ( ! hashes.includes( value ) ) {
73 hashes.push( value );
74 }
75 Cookies.set( 'passster', hashes.join( '|' ), {
76 expires: getDurationBySettings(),
77 sameSite: 'strict',
78 } );
79 }
80
81 // Handle successful unlock.
82 function handleSuccess( data, $form, redirect, cookieValue ) {
83 if ( ! ps_ajax.disable_cookie && cookieValue ) {
84 setCookie( cookieValue );
85 }
86
87 if ( data.redirect ) {
88 window.location.replace( data.redirect );
89 return;
90 }
91
92 if ( ! ps_ajax.unlock_mode ) {
93 window.location.href = getCacheFriendlyURL();
94 } else {
95 $form.find( '.passster-error' ).hide();
96 if ( data.requires_reload ) {
97 window.location.href = getCacheFriendlyURL();
98 return;
99 }
100 if ( data.content ) {
101 let content = data.content;
102 if ( ps_ajax.shortcodes ) {
103 $.each( ps_ajax.shortcodes, function( key, value ) {
104 content = content.replace( key, value );
105 } );
106 }
107 $form.replaceWith( content );
108 }
109 if ( redirect ) {
110 window.location.replace( redirect );
111 }
112 }
113 }
114
115 // Show error.
116 function showError( $form, message ) {
117 const $error = $form.find( '.passster-error' );
118 $error.text( message || 'An error occurred.' );
119 $error.show().fadeOut( 3500 );
120 }
121
122 // Check if we have an unlock link.
123 if ( ps_ajax.link_pass ) {
124 if ( ! ps_ajax.disable_cookie ) {
125 if ( ps_ajax.link_pass.length < 25 ) {
126 setHashedCookie( atob( ps_ajax.link_pass ) ).then( () => {
127 window.location.replace( ps_ajax.permalink + '?pts=' + Math.floor( Date.now() / 1000 ) );
128 } );
129 } else {
130 setCookie( ps_ajax.link_pass );
131 window.location.replace( ps_ajax.permalink + '?pts=' + Math.floor( Date.now() / 1000 ) );
132 }
133 }
134 }
135
136 // Password form submit - using REST API.
137 $( '.passster-submit' ).on( 'click', async function( e ) {
138 e.preventDefault();
139
140 const $button = $( this );
141 const psId = $button.attr( 'data-psid' );
142 const $form = $( '#' + psId );
143 const $input = $form.find( '.passster-password' );
144 const postId = parseInt( $button.attr( 'data-post-id' ) ) || ps_ajax.post_id;
145
146 // Validate form.
147 const formEl = $form.find( 'form' )[ 0 ];
148 if ( formEl && ! formEl.checkValidity() ) {
149 formEl.reportValidity();
150 return;
151 }
152
153 // Get form data.
154 const password = $input.val();
155 const type = ( $input.attr( 'data-protection-type' ) || 'password' ).replace( /-/g, '_' );
156 const areaId = parseInt( $input.attr( 'data-area' ) ) || 0;
157 const listId = parseInt( $input.attr( 'data-list' ) ) || 0;
158 const lists = $input.attr( 'data-lists' ) || '';
159 const protection = $input.attr( 'data-protection' ) || '';
160 const acf = $button.attr( 'data-acf' ) || '';
161 const redirect = $button.attr( 'data-redirect' ) || '';
162 const termId = parseInt( $button.attr( 'data-term-id' ) ) || 0;
163
164 // Get block ID from parent wrapper if exists.
165 const $wrapper = $form.closest( '.passster-protected-content' );
166 const blockId = $wrapper.length ? $wrapper.attr( 'data-block-id' ) : '';
167
168 $form.find( '.ps-loader' ).css( 'display', 'block' );
169
170 try {
171 const response = await fetch( restUrl + '/unlock', {
172 method: 'POST',
173 credentials: 'same-origin',
174 headers: { 'Content-Type': 'application/json' },
175 body: JSON.stringify( {
176 password,
177 type,
178 post_id: postId,
179 area_id: areaId,
180 block_id: blockId,
181 list_id: listId,
182 lists,
183 protection,
184 acf,
185 redirect,
186 term_id: termId,
187 } ),
188 } );
189
190 const data = await response.json();
191 $form.find( '.ps-loader' ).css( 'display', 'none' );
192 if ( data.success ) {
193 if ( ! ps_ajax.disable_cookie ) {
194 await setHashedCookie( password );
195 }
196 handleSuccess( data, $form, redirect, null );
197 } else {
198 showError( $form, data.error );
199 $input.val( '' );
200 }
201 } catch ( err ) {
202 $form.find( '.ps-loader' ).css( 'display', 'none' );
203 showError( $form, 'An error occurred. Please try again.' );
204 }
205 } );
206
207 // Captcha validation via REST API.
208 async function validateCaptcha( token, type, $form, psId, areaId, redirect, protection ) {
209 try {
210 const response = await fetch( restUrl + '/captcha', {
211 method: 'POST',
212 credentials: 'same-origin',
213 headers: { 'Content-Type': 'application/json' },
214 body: JSON.stringify( {
215 token,
216 type,
217 post_id: ps_ajax.post_id,
218 area_id: areaId,
219 redirect,
220 protection: protection || '',
221 } ),
222 } );
223
224 const data = await response.json();
225
226 if ( data.success ) {
227 handleSuccess( data, $form, redirect, type.replace( '_v2', '' ).replace( '_v3', '' ) );
228 } else {
229 showError( $form, data.error );
230 }
231 } catch ( err ) {
232 showError( $form, 'Captcha validation failed.' );
233 }
234 }
235
236 // Recaptcha v2
237 if ( $( '.recaptcha-form-v2' ).length > 0 && window.grecaptcha ) {
238 grecaptcha.ready( function() {
239 grecaptcha.render( 'ps-recaptcha-v2', {
240 sitekey: ps_ajax.recaptcha_key,
241 callback( token ) {
242 const psId = $( '.recaptcha-v2-submit' ).attr( 'data-psid' );
243 const $form = $( '#' + psId );
244 const $btn = $form.find( '.recaptcha-v2-submit' );
245 const areaId = parseInt( $btn.attr( 'data-area' ) ) || 0;
246 const redirect = $btn.attr( 'data-redirect' ) || '';
247 const protection = $btn.attr( 'data-protection' ) || '';
248
249 validateCaptcha( token, 'recaptcha_v2', $form, psId, areaId, redirect, protection );
250 },
251 } );
252 } );
253 }
254
255 // ReCaptcha v3
256 $( '.recaptcha-form' ).on( 'submit', function( event ) {
257 event.preventDefault();
258
259 const $thisForm = $( this );
260 const $btn = $thisForm.find( '.passster-submit-recaptcha' );
261 const psId = $btn.attr( 'data-psid' );
262 const $form = $( '#' + psId );
263 const areaId = parseInt( $btn.attr( 'data-area' ) ) || 0;
264 const redirect = $btn.attr( 'data-redirect' ) || '';
265 const protection = $btn.attr( 'data-protection' ) || '';
266
267 if ( window.grecaptcha ) {
268 grecaptcha.ready( function() {
269 grecaptcha.execute( ps_ajax.recaptcha_key, { action: 'validate_input' } ).then( function( token ) {
270 validateCaptcha( token, 'recaptcha_v3', $form, psId, areaId, redirect, protection );
271 } );
272 } );
273 }
274 } );
275
276 // hCaptcha
277 $( '.hcaptcha-form' ).on( 'submit', function( event ) {
278 event.preventDefault();
279
280 const $thisForm = $( this );
281 const $btn = $thisForm.find( '.passster-submit-recaptcha' );
282 const psId = $btn.attr( 'data-psid' );
283 const $form = $( '#' + psId );
284 const areaId = parseInt( $btn.attr( 'data-area' ) ) || 0;
285 const redirect = $btn.attr( 'data-redirect' ) || '';
286 const protection = $btn.attr( 'data-protection' ) || '';
287
288 if ( window.hcaptcha ) {
289 hcaptcha.execute( { async: true } )
290 .then( ( { response } ) => {
291 validateCaptcha( response, 'hcaptcha', $form, psId, areaId, redirect, protection );
292 } )
293 .catch( ( err ) => {
294 showError( $form, err.message || 'hCaptcha error' );
295 } );
296 }
297 } );
298
299 // Turnstile
300 let turnstileToken = '';
301 if ( $( '.turnstile-form' ).length > 0 && window.turnstile ) {
302 window.turnstile.ready( function() {
303 window.turnstile.render( '.passster-turnstile', {
304 sitekey: ps_ajax.turnstile_key,
305 callback( token ) {
306 turnstileToken = token;
307 },
308 } );
309 } );
310
311 $( '.turnstile-form' ).on( 'submit', function( event ) {
312 event.preventDefault();
313
314 const $thisForm = $( this );
315 const $btn = $thisForm.find( '.passster-submit-turnstile' );
316 const psId = $btn.attr( 'data-psid' );
317 const $form = $( '#' + psId );
318 const areaId = parseInt( $btn.attr( 'data-area' ) ) || 0;
319 const redirect = $btn.attr( 'data-redirect' ) || '';
320 const protection = $btn.attr( 'data-protection' ) || '';
321
322 if ( ! turnstileToken ) {
323 showError( $form, 'Please complete the verification.' );
324 return;
325 }
326
327 validateCaptcha( turnstileToken, 'turnstile', $form, psId, areaId, redirect, protection );
328 } );
329 }
330
331 // Concurrent logout - using REST API.
332 $( document ).on( 'click', '#ps-logout', async function() {
333 try {
334 const response = await fetch( restUrl + '/logout', {
335 method: 'POST',
336 credentials: 'same-origin',
337 headers: { 'Content-Type': 'application/json' },
338 } );
339 const data = await response.json();
340 if ( data.success ) {
341 Cookies.set( 'passster', '', { expires: 0, sameSite: 'strict' } );
342 window.location.href = getCacheFriendlyURL();
343 }
344 } catch ( e ) {
345 window.location.href = getCacheFriendlyURL();
346 }
347 } );
348 } );
349