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 +319 -221 3.5.4 → 4.3.17 View file →
@@ -1,245 +1,343 @@
1 -jQuery(document).ready(function( $ ) {
1 +jQuery( document ).ready( function( $ ) {
2 + const restUrl = ps_ajax.rest_url + 'passster/v1';
2 3
3 - // Check if we have an unlock link.
4 - if ( ps_ajax.link_pass ) {
5 - if ( 'on' === ps_ajax.use_cookie ) {
6 - Cookies.set('passster', ps_ajax.link_pass, { expires: parseInt( ps_ajax.days ) });
7 - window.location.replace( ps_ajax.permalink );
8 - }
9 - }
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 + }
10 17
11 - // Passwords
12 - $('.passster-submit').on('click', function(e){
13 - e.preventDefault();
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 + }
14 26
15 - // Validate form before submitting ajax.
16 - var form = $(this).parent().parent();
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 + }
17 42
18 - if ( ! $(form)[0].checkValidity() ) {
19 - $(form)[0].reportValidity();
20 - }
21 -
22 - ps_id = $(this).attr('data-psid');
23 - form = $( "#" + ps_id );
24 - password = $( "#" + ps_id + ' .passster-password').attr('data-password');
25 - type = $( "#" + ps_id + ' .passster-password').attr('data-protection-type');
26 - list = $( "#" + ps_id + ' .passster-password').attr('data-list');
27 - area = $( "#" + ps_id + ' .passster-password').attr('data-area');
28 - protection = $( "#" + ps_id + ' .passster-password').attr('data-protection');
29 - input = $( "#" + ps_id + ' .passster-password').val();
30 - acf = $( this ).attr('data-acf');
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 + }
31 52
32 - $.ajax({
33 - type: "post",
34 - dataType: "json",
35 - url: ps_ajax.ajax_url,
36 - data: { 'action': 'validate_input','nonce' : ps_ajax.nonce, 'input' : input, 'password' : password, 'post_id' : ps_ajax.post_id, 'type' : type, 'list' : list, 'area' : area, 'protection' : protection, 'acf' : acf },
37 - beforeSend: function() {
38 - form.find(".ps-loader").css('display', 'block');
39 - },
40 - success: function(response){
41 - form.find(".ps-loader").css('display', 'none');
42 - if ( true === response.success ) {
43 - // if no ajax.
44 - if ( 'on' === ps_ajax.no_ajax ) {
45 - Cookies.set('passster', btoa( input ), { expires: parseInt( ps_ajax.days ) });
46 - window.location.reload();
47 - } else {
48 - // set cookie if activated.
49 - if ( 'on' === ps_ajax.use_cookie ) {
50 - Cookies.set('passster', btoa( input ), { expires: parseInt( ps_ajax.days ) });
51 - }
52 - form.find('.passster-error').hide();
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 - // replace shortcodes.
55 - let content = response.content;
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 + }
56 80
57 - $.each(ps_ajax.shortcodes, function(key, value) {
58 - content = content.replace( key, value );
59 - });
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 - $( "#" + ps_id ).replaceWith( content );
62 - }
63 - } else {
64 - form.find('.passster-error').text(response.error);
65 - form.find('.passster-error').show().fadeOut(2500);
66 - $( "#" + ps_id + ' .passster-password').val('');
67 - }
68 - }
69 - });
70 - });
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 + }
71 110
72 - // Recaptcha v2
73 - if ( $('.recaptcha-form-v2').length > 0 ) {
74 - grecaptcha.ready(function() {
75 - grecaptcha.render('ps-recaptcha-v2', {
76 - 'sitekey' : ps_ajax.recaptcha_key,
77 - 'callback' : function( token ) {
78 - ps_id = $('.recaptcha-v2-submit').attr('data-psid');
79 - form = $( "#" + ps_id );
80 - protection = $('.recaptcha-v2-submit').attr('data-protection');
81 - acf = $( '.recaptcha-v2-submit' ).attr('data-acf');
82 - area = $( "#" + ps_id).find('.recaptcha-v2-submit').attr('data-area');
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 + }
83 117
84 - $.ajax({
85 - type: "post",
86 - dataType: "json",
87 - url: ps_ajax.ajax_url,
88 - data: { 'action': 'validate_input','nonce' : ps_ajax.nonce, 'token' : token, 'post_id' : ps_ajax.post_id, 'type' : 'recaptcha', 'protection' : protection, 'captcha_id' : ps_id, 'acf' : acf, 'area' : area },
89 - success: function(response){
90 - // todo: set cookie if activated.
91 - if ( true === response.success ) {
92 - // if no ajax.
93 - if ( 'on' === ps_ajax.no_ajax ) {
94 - Cookies.set('passster', btoa( 'recaptcha' ), { expires: parseInt( ps_ajax.days ) });
95 - window.location.reload();
96 - } else {
97 - // set cookie if activated.
98 - if ( 'on' === ps_ajax.use_cookie ) {
99 - Cookies.set('passster', btoa( 'recaptcha' ), { expires: parseInt( ps_ajax.days ) });
100 - }
101 - form.find('.passster-error').hide();
102 -
103 - // replace shortcodes.
104 - let content = response.content;
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 + }
105 131
106 - $.each(ps_ajax.shortcodes, function(key, value) {
107 - content = content.replace( key, value );
108 - });
132 + // Password form submit - using REST API.
133 + $( '.passster-submit' ).on( 'click', async function( e ) {
134 + e.preventDefault();
109 135
110 - ( "#" + ps_id ).replaceWith( content );
111 - }
112 - } else {
113 - form.find('.passster-error').text(response.error);
114 - form.find('.passster-error').show().fadeOut(2500);
115 - }
116 - }
117 - });
118 - }
119 - });
120 - });
121 - }
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;
122 141
123 - // ReCaptcha v3
124 - $('.recaptcha-form').submit(function(event) {
125 - event.preventDefault();
142 + // Validate form.
143 + const formEl = $form.find( 'form' )[ 0 ];
144 + if ( formEl && ! formEl.checkValidity() ) {
145 + formEl.reportValidity();
146 + return;
147 + }
126 148
127 - ps_id = $(this).find('.passster-submit-recaptcha').attr('data-psid');
128 - form = $( "#" + ps_id );
129 - protection = $(this).find('.passster-submit-recaptcha').attr('data-protection');
130 - acf = $(this).find('.passster-submit-recaptcha').attr('data-acf');
131 - area = $(this).find('.passster-submit-recaptcha').attr('data-area');
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' ) || '';
132 160
133 - grecaptcha.ready(function() {
134 - grecaptcha.execute(ps_ajax.recaptcha_key, {action: 'validate_input'}).then(function(token) {
135 -
136 - form.prepend('<input type="hidden" name="token" value="' + token + '">');
137 - form.prepend('<input type="hidden" name="action" value="validate_input">');
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' ) : '';
138 164
139 - $.ajax({
140 - type: "post",
141 - dataType: "json",
142 - url: ps_ajax.ajax_url,
143 - data: { 'action': 'validate_input','nonce' : ps_ajax.nonce, 'token' : token, 'post_id' : ps_ajax.post_id, 'type' : 'recaptcha', 'protection' : protection, 'captcha_id' : ps_id, 'acf' : acf, 'area' : area },
144 - success: function(response){
145 - // todo: set cookie if activated.
146 - if ( true === response.success ) {
147 - // if no ajax.
148 - if ( 'on' === ps_ajax.no_ajax ) {
149 - Cookies.set('passster', btoa( 'recaptcha' ), { expires: parseInt( ps_ajax.days ) });
150 - window.location.reload();
151 - } else {
152 - // set cookie if activated.
153 - if ( 'on' === ps_ajax.use_cookie ) {
154 - Cookies.set('passster', btoa( 'recaptcha' ), { expires: parseInt( ps_ajax.days ) });
155 - }
156 - form.find('.passster-error').hide();
157 - // replace shortcodes.
158 - let content = response.content;
165 + $form.find( '.ps-loader' ).css( 'display', 'block' );
159 166
160 - $.each(ps_ajax.shortcodes, function(key, value) {
161 - content = content.replace( key, value );
162 - });
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 + } );
163 187
164 - form.replaceWith( content );
165 - }
166 - } else {
167 - form.find('.passster-error').text(response.error);
168 - form.find('.passster-error').show().fadeOut(2500);
169 - }
170 - }
171 - });
172 - });
173 - });
174 - });
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 + } );
175 201
176 - // Captcha
177 - if ( $('.passster-captcha-input').length > 0 ) {
178 - var captcha = new jCaptcha({
179 - el: '.passster-captcha-input',
180 - canvasClass: 'jCaptchaCanvas',
181 - canvasStyle: {
182 - // properties for captcha stylings
183 - width: 100,
184 - height: 25,
185 - textBaseline: 'top',
186 - font: '22px Arial',
187 - textAlign: 'left',
188 - fillStyle: '#000',
189 - },
190 - 'requiredValue' : '',
191 - callback: function(response ) {
192 - if (response == 'success') {
193 - ps_id = $('.passster-submit-captcha').attr('data-psid');
194 - form = $( "#" + ps_id );
195 - protection = $('.passster-submit-captcha').attr('data-protection');
196 - acf = $('.passster-submit-captcha').attr('data-acf');
197 - area = $( "#" + ps_id).find('.passster-captcha-input').attr('data-area');
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 + } );
198 218
199 - $.ajax({
200 - type: "post",
201 - dataType: "json",
202 - url: ps_ajax.ajax_url,
203 - data: { 'action': 'validate_input','nonce' : ps_ajax.nonce, 'captcha' : 'success', 'post_id' : ps_ajax.post_id, 'type' : 'captcha', 'protection' : protection, 'captcha_id' : ps_id, 'acf' : acf, 'area' : area },
204 - success: function(response){
205 - if ( true === response.success ) {
206 - // if no ajax.
207 - if ( 'on' === ps_ajax.no_ajax ) {
208 - Cookies.set('passster', btoa( 'captcha' ), { expires: parseInt( ps_ajax.days ) });
209 - window.location.reload();
210 - } else {
211 - // set cookie if activated.
212 - if ( 'on' === ps_ajax.use_cookie ) {
213 - Cookies.set('passster', btoa( 'captcha' ), { expires: parseInt( ps_ajax.days ) });
214 - }
215 - form.find('.passster-error').hide();
216 - // replace shortcodes.
217 - let content = response.content;
219 + const data = await response.json();
218 220
219 - $.each(ps_ajax.shortcodes, function(key, value) {
220 - content = content.replace( key, value );
221 - });
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 + }
222 230
223 - $( "#" + ps_id ).replaceWith( content );
224 - }
225 - } else {
226 - form.find('.passster-error').text(response.error);
227 - form.find('.passster-error').show().fadeOut(2500);
228 - }
229 - }
230 - });
231 - }
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' ) || '';
232 243
233 - if (response == 'error') {
234 - form.find('.passster-error').text(ps_ajax.captcha_error);
235 - form.find('.passster-error').show().fadeOut(2500);
236 - }
237 - }
238 - });
244 + validateCaptcha( token, 'recaptcha_v2', $form, psId, areaId, redirect, protection );
245 + },
246 + } );
247 + } );
248 + }
239 249
240 - document.querySelector('.captcha-form').addEventListener('submit', function(e){
241 - e.preventDefault();
242 - captcha.validate()
243 - });
244 - }
245 -});
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 +} );