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
content-protector / inc / class-ps-public.php

class-ps-public.php in Passster – Password Protect Pages and Content 4.3.17, at inc/class-ps-public.php

450 lines 19.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace passster;
4
5 use Exception;
6 class PS_Public {
7 /**
8 * Contains instance or null
9 *
10 * @var object|null
11 */
12 private static $instance = null;
13
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 /**
23 * Constructor for PS_Public
24 */
25 public function __construct() {
26 add_shortcode( 'content_protector', array($this, 'render_shortcode') );
27 add_shortcode( 'passster', array($this, 'render_shortcode') );
28 add_filter( 'the_content', array($this, 'filter_the_content') );
29 add_filter( 'acf_the_content', array($this, 'filter_the_content') );
30 add_filter( 'get_the_excerpt', array($this, 'filter_the_content') );
31 add_action( 'template_redirect', array($this, 'check_global_proctection') );
32 add_action( 'wp_enqueue_scripts', array($this, 'add_public_scripts'), 9999 );
33 }
34
35 /**
36 * Returns instance of PS_Public.
37 *
38 * @return object
39 */
40 public static function get_instance() {
41 if ( null === self::$instance ) {
42 self::$instance = new self();
43 }
44 return self::$instance;
45 }
46
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 /**
66 * Render the Passster shortcode.
67 *
68 * @param array $atts array of attributes.
69 * @param string|null $content the current content.
70 *
71 * @return string
72 */
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 }
98 // check if valid before restrict anything.
99 $valid = PS_Conditional::is_valid( $atts );
100 $options = get_option( 'passster' );
101 if ( $valid ) {
102 if ( !empty( $atts['area'] ) ) {
103 $area_id = esc_html( $atts['area'] );
104 $area = get_post( $area_id );
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 }
110 } else {
111 $content = $this->render_unlocked_content( $content );
112 do_action( 'passster_content_unlocked' );
113 return apply_filters( 'passster_content', $content );
114 }
115 }
116 // do nothing if no atts.
117 if ( empty( $atts ) ) {
118 return $content;
119 }
120 // Set default form.
121 $form = PS_Form::get_password_form();
122 // Password.
123 if ( !empty( $atts['password'] ) ) {
124 $form = PS_Form::get_password_form();
125 $form = str_replace( '[PASSSTER_TYPE]', 'password', $form );
126 }
127 // Area.
128 if ( !empty( $atts['area'] ) ) {
129 $area_id = absint( $atts['area'] );
130 $form = str_replace( '[PASSSTER_AREA]', $area_id, $form );
131 }
132 // Page.
133 if ( !empty( $atts['protection'] ) ) {
134 $form = str_replace( '[PASSSTER_PROTECTION]', 'full', $form );
135 } elseif ( !empty( $atts['area'] ) ) {
136 $form = str_replace( '[PASSSTER_PROTECTION]', 'area', $form );
137 }
138 // Redirect.
139 if ( !empty( $atts['redirect'] ) ) {
140 $form = str_replace( '[PASSSTER_REDIRECT]', esc_url( $atts['redirect'] ), $form );
141 } else {
142 $form = str_replace( '[PASSSTER_REDIRECT]', '', $form );
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 );
158 // headline.
159 if ( !empty( $options['hide_headline'] ) ) {
160 $form = str_replace( '[PASSSTER_FORM_HEADLINE]', '', $form );
161 } elseif ( !empty( $atts['headline'] ) ) {
162 $form = str_replace( '[PASSSTER_FORM_HEADLINE]', esc_html( $atts['headline'] ), $form );
163 } else {
164 $form = str_replace( '[PASSSTER_FORM_HEADLINE]', esc_html( $options['headline'] ), $form );
165 }
166 // instruction.
167 if ( !empty( $atts['instruction'] ) ) {
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 );
172 } else {
173 $form = str_replace( '[PASSSTER_FORM_INSTRUCTIONS]', wp_kses_post( $options['instruction'] ), $form );
174 }
175 // placeholder.
176 if ( !empty( $atts['placeholder'] ) ) {
177 $form = str_replace( '[PASSSTER_PLACEHOLDER]', esc_attr( $atts['placeholder'] ), $form );
178 } else {
179 $form = str_replace( '[PASSSTER_PLACEHOLDER]', esc_attr( $options['placeholder'] ), $form );
180 }
181 // label.
182 $form = str_replace( '[PASSSTER_LABEL]', esc_html__( 'Enter your password', 'content-protector' ), $form );
183 // button.
184 if ( !empty( $atts['button'] ) ) {
185 $form = str_replace( '[PASSSTER_BUTTON_LABEL]', esc_html( $atts['button'] ), $form );
186 } else {
187 $form = str_replace( '[PASSSTER_BUTTON_LABEL]', esc_html( $options['button_label'] ), $form );
188 }
189 // modify id.
190 if ( !empty( $atts['id'] ) ) {
191 $form = str_replace( '[PASSSTER_ID]', 'ps-' . esc_attr( $atts['id'] ), $form );
192 } else {
193 $form = str_replace( '[PASSSTER_ID]', 'ps-' . wp_rand( 10, 1000 ), $form );
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 );
203 // hide or not.
204 if ( !empty( $atts['hide'] ) ) {
205 $form = str_replace( '[PASSSTER_HIDE]', ' passster-hide', $form );
206 } else {
207 $form = str_replace( '[PASSSTER_HIDE]', '', $form );
208 }
209 // ACF field.
210 if ( !empty( $atts['acf'] ) ) {
211 $form = str_replace( '[PASSSTER_ACF]', ' data-acf="' . esc_url( $atts['acf'] ) . '"', $form );
212 } else {
213 $form = str_replace( '[PASSSTER_ACF]', '', $form );
214 }
215 return $form;
216 }
217
218 /**
219 * Filters the_content with Passster.
220 *
221 * @param string $content given content.
222 *
223 * @return string
224 * @throws Exception
225 */
226 public function filter_the_content( string $content ) : string {
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 }
246 $activate_protection = get_post_meta( $post_id, 'passster_activate_protection', true );
247 // user restriction.
248 $user_restriction_type = get_post_meta( $post_id, 'passster_user_restriction_type', true );
249 $user_restriction = get_post_meta( $post_id, 'passster_user_restriction', true );
250 // Redirection.
251 $redirection = get_post_meta( $post_id, 'passster_redirect_url', true );
252 // texts.
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 ) : '' );
258 $id = get_post_meta( $post_id, 'passster_id', true );
259 if ( !$activate_protection ) {
260 return $content;
261 }
262 // build atts array to validate.
263 $atts = array();
264 $shortcode = '';
265 $password = get_post_meta( $post_id, 'passster_password', true );
266 $atts['password'] = $password;
267 $shortcode = '[passster password="' . $password . '" protection="full" ';
268 if ( !empty( $redirection ) ) {
269 $shortcode .= 'redirect="' . $redirection . '" ';
270 }
271 if ( !empty( $headline ) ) {
272 $shortcode .= 'headline="' . $headline . '" ';
273 }
274 if ( !empty( $instruction ) ) {
275 $shortcode .= 'instruction="' . base64_encode( $instruction ) . '" ';
276 }
277 if ( !empty( $placeholder ) ) {
278 $shortcode .= 'placeholder="' . $placeholder . '" ';
279 }
280 if ( !empty( $button ) ) {
281 $shortcode .= 'button="' . $button . '" ';
282 }
283 if ( !empty( $id ) ) {
284 $shortcode .= 'id="' . $id . '" ';
285 }
286 $shortcode .= ']{content}[/passster]';
287 // check if valid before restrict anything.
288 $valid = PS_Conditional::is_valid( $atts );
289 if ( $valid ) {
290 return $content;
291 }
292 // replace placeholder with content.
293 $shortcode = str_replace( '{content}', $content, $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;
301 }
302
303 /**
304 * Redirect if global protection is activated and no password is set.
305 *
306 * @return void
307 * @throws Exception
308 */
309 public function check_global_proctection() {
310 $options = get_option( 'passster' );
311 $post_id = get_queried_object_id();
312 if ( !$post_id ) {
313 return;
314 }
315 // Allow Elementor editing the page.
316 $elementor_preview = filter_input( INPUT_GET, 'elementor-preview', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
317 // Allow Live Canvas Editor.
318 $live_canvas_preview = filter_input( INPUT_GET, 'lc_action_launch_editing', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
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 }
323 }
324 if ( !isset( $options['global_protection_id'] ) ) {
325 return;
326 }
327 if ( !isset( $options['activate_global_protection'] ) ) {
328 return;
329 }
330 // Build $atts array based on protection settings.
331 $post_id = esc_html( $options['global_protection_id'] );
332 $is_active = esc_html( $options['activate_global_protection'] );
333 $atts = array();
334 $password = get_post_meta( $post_id, 'passster_password', true );
335 $atts['password'] = $password;
336 if ( !empty( $post_id ) ) {
337 if ( $is_active ) {
338 if ( is_page( $post_id ) || is_single( $post_id ) ) {
339 return;
340 }
341 // Check excluded pages.
342 if ( isset( $options['exclude_pages'] ) ) {
343 foreach ( $options['exclude_pages'] as $excluded_page_id ) {
344 if ( is_page( $excluded_page_id ) ) {
345 return;
346 }
347 }
348 }
349 // Check if cookie is set.
350 $cookie = esc_html( $_COOKIE['passster'] );
351 if ( !PS_Conditional::is_valid( $atts ) ) {
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 }
357 wp_redirect( esc_url_raw( $global_protection_url ) );
358 exit;
359 }
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 );
446 }
447 }
448
449 }
450