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