PluginProbe
Passster – Password Protect Pages and Content / 4.3.16
Passster – Password Protect Pages and Content v4.3.16
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 4.2.15 All 48 releases
content-protector / assets / public / passster-public.js

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

344 lines 10.1 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, postId ) {
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, post_id: postId } ),
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, postId ) {
55 const hash = await hashPassword( password, postId );
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 ) {
83 if ( data.redirect ) {
84 window.location.replace( data.redirect );
85 return;
86 }
87
88 if ( ! ps_ajax.unlock_mode ) {
89 window.location.href = getCacheFriendlyURL();
90 } else {
91 $form.find( '.passster-error' ).hide();
92 if ( data.requires_reload ) {
93 window.location.href = getCacheFriendlyURL();
94 return;
95 }
96 if ( data.content ) {
97 let content = data.content;
98 if ( ps_ajax.shortcodes ) {
99 $.each( ps_ajax.shortcodes, function( key, value ) {
100 content = content.replace( key, value );
101 } );
102 }
103 $form.replaceWith( content );
104 }
105 if ( redirect ) {
106 window.location.replace( redirect );
107 }
108 }
109 }
110
111 // Show error.
112 function showError( $form, message ) {
113 const $error = $form.find( '.passster-error' );
114 $error.text( message || 'An error occurred.' );
115 $error.show().fadeOut( 3500 );
116 }
117
118 // Check if we have an unlock link.
119 if ( ps_ajax.link_pass ) {
120 if ( ! ps_ajax.disable_cookie ) {
121 if ( ps_ajax.link_pass.length < 25 ) {
122 setHashedCookie( atob( ps_ajax.link_pass ), ps_ajax.post_id ).then( () => {
123 window.location.replace( ps_ajax.permalink + '?pts=' + Math.floor( Date.now() / 1000 ) );
124 } );
125 } else {
126 setCookie( ps_ajax.link_pass );
127 window.location.replace( ps_ajax.permalink + '?pts=' + Math.floor( Date.now() / 1000 ) );
128 }
129 }
130 }
131
132 // Password form submit - using REST API.
133 $( '.passster-submit' ).on( 'click', async function( e ) {
134 e.preventDefault();
135
136 const $button = $( this );
137 const psId = $button.attr( 'data-psid' );
138 const $form = $( '#' + psId );
139 const $input = $form.find( '.passster-password' );
140 const postId = parseInt( $button.attr( 'data-post-id' ) ) || ps_ajax.post_id;
141
142 // Validate form.
143 const formEl = $form.find( 'form' )[ 0 ];
144 if ( formEl && ! formEl.checkValidity() ) {
145 formEl.reportValidity();
146 return;
147 }
148
149 // Get form data.
150 const password = $input.val();
151 const type = ( $input.attr( 'data-protection-type' ) || 'password' ).replace( /-/g, '_' );
152 const areaId = parseInt( $input.attr( 'data-area' ) ) || 0;
153 const listId = parseInt( $input.attr( 'data-list' ) ) || 0;
154 const lists = $input.attr( 'data-lists' ) || '';
155 const protection = $input.attr( 'data-protection' ) || '';
156 const acf = $button.attr( 'data-acf' ) || '';
157 const redirect = $button.attr( 'data-redirect' ) || '';
158 const termId = parseInt( $button.attr( 'data-term-id' ) ) || 0;
159 const postType = $button.attr( 'data-post-type' ) || '';
160
161 // Get block ID from parent wrapper if exists.
162 const $wrapper = $form.closest( '.passster-protected-content' );
163 const blockId = $wrapper.length ? $wrapper.attr( 'data-block-id' ) : '';
164
165 $form.find( '.ps-loader' ).css( 'display', 'block' );
166
167 try {
168 const response = await fetch( restUrl + '/unlock', {
169 method: 'POST',
170 credentials: 'same-origin',
171 headers: { 'Content-Type': 'application/json' },
172 body: JSON.stringify( {
173 password,
174 type,
175 post_id: postId,
176 area_id: areaId,
177 block_id: blockId,
178 list_id: listId,
179 lists,
180 protection,
181 acf,
182 redirect,
183 term_id: termId,
184 post_type: postType,
185 } ),
186 } );
187
188 const data = await response.json();
189 $form.find( '.ps-loader' ).css( 'display', 'none' );
190 if ( data.success ) {
191 handleSuccess( data, $form, redirect );
192 } else {
193 showError( $form, data.error );
194 $input.val( '' );
195 }
196 } catch ( err ) {
197 $form.find( '.ps-loader' ).css( 'display', 'none' );
198 showError( $form, 'An error occurred. Please try again.' );
199 }
200 } );
201
202 // Captcha validation via REST API.
203 async function validateCaptcha( token, type, $form, psId, areaId, redirect, protection ) {
204 try {
205 const response = await fetch( restUrl + '/captcha', {
206 method: 'POST',
207 credentials: 'same-origin',
208 headers: { 'Content-Type': 'application/json' },
209 body: JSON.stringify( {
210 token,
211 type,
212 post_id: ps_ajax.post_id,
213 area_id: areaId,
214 redirect,
215 protection: protection || '',
216 } ),
217 } );
218
219 const data = await response.json();
220
221 if ( data.success ) {
222 handleSuccess( data, $form, redirect );
223 } else {
224 showError( $form, data.error );
225 }
226 } catch ( err ) {
227 showError( $form, 'Captcha validation failed.' );
228 }
229 }
230
231 // Recaptcha v2
232 if ( $( '.recaptcha-form-v2' ).length > 0 && window.grecaptcha ) {
233 grecaptcha.ready( function() {
234 grecaptcha.render( 'ps-recaptcha-v2', {
235 sitekey: ps_ajax.recaptcha_key,
236 callback( token ) {
237 const psId = $( '.recaptcha-v2-submit' ).attr( 'data-psid' );
238 const $form = $( '#' + psId );
239 const $btn = $form.find( '.recaptcha-v2-submit' );
240 const areaId = parseInt( $btn.attr( 'data-area' ) ) || 0;
241 const redirect = $btn.attr( 'data-redirect' ) || '';
242 const protection = $btn.attr( 'data-protection' ) || '';
243
244 validateCaptcha( token, 'recaptcha_v2', $form, psId, areaId, redirect, protection );
245 },
246 } );
247 } );
248 }
249
250 // ReCaptcha v3
251 $( '.recaptcha-form' ).on( 'submit', function( event ) {
252 event.preventDefault();
253
254 const $thisForm = $( this );
255 const $btn = $thisForm.find( '.passster-submit-recaptcha' );
256 const psId = $btn.attr( 'data-psid' );
257 const $form = $( '#' + psId );
258 const areaId = parseInt( $btn.attr( 'data-area' ) ) || 0;
259 const redirect = $btn.attr( 'data-redirect' ) || '';
260 const protection = $btn.attr( 'data-protection' ) || '';
261
262 if ( window.grecaptcha ) {
263 grecaptcha.ready( function() {
264 grecaptcha.execute( ps_ajax.recaptcha_key, { action: 'validate_input' } ).then( function( token ) {
265 validateCaptcha( token, 'recaptcha_v3', $form, psId, areaId, redirect, protection );
266 } );
267 } );
268 }
269 } );
270
271 // hCaptcha
272 $( '.hcaptcha-form' ).on( 'submit', function( event ) {
273 event.preventDefault();
274
275 const $thisForm = $( this );
276 const $btn = $thisForm.find( '.passster-submit-recaptcha' );
277 const psId = $btn.attr( 'data-psid' );
278 const $form = $( '#' + psId );
279 const areaId = parseInt( $btn.attr( 'data-area' ) ) || 0;
280 const redirect = $btn.attr( 'data-redirect' ) || '';
281 const protection = $btn.attr( 'data-protection' ) || '';
282
283 if ( window.hcaptcha ) {
284 hcaptcha.execute( { async: true } )
285 .then( ( { response } ) => {
286 validateCaptcha( response, 'hcaptcha', $form, psId, areaId, redirect, protection );
287 } )
288 .catch( ( err ) => {
289 showError( $form, err.message || 'hCaptcha error' );
290 } );
291 }
292 } );
293
294 // Turnstile
295 let turnstileToken = '';
296 if ( $( '.turnstile-form' ).length > 0 && window.turnstile ) {
297 window.turnstile.ready( function() {
298 window.turnstile.render( '.passster-turnstile', {
299 sitekey: ps_ajax.turnstile_key,
300 callback( token ) {
301 turnstileToken = token;
302 },
303 } );
304 } );
305
306 $( '.turnstile-form' ).on( 'submit', function( event ) {
307 event.preventDefault();
308
309 const $thisForm = $( this );
310 const $btn = $thisForm.find( '.passster-submit-turnstile' );
311 const psId = $btn.attr( 'data-psid' );
312 const $form = $( '#' + psId );
313 const areaId = parseInt( $btn.attr( 'data-area' ) ) || 0;
314 const redirect = $btn.attr( 'data-redirect' ) || '';
315 const protection = $btn.attr( 'data-protection' ) || '';
316
317 if ( ! turnstileToken ) {
318 showError( $form, 'Please complete the verification.' );
319 return;
320 }
321
322 validateCaptcha( turnstileToken, 'turnstile', $form, psId, areaId, redirect, protection );
323 } );
324 }
325
326 // Concurrent logout - using REST API.
327 $( document ).on( 'click', '#ps-logout', async function() {
328 try {
329 const response = await fetch( restUrl + '/logout', {
330 method: 'POST',
331 credentials: 'same-origin',
332 headers: { 'Content-Type': 'application/json' },
333 } );
334 const data = await response.json();
335 if ( data.success ) {
336 Cookies.set( 'passster', '', { expires: 0, sameSite: 'strict' } );
337 window.location.href = getCacheFriendlyURL();
338 }
339 } catch ( e ) {
340 window.location.href = getCacheFriendlyURL();
341 }
342 } );
343 } );
344