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