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
← All changes | inc/class-ps-public.php +221 -30 4.2.114.3.16 View file →
@@ -11,8 +11,16 @@
11 11 */
12 12 private static $instance = null;
13 13
14 14 /**
15 + * Track which posts have already had their protection form rendered
16 + * to prevent duplicate forms on page builders like Avada.
17 + *
18 + * @var array
19 + */
20 + private static $rendered_protection = array();
21 +
22 + /**
15 23 * Constructor for PS_Public
16 24 */
17 25 public function __construct() {
18 26 add_shortcode( 'content_protector', array($this, 'render_shortcode') );
@@ -20,8 +28,9 @@
20 28 add_filter( 'the_content', array($this, 'filter_the_content') );
21 29 add_filter( 'acf_the_content', array($this, 'filter_the_content') );
22 30 add_filter( 'get_the_excerpt', array($this, 'filter_the_content') );
23 31 add_action( 'template_redirect', array($this, 'check_global_proctection') );
32 + add_action( 'wp_enqueue_scripts', array($this, 'add_public_scripts'), 9999 );
24 33 }
25 34
26 35 /**
27 36 * Returns instance of PS_Public.
@@ -35,8 +44,22 @@
35 44 return self::$instance;
36 45 }
37 46
38 47 /**
48 + * Avoids re-entering the_content recursively.
49 + *
50 + * @param string $content the unlocked content.
51 + *
52 + * @return string
53 + */
54 + private function render_unlocked_content( string $content ) : string {
55 + if ( doing_filter( 'the_content' ) ) {
56 + return wpautop( do_shortcode( $content ) );
57 + }
58 + return apply_filters( 'the_content', $content );
59 + }
60 +
61 + /**
39 62 * Render the Passster shortcode.
40 63 *
41 64 * @param array $atts array of attributes.
42 65 * @param string|null $content the current content.
@@ -43,8 +66,32 @@
43 66 *
44 67 * @return string
45 68 */
46 69 public function render_shortcode( array $atts, string $content = null ) : string {
70 + // Schedule check for protected areas (PRO only).
71 + if ( !empty( $atts['area'] ) && \passster_fs()->is_plan_or_trial__premium_only( 'pro' ) ) {
72 + $area_id = absint( $atts['area'] );
73 + $schedule_enabled = get_post_meta( $area_id, 'passster_schedule_enabled', true );
74 + if ( $schedule_enabled ) {
75 + $schedule_start = get_post_meta( $area_id, 'passster_schedule_start', true );
76 + $schedule_end = get_post_meta( $area_id, 'passster_schedule_end', true );
77 + $now = current_time( 'timestamp' );
78 + $in_schedule = true;
79 + if ( !empty( $schedule_start ) && $now < strtotime( $schedule_start ) ) {
80 + $in_schedule = false;
81 + }
82 + if ( !empty( $schedule_end ) && $now > strtotime( $schedule_end ) ) {
83 + $in_schedule = false;
84 + }
85 + if ( !$in_schedule ) {
86 + $area = get_post( $area_id );
87 + if ( $area && ('publish' === $area->post_status || current_user_can( 'edit_post', $area_id )) ) {
88 + return $this->render_unlocked_content( str_replace( '{post-id}', get_the_id(), $area->post_content ) );
89 + }
90 + return $content ?? '';
91 + }
92 + }
93 + }
47 94 // check if valid before restrict anything.
48 95 $valid = PS_Conditional::is_valid( $atts );
49 96 $options = get_option( 'passster' );
50 97 if ( $valid ) {
@@ -50,13 +97,15 @@
50 97 if ( $valid ) {
51 98 if ( !empty( $atts['area'] ) ) {
52 99 $area_id = esc_html( $atts['area'] );
53 100 $area = get_post( $area_id );
54 - $content = $area->post_content;
55 - do_action( 'passster_content_unlocked' );
56 - return apply_filters( 'the_content', str_replace( '{post-id}', get_the_id(), $content ) );
101 + if ( 'publish' === $area->post_status || current_user_can( 'edit_post', $area_id ) ) {
102 + $content = $area->post_content;
103 + do_action( 'passster_content_unlocked' );
104 + return $this->render_unlocked_content( str_replace( '{post-id}', get_the_id(), $content ) );
105 + }
57 106 } else {
58 - $content = apply_filters( 'the_content', $content );
107 + $content = $this->render_unlocked_content( $content );
59 108 do_action( 'passster_content_unlocked' );
60 109 return apply_filters( 'passster_content', $content );
61 110 }
62 111 }
@@ -72,18 +121,16 @@
72 121 $form = str_replace( '[PASSSTER_TYPE]', 'password', $form );
73 122 }
74 123 // Area.
75 124 if ( !empty( $atts['area'] ) ) {
76 - $area_id = esc_html( $atts['area'] );
125 + $area_id = absint( $atts['area'] );
77 126 $form = str_replace( '[PASSSTER_AREA]', $area_id, $form );
78 127 }
79 128 // Page.
80 129 if ( !empty( $atts['protection'] ) ) {
81 130 $form = str_replace( '[PASSSTER_PROTECTION]', 'full', $form );
82 - } else {
83 - if ( !empty( $atts['area'] ) ) {
84 - $form = str_replace( '[PASSSTER_PROTECTION]', 'area', $form );
85 - }
131 + } elseif ( !empty( $atts['area'] ) ) {
132 + $form = str_replace( '[PASSSTER_PROTECTION]', 'area', $form );
86 133 }
87 134 // Redirect.
88 135 if ( !empty( $atts['redirect'] ) ) {
89 136 $form = str_replace( '[PASSSTER_REDIRECT]', esc_url( $atts['redirect'] ), $form );
@@ -89,8 +136,22 @@
89 136 $form = str_replace( '[PASSSTER_REDIRECT]', esc_url( $atts['redirect'] ), $form );
90 137 } else {
91 138 $form = str_replace( '[PASSSTER_REDIRECT]', '', $form );
92 139 }
140 + // headline tag.
141 + $allowed_headline_tags = array(
142 + 'span',
143 + 'p',
144 + 'div',
145 + 'h1',
146 + 'h2',
147 + 'h3',
148 + 'h4',
149 + 'h5',
150 + 'h6'
151 + );
152 + $headline_tag = ( isset( $options['headline_tag'] ) && in_array( $options['headline_tag'], $allowed_headline_tags, true ) ? $options['headline_tag'] : 'span' );
153 + $form = str_replace( '[PASSSTER_HEADLINE_TAG]', $headline_tag, $form );
93 154 // headline.
94 155 if ( !empty( $options['hide_headline'] ) ) {
95 156 $form = str_replace( '[PASSSTER_FORM_HEADLINE]', '', $form );
96 157 } elseif ( !empty( $atts['headline'] ) ) {
@@ -95,34 +156,47 @@
95 156 $form = str_replace( '[PASSSTER_FORM_HEADLINE]', '', $form );
96 157 } elseif ( !empty( $atts['headline'] ) ) {
97 158 $form = str_replace( '[PASSSTER_FORM_HEADLINE]', esc_html( $atts['headline'] ), $form );
98 159 } else {
99 - $form = str_replace( '[PASSSTER_FORM_HEADLINE]', $options['headline'], $form );
160 + $form = str_replace( '[PASSSTER_FORM_HEADLINE]', esc_html( $options['headline'] ), $form );
100 161 }
101 162 // instruction.
102 163 if ( !empty( $atts['instruction'] ) ) {
103 - $form = str_replace( '[PASSSTER_FORM_INSTRUCTIONS]', esc_html( $atts['instruction'] ), $form );
164 + $decoded_instruction = base64_decode( $atts['instruction'] );
165 + $decoded_instruction = html_entity_decode( $decoded_instruction );
166 + $sanitized_instruction = wp_kses_post( $decoded_instruction );
167 + $form = str_replace( '[PASSSTER_FORM_INSTRUCTIONS]', $sanitized_instruction, $form );
104 168 } else {
105 - $form = str_replace( '[PASSSTER_FORM_INSTRUCTIONS]', $options['instruction'], $form );
169 + $form = str_replace( '[PASSSTER_FORM_INSTRUCTIONS]', wp_kses_post( $options['instruction'] ), $form );
106 170 }
107 171 // placeholder.
108 172 if ( !empty( $atts['placeholder'] ) ) {
109 - $form = str_replace( '[PASSSTER_PLACEHOLDER]', esc_html( $atts['placeholder'] ), $form );
173 + $form = str_replace( '[PASSSTER_PLACEHOLDER]', esc_attr( $atts['placeholder'] ), $form );
110 174 } else {
111 - $form = str_replace( '[PASSSTER_PLACEHOLDER]', $options['placeholder'], $form );
175 + $form = str_replace( '[PASSSTER_PLACEHOLDER]', esc_attr( $options['placeholder'] ), $form );
112 176 }
177 + // label.
178 + $form = str_replace( '[PASSSTER_LABEL]', esc_html__( 'Enter your password', 'content-protector' ), $form );
113 179 // button.
114 180 if ( !empty( $atts['button'] ) ) {
115 181 $form = str_replace( '[PASSSTER_BUTTON_LABEL]', esc_html( $atts['button'] ), $form );
116 182 } else {
117 - $form = str_replace( '[PASSSTER_BUTTON_LABEL]', $options['button_label'], $form );
183 + $form = str_replace( '[PASSSTER_BUTTON_LABEL]', esc_html( $options['button_label'] ), $form );
118 184 }
119 185 // modify id.
120 186 if ( !empty( $atts['id'] ) ) {
121 - $form = str_replace( '[PASSSTER_ID]', 'ps-' . esc_html( $atts['id'] ), $form );
187 + $form = str_replace( '[PASSSTER_ID]', 'ps-' . esc_attr( $atts['id'] ), $form );
122 188 } else {
123 189 $form = str_replace( '[PASSSTER_ID]', 'ps-' . wp_rand( 10, 1000 ), $form );
124 190 }
191 + // post id (per-form, for correct REST unlock on archive pages with multiple protected posts).
192 + $form = str_replace( '[PASSSTER_POST_ID]', absint( get_the_ID() ), $form );
193 + // term id (for category archive protection — passed to REST API so it can validate against term meta).
194 + $term_id_val = ( !empty( $atts['term_id'] ) ? absint( $atts['term_id'] ) : 0 );
195 + $form = str_replace( '[PASSSTER_TERM_ID]', $term_id_val, $form );
196 + // post type (for post type archive protection — passed to REST API so it can validate against post type config).
197 + $post_type_val = ( !empty( $atts['post_type'] ) ? sanitize_key( $atts['post_type'] ) : '' );
198 + $form = str_replace( '[PASSSTER_POST_TYPE]', esc_attr( $post_type_val ), $form );
125 199 // hide or not.
126 200 if ( !empty( $atts['hide'] ) ) {
127 201 $form = str_replace( '[PASSSTER_HIDE]', ' passster-hide', $form );
128 202 } else {
@@ -129,9 +203,9 @@
129 203 $form = str_replace( '[PASSSTER_HIDE]', '', $form );
130 204 }
131 205 // ACF field.
132 206 if ( !empty( $atts['acf'] ) ) {
133 - $form = str_replace( '[PASSSTER_ACF]', ' data-acf="' . esc_html( $atts['acf'] ) . '"', $form );
207 + $form = str_replace( '[PASSSTER_ACF]', ' data-acf="' . esc_url( $atts['acf'] ) . '"', $form );
134 208 } else {
135 209 $form = str_replace( '[PASSSTER_ACF]', '', $form );
136 210 }
137 211 return $form;
@@ -146,8 +220,26 @@
146 220 * @throws Exception
147 221 */
148 222 public function filter_the_content( string $content ) : string {
149 223 $post_id = get_the_id();
224 + // Prevent duplicate form rendering (fixes issue with Avada and other page builders)
225 + if ( isset( self::$rendered_protection[$post_id] ) ) {
226 + // Already rendered the protection form for this post, return protected content placeholder
227 + // or the form that was already generated
228 + return self::$rendered_protection[$post_id]['form'] ?? $content;
229 + }
230 + $parent_id = wp_get_post_parent_id( $post_id );
231 + if ( $parent_id ) {
232 + $activate_protection = get_post_meta( $parent_id, 'passster_activate_protection', true );
233 + $children_protection = get_post_meta( $parent_id, 'passster_protect_child_pages', true );
234 + if ( $activate_protection && $children_protection ) {
235 + $post_id = $parent_id;
236 + // Check parent too
237 + if ( isset( self::$rendered_protection[$post_id] ) ) {
238 + return self::$rendered_protection[$post_id]['form'] ?? $content;
239 + }
240 + }
241 + }
150 242 $activate_protection = get_post_meta( $post_id, 'passster_activate_protection', true );
151 243 // user restriction.
152 244 $user_restriction_type = get_post_meta( $post_id, 'passster_user_restriction_type', true );
153 245 $user_restriction = get_post_meta( $post_id, 'passster_user_restriction', true );
@@ -153,12 +245,13 @@
153 245 $user_restriction = get_post_meta( $post_id, 'passster_user_restriction', true );
154 246 // Redirection.
155 247 $redirection = get_post_meta( $post_id, 'passster_redirect_url', true );
156 248 // texts.
157 - $headline = get_post_meta( $post_id, 'passster_headline', true );
158 - $instruction = get_post_meta( $post_id, 'passster_instruction', true );
159 - $placeholder = get_post_meta( $post_id, 'passster_placeholder', true );
160 - $button = get_post_meta( $post_id, 'passster_button', true );
249 + $overwrite_defaults = get_post_meta( $post_id, 'passster_activate_overwrite_defaults', true );
250 + $headline = ( $overwrite_defaults ? get_post_meta( $post_id, 'passster_headline', true ) : '' );
251 + $instruction = ( $overwrite_defaults ? get_post_meta( $post_id, 'passster_instruction', true ) : '' );
252 + $placeholder = ( $overwrite_defaults ? get_post_meta( $post_id, 'passster_placeholder', true ) : '' );
253 + $button = ( $overwrite_defaults ? get_post_meta( $post_id, 'passster_button', true ) : '' );
161 254 $id = get_post_meta( $post_id, 'passster_id', true );
162 255 if ( !$activate_protection ) {
163 256 return $content;
164 257 }
@@ -174,9 +267,9 @@
174 267 if ( !empty( $headline ) ) {
175 268 $shortcode .= 'headline="' . $headline . '" ';
176 269 }
177 270 if ( !empty( $instruction ) ) {
178 - $shortcode .= 'instruction="' . $instruction . '" ';
271 + $shortcode .= 'instruction="' . base64_encode( $instruction ) . '" ';
179 272 }
180 273 if ( !empty( $placeholder ) ) {
181 274 $shortcode .= 'placeholder="' . $placeholder . '" ';
182 275 }
@@ -193,9 +286,15 @@
193 286 return $content;
194 287 }
195 288 // replace placeholder with content.
196 289 $shortcode = str_replace( '{content}', $content, $shortcode );
197 - return do_shortcode( $shortcode );
290 + // Generate the form
291 + $rendered_form = do_shortcode( $shortcode );
292 + // Store reference to prevent duplicate rendering (Avada, Elementor, etc.)
293 + self::$rendered_protection[$post_id] = array(
294 + 'form' => $rendered_form,
295 + );
296 + return $rendered_form;
198 297 }
199 298
200 299 /**
201 300 * Redirect if global protection is activated and no password is set.
@@ -204,17 +303,20 @@
204 303 * @throws Exception
205 304 */
206 305 public function check_global_proctection() {
207 306 $options = get_option( 'passster' );
307 + $post_id = get_queried_object_id();
308 + if ( !$post_id ) {
309 + return;
310 + }
208 311 // Allow Elementor editing the page.
209 312 $elementor_preview = filter_input( INPUT_GET, 'elementor-preview', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
210 - if ( $elementor_preview ) {
211 - return;
212 - }
213 313 // Allow Live Canvas Editor.
214 314 $live_canvas_preview = filter_input( INPUT_GET, 'lc_action_launch_editing', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
215 - if ( $live_canvas_preview ) {
216 - return;
315 + if ( is_preview() || $elementor_preview || $live_canvas_preview ) {
316 + if ( is_user_logged_in() && current_user_can( 'edit_post', $post_id ) ) {
317 + return;
318 + }
217 319 }
218 320 if ( !isset( $options['global_protection_id'] ) ) {
219 321 return;
220 322 }
@@ -225,9 +327,9 @@
225 327 $post_id = esc_html( $options['global_protection_id'] );
226 328 $is_active = esc_html( $options['activate_global_protection'] );
227 329 $atts = array();
228 330 $password = get_post_meta( $post_id, 'passster_password', true );
229 - $atts['password'] = esc_html( $password );
331 + $atts['password'] = $password;
230 332 if ( !empty( $post_id ) ) {
231 333 if ( $is_active ) {
232 334 if ( is_page( $post_id ) || is_single( $post_id ) ) {
233 335 return;
@@ -241,14 +343,103 @@
241 343 }
242 344 }
243 345 // Check if cookie is set.
244 346 $cookie = esc_html( $_COOKIE['passster'] );
245 - if ( empty( $cookie ) || !PS_Conditional::is_valid( $atts ) ) {
347 + if ( !PS_Conditional::is_valid( $atts ) ) {
246 348 $global_protection_url = get_permalink( $post_id );
349 + $pass_param = filter_input( INPUT_GET, 'pass', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
350 + if ( !empty( $pass_param ) ) {
351 + $global_protection_url = add_query_arg( 'pass', $pass_param, $global_protection_url );
352 + }
247 353 wp_redirect( esc_url_raw( $global_protection_url ) );
248 354 exit;
249 355 }
250 356 }
357 + }
358 + }
359 +
360 + /**
361 + * Enqueue scripts for shortcode
362 + *
363 + * @return void
364 + */
365 + public function add_public_scripts() {
366 + $suffix = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min' );
367 + $options = get_option( 'passster' );
368 + // Only load CSS if not disabled (allows themes to style the form)
369 + if ( empty( $options['disable_css'] ) ) {
370 + wp_enqueue_style(
371 + 'passster-public',
372 + PASSSTER_URL . '/assets/public/passster-public' . $suffix . '.css',
373 + array(),
374 + PASSSTER_VERSION,
375 + 'all'
376 + );
377 + }
378 + wp_enqueue_script(
379 + 'passster-cookie',
380 + PASSSTER_URL . '/assets/public/cookie.js',
381 + array('jquery', 'wp-api-fetch'),
382 + PASSSTER_VERSION,
383 + false
384 + );
385 + wp_enqueue_script(
386 + 'passster-public',
387 + PASSSTER_URL . '/assets/public/passster-public' . $suffix . '.js',
388 + array('jquery', 'passster-cookie'),
389 + PASSSTER_VERSION,
390 + false
391 + );
392 + $shortcodes = array();
393 + if ( isset( $options['third_party_shortcodes'] ) && !empty( $options['third_party_shortcodes'] ) ) {
394 + $shortcodes_in_options = explode( ',', $options['third_party_shortcodes'] );
395 + if ( is_array( $shortcodes_in_options ) ) {
396 + foreach ( $shortcodes_in_options as $shortcode ) {
397 + $shortcodes[$shortcode] = do_shortcode( str_replace( '{post-id}', get_the_id(), $shortcode ) );
398 + }
399 + }
400 + }
401 + // Archive/taxonomy pages have no singular post, so get_permalink() can't be used
402 + // to build the "reload after unlock" URL for links generated by area/CPT-level protection.
403 + $current_post_id = get_the_id();
404 + $reload_url = ( $current_post_id ? get_permalink( $current_post_id ) : esc_url_raw( remove_query_arg( 'pass' ) ) );
405 + $args = array(
406 + 'ajax_url' => admin_url() . 'admin-ajax.php',
407 + 'rest_url' => get_rest_url(),
408 + 'nonce' => wp_create_nonce( 'ps-password-nonce' ),
409 + 'hash_nonce' => wp_create_nonce( 'ps-hash-nonce' ),
410 + 'logout_nonce' => wp_create_nonce( 'ps-logout-nonce' ),
411 + 'post_id' => $current_post_id,
412 + 'shortcodes' => $shortcodes,
413 + 'permalink' => $reload_url,
414 + );
415 + if ( isset( $options['cookie_duration_unit'] ) ) {
416 + $args['cookie_duration_unit'] = esc_html( $options['cookie_duration_unit'] );
417 + } else {
418 + $args['cookie_duration_unit'] = 'days';
419 + }
420 + if ( isset( $options['cookie_duration'] ) ) {
421 + $args['cookie_duration'] = esc_html( $options['cookie_duration'] );
422 + } else {
423 + $args['cookie_duration'] = 1;
424 + }
425 + if ( isset( $options['disable_cookie'] ) ) {
426 + $args['disable_cookie'] = esc_html( $options['disable_cookie'] );
427 + } else {
428 + $args['disable_cookie'] = false;
429 + }
430 + $args['unlock_mode'] = !empty( $options['unlock_mode'] );
431 + wp_localize_script( 'passster-public', 'ps_ajax', $args );
432 + // if password type hint used.
433 + $password_typing = $options['show_password'];
434 + if ( $password_typing ) {
435 + wp_enqueue_script(
436 + 'password-typing',
437 + PASSSTER_URL . '/assets/public/password-typing.js',
438 + array('jquery'),
439 + PASSSTER_VERSION,
440 + false
441 + );
251 442 }
252 443 }
253 444
254 445 }