PluginProbe
Passster – Password Protect Pages and Content / 4.3.17
Passster – Password Protect Pages and Content v4.3.17
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
← All changes | assets/public/passster-public.js +308 -269 4.0 → 4.3.17 View file →
@@ -1,304 +1,343 @@
1 -jQuery(document).ready(function ($) {
1 +jQuery( document ).ready( function( $ ) {
2 + const restUrl = ps_ajax.rest_url + 'passster/v1';
2 3
3 - // Get cookie duration.
4 - function getDurationBySettings() {
5 - switch (ps_ajax.cookie_duration_unit) {
6 - case 'days':
7 - return parseInt(ps_ajax.cookie_duration);
8 - break;
9 - case 'hours':
10 - return new Date(new Date().getTime() + (ps_ajax.cookie_duration * 10) * 60 * 1000);
11 - break;
12 - case 'minutes':
13 - return new Date(new Date().getTime() + ps_ajax.cookie_duration * 60 * 1000);
14 - break;
15 - default:
16 - return parseInt(ps_ajax.cookie_duration);
17 - }
18 - }
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 + }
19 17
20 - // Get cache busting URL.
21 - function getCacheFriendlyURL() {
22 - return (location.origin).concat(location.pathname).concat(location.hash) + '?pts=' + Math.floor(Date.now() / 1000);
23 - }
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 + }
24 26
25 - // Get hashed cookie via Ajax.
26 - function setHashedCookie(password) {
27 - $.ajax({
28 - type: "post", dataType: "json", url: ps_ajax.ajax_url, data: {
29 - 'action': 'hash_password', 'hash_nonce': ps_ajax.hash_nonce, 'password': password,
30 - }, success: function (response) {
31 - Cookies.set('passster', response.password, {
32 - expires: getDurationBySettings(), sameSite: 'strict'
33 - });
34 - }, async: false,
35 - });
36 - }
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 + }
37 42
38 - // Check if we have an unlock link.
39 - if (ps_ajax.link_pass) {
40 - if (!ps_ajax.disable_cookie) {
41 - if (ps_ajax.link_pass.length < 25) {
42 - // It's an old base64 encryption.
43 - setHashedCookie(atob(ps_ajax.link_pass));
44 - } else {
45 - Cookies.set('passster', ps_ajax.link_pass, {
46 - expires: getDurationBySettings(), sameSite: 'strict'
47 - });
48 - }
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 + }
49 52
50 - window.location.replace(ps_ajax.permalink);
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 + }
53 68
54 - // Passwords
55 - $('.passster-submit').on('click', function (e) {
56 - e.preventDefault();
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 + }
57 80
58 - // Validate form before submitting ajax.
59 - var form = $(this).parent().parent();
81 + // Handle successful unlock.
82 + function handleSuccess( data, $form, redirect ) {
83 + if ( data.redirect ) {
84 + window.location.replace( data.redirect );
85 + return;
86 + }
60 87
61 - if (!$(form)[0].checkValidity()) {
62 - $(form)[0].reportValidity();
63 - }
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 + }
64 110
65 - ps_id = $(this).attr('data-psid');
66 - form = $("#" + ps_id);
67 - password = $("#" + ps_id + ' .passster-password').attr('data-password');
68 - type = $("#" + ps_id + ' .passster-password').attr('data-protection-type');
69 - list = $("#" + ps_id + ' .passster-password').attr('data-list');
70 - area = $("#" + ps_id + ' .passster-password').attr('data-area');
71 - protection = $("#" + ps_id + ' .passster-password').attr('data-protection');
72 - redirect = $(this).attr('data-redirect');
73 - input = $("#" + ps_id + ' .passster-password').val();
74 - acf = $(this).attr('data-acf');
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 + }
75 117
76 - $.ajax({
77 - type: "post", dataType: "json", url: ps_ajax.ajax_url, data: {
78 - 'action': 'validate_input',
79 - 'nonce': ps_ajax.nonce,
80 - 'input': input,
81 - 'password': password,
82 - 'post_id': ps_ajax.post_id,
83 - 'type': type,
84 - 'list': list,
85 - 'area': area,
86 - 'protection': protection,
87 - 'acf': acf,
88 - 'redirect': redirect
89 - }, beforeSend: function () {
90 - form.find(".ps-loader").css('display', 'block');
91 - }, success: function (response) {
92 - form.find(".ps-loader").css('display', 'none');
93 - if (true === response.success) {
94 - // if no ajax.
95 - if (!ps_ajax.unlock_mode) {
96 - setHashedCookie(input);
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 + }
97 131
98 - if (response.redirect) {
99 - window.location.replace(redirect);
100 - } else {
101 - window.location.href = getCacheFriendlyURL();
102 - }
103 - } else {
132 + // Password form submit - using REST API.
133 + $( '.passster-submit' ).on( 'click', async function( e ) {
134 + e.preventDefault();
104 135
105 - // set cookie if activated.
106 - if (!ps_ajax.disable_cookie) {
107 - setHashedCookie(input);
108 - }
109 - form.find('.passster-error').hide();
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;
110 141
111 - // replace shortcodes.
112 - let content = response.content;
142 + // Validate form.
143 + const formEl = $form.find( 'form' )[ 0 ];
144 + if ( formEl && ! formEl.checkValidity() ) {
145 + formEl.reportValidity();
146 + return;
147 + }
113 148
114 - if (content) {
115 - $.each(ps_ajax.shortcodes, function (key, value) {
116 - content = content.replace(key, value);
117 - });
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' ) || '';
118 160
119 - $("#" + ps_id).replaceWith(content);
120 - }
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' ) : '';
121 164
122 - // Redirect?
123 - if (response.redirect) {
124 - window.location.replace(redirect);
125 - }
165 + $form.find( '.ps-loader' ).css( 'display', 'block' );
126 166
127 - }
128 - } else {
129 - form.find('.passster-error').text(response.error);
130 - form.find('.passster-error').show().fadeOut(3500);
131 - $("#" + ps_id + ' .passster-password').val('');
132 - }
133 - }
134 - });
135 - });
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 + } );
136 187
137 - // Recaptcha v2
138 - if ($('.recaptcha-form-v2').length > 0) {
139 - grecaptcha.ready(function () {
140 - grecaptcha.render('ps-recaptcha-v2', {
141 - 'sitekey': ps_ajax.recaptcha_key, 'callback': function (token) {
142 - ps_id = $('.recaptcha-v2-submit').attr('data-psid');
143 - form = $("#" + ps_id);
144 - protection = $('.recaptcha-v2-submit').attr('data-protection');
145 - acf = $('.recaptcha-v2-submit').attr('data-acf');
146 - area = $("#" + ps_id).find('.recaptcha-v2-submit').attr('data-area');
147 - redirect = $("#" + ps_id).find('.recaptcha-v2-submit').attr('data-redirect');
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 + } );
148 201
149 - $.ajax({
150 - type: "post", dataType: "json", url: ps_ajax.ajax_url, data: {
151 - 'action': 'validate_input',
152 - 'nonce': ps_ajax.nonce,
153 - 'token': token,
154 - 'post_id': ps_ajax.post_id,
155 - 'type': 'recaptcha',
156 - 'protection': protection,
157 - 'captcha_id': ps_id,
158 - 'acf': acf,
159 - 'area': area,
160 - 'redirect': redirect
161 - }, success: function (response) {
162 - if (true === response.success) {
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 + } );
163 218
164 - // if no ajax.
165 - if (!ps_ajax.unlock_mode) {
166 - Cookies.set('passster', 'recaptcha', {
167 - expires: getDurationBySettings(), sameSite: 'strict'
168 - });
219 + const data = await response.json();
169 220
170 - if (response.redirect) {
171 - window.location.replace(redirect);
172 - } else {
173 - window.location.href = getCacheFriendlyURL();
174 - }
175 - } else {
176 - // set cookie if activated.
177 - if (!ps_ajax.disable_cookie) {
178 - Cookies.set('passster', 'recaptcha', {
179 - expires: getDurationBySettings(), sameSite: 'strict'
180 - });
181 - }
182 - form.find('.passster-error').hide();
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 + }
183 230
184 - // replace shortcodes.
185 - let content = response.content;
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' ) || '';
186 243
187 - if (content) {
244 + validateCaptcha( token, 'recaptcha_v2', $form, psId, areaId, redirect, protection );
245 + },
246 + } );
247 + } );
248 + }
188 249
189 - $.each(ps_ajax.shortcodes, function (key, value) {
190 - content = content.replace(key, value);
191 - });
250 + // ReCaptcha v3
251 + $( '.recaptcha-form' ).on( 'submit', function( event ) {
252 + event.preventDefault();
192 253
193 - $("#" + ps_id).replaceWith(content);
194 - }
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' ) || '';
195 261
196 - // Redirect?
197 - if (response.redirect) {
198 - window.location.replace(redirect);
199 - }
200 - }
201 - } else {
202 - form.find('.passster-error').text(response.error);
203 - form.find('.passster-error').show().fadeOut(3500);
204 - }
205 - }
206 - });
207 - }
208 - });
209 - });
210 - }
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 + } );
211 270
212 - // ReCaptcha v3
213 - $('.recaptcha-form').on('submit', function (event) {
214 - event.preventDefault();
271 + // hCaptcha
272 + $( '.hcaptcha-form' ).on( 'submit', function( event ) {
273 + event.preventDefault();
215 274
216 - ps_id = $(this).find('.passster-submit-recaptcha').attr('data-psid');
217 - form = $("#" + ps_id);
218 - protection = $(this).find('.passster-submit-recaptcha').attr('data-protection');
219 - acf = $(this).find('.passster-submit-recaptcha').attr('data-acf');
220 - area = $(this).find('.passster-submit-recaptcha').attr('data-area');
221 - redirect = $(this).find('.passster-submit-recaptcha').attr('data-redirect');
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' ) || '';
222 282
223 - grecaptcha.ready(function () {
224 - grecaptcha.execute(ps_ajax.recaptcha_key, {action: 'validate_input'}).then(function (token) {
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 + } );
225 293
226 - form.prepend('<input type="hidden" name="token" value="' + token + '">');
227 - form.prepend('<input type="hidden" name="action" value="validate_input">');
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 + } );
228 305
229 - $.ajax({
230 - type: "post", dataType: "json", url: ps_ajax.ajax_url, data: {
231 - 'action': 'validate_input',
232 - 'nonce': ps_ajax.nonce,
233 - 'token': token,
234 - 'post_id': ps_ajax.post_id,
235 - 'type': 'recaptcha',
236 - 'protection': protection,
237 - 'captcha_id': ps_id,
238 - 'acf': acf,
239 - 'area': area,
240 - 'redirect': redirect
241 - }, success: function (response) {
242 - if (true === response.success) {
306 + $( '.turnstile-form' ).on( 'submit', function( event ) {
307 + event.preventDefault();
243 308
244 - // if no ajax.
245 - if (!ps_ajax.unlock_mode) {
246 - Cookies.set('passster', 'recaptcha', {
247 - expires: getDurationBySettings(), sameSite: 'strict'
248 - });
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' ) || '';
249 316
250 - if (response.redirect) {
251 - window.location.replace(redirect);
252 - } else {
253 - window.location.href = getCacheFriendlyURL();
254 - }
255 - } else {
256 - // set cookie if activated.
257 - if (!ps_ajax.disable_cookie) {
258 - Cookies.set('passster', 'recaptcha', {
259 - expires: getDurationBySettings(), sameSite: 'strict'
260 - });
261 - }
262 - form.find('.passster-error').hide();
263 - // replace shortcodes.
264 - let content = response.content;
317 + if ( ! turnstileToken ) {
318 + showError( $form, 'Please complete the verification.' );
319 + return;
320 + }
265 321
266 - if (content) {
267 - $.each(ps_ajax.shortcodes, function (key, value) {
268 - content = content.replace(key, value);
269 - });
322 + validateCaptcha( turnstileToken, 'turnstile', $form, psId, areaId, redirect, protection );
323 + } );
324 + }
270 325
271 - form.replaceWith(content);
272 - }
273 -
274 - // Redirect?
275 - if (response.redirect) {
276 - window.location.replace(redirect);
277 - }
278 - }
279 - } else {
280 - form.find('.passster-error').text(response.error);
281 - form.find('.passster-error').show().fadeOut(3500);
282 - }
283 - }
284 - });
285 - });
286 - });
287 - });
288 -
289 - // Concurrent logout.
290 - $(document).on('click', '#ps-logout', function () {
291 - $.ajax({
292 - type: 'post',
293 - dataType: 'json',
294 - url: ps_ajax.ajax_url,
295 - data: {'action': 'handle_logout', 'logout_nonce': ps_ajax.logout_nonce},
296 - success: function (response) {
297 - if (true === response.success) {
298 - Cookies.set('passster', '', {expires: 0, sameSite: 'strict'});
299 - window.location.href = getCacheFriendlyURL();
300 - }
301 - }
302 - });
303 - });
304 -});
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 +} );