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

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

446 lines 18.8 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 return wpautop( do_shortcode( $content ) );
57 }
58 return apply_filters( 'the_content', $content );
59 }
60
61 /**
62 * Render the Passster shortcode.
63 *
64 * @param array $atts array of attributes.
65 * @param string|null $content the current content.
66 *
67 * @return string
68 */
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 }
94 // check if valid before restrict anything.
95 $valid = PS_Conditional::is_valid( $atts );
96 $options = get_option( 'passster' );
97 if ( $valid ) {
98 if ( !empty( $atts['area'] ) ) {
99 $area_id = esc_html( $atts['area'] );
100 $area = get_post( $area_id );
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 }
106 } else {
107 $content = $this->render_unlocked_content( $content );
108 do_action( 'passster_content_unlocked' );
109 return apply_filters( 'passster_content', $content );
110 }
111 }
112 // do nothing if no atts.
113 if ( empty( $atts ) ) {
114 return $content;
115 }
116 // Set default form.
117 $form = PS_Form::get_password_form();
118 // Password.
119 if ( !empty( $atts['password'] ) ) {
120 $form = PS_Form::get_password_form();
121 $form = str_replace( '[PASSSTER_TYPE]', 'password', $form );
122 }
123 // Area.
124 if ( !empty( $atts['area'] ) ) {
125 $area_id = absint( $atts['area'] );
126 $form = str_replace( '[PASSSTER_AREA]', $area_id, $form );
127 }
128 // Page.
129 if ( !empty( $atts['protection'] ) ) {
130 $form = str_replace( '[PASSSTER_PROTECTION]', 'full', $form );
131 } elseif ( !empty( $atts['area'] ) ) {
132 $form = str_replace( '[PASSSTER_PROTECTION]', 'area', $form );
133 }
134 // Redirect.
135 if ( !empty( $atts['redirect'] ) ) {
136 $form = str_replace( '[PASSSTER_REDIRECT]', esc_url( $atts['redirect'] ), $form );
137 } else {
138 $form = str_replace( '[PASSSTER_REDIRECT]', '', $form );
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 );
154 // headline.
155 if ( !empty( $options['hide_headline'] ) ) {
156 $form = str_replace( '[PASSSTER_FORM_HEADLINE]', '', $form );
157 } elseif ( !empty( $atts['headline'] ) ) {
158 $form = str_replace( '[PASSSTER_FORM_HEADLINE]', esc_html( $atts['headline'] ), $form );
159 } else {
160 $form = str_replace( '[PASSSTER_FORM_HEADLINE]', esc_html( $options['headline'] ), $form );
161 }
162 // instruction.
163 if ( !empty( $atts['instruction'] ) ) {
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 );
168 } else {
169 $form = str_replace( '[PASSSTER_FORM_INSTRUCTIONS]', wp_kses_post( $options['instruction'] ), $form );
170 }
171 // placeholder.
172 if ( !empty( $atts['placeholder'] ) ) {
173 $form = str_replace( '[PASSSTER_PLACEHOLDER]', esc_attr( $atts['placeholder'] ), $form );
174 } else {
175 $form = str_replace( '[PASSSTER_PLACEHOLDER]', esc_attr( $options['placeholder'] ), $form );
176 }
177 // label.
178 $form = str_replace( '[PASSSTER_LABEL]', esc_html__( 'Enter your password', 'content-protector' ), $form );
179 // button.
180 if ( !empty( $atts['button'] ) ) {
181 $form = str_replace( '[PASSSTER_BUTTON_LABEL]', esc_html( $atts['button'] ), $form );
182 } else {
183 $form = str_replace( '[PASSSTER_BUTTON_LABEL]', esc_html( $options['button_label'] ), $form );
184 }
185 // modify id.
186 if ( !empty( $atts['id'] ) ) {
187 $form = str_replace( '[PASSSTER_ID]', 'ps-' . esc_attr( $atts['id'] ), $form );
188 } else {
189 $form = str_replace( '[PASSSTER_ID]', 'ps-' . wp_rand( 10, 1000 ), $form );
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 );
199 // hide or not.
200 if ( !empty( $atts['hide'] ) ) {
201 $form = str_replace( '[PASSSTER_HIDE]', ' passster-hide', $form );
202 } else {
203 $form = str_replace( '[PASSSTER_HIDE]', '', $form );
204 }
205 // ACF field.
206 if ( !empty( $atts['acf'] ) ) {
207 $form = str_replace( '[PASSSTER_ACF]', ' data-acf="' . esc_url( $atts['acf'] ) . '"', $form );
208 } else {
209 $form = str_replace( '[PASSSTER_ACF]', '', $form );
210 }
211 return $form;
212 }
213
214 /**
215 * Filters the_content with Passster.
216 *
217 * @param string $content given content.
218 *
219 * @return string
220 * @throws Exception
221 */
222 public function filter_the_content( string $content ) : string {
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 }
242 $activate_protection = get_post_meta( $post_id, 'passster_activate_protection', true );
243 // user restriction.
244 $user_restriction_type = get_post_meta( $post_id, 'passster_user_restriction_type', true );
245 $user_restriction = get_post_meta( $post_id, 'passster_user_restriction', true );
246 // Redirection.
247 $redirection = get_post_meta( $post_id, 'passster_redirect_url', true );
248 // texts.
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 ) : '' );
254 $id = get_post_meta( $post_id, 'passster_id', true );
255 if ( !$activate_protection ) {
256 return $content;
257 }
258 // build atts array to validate.
259 $atts = array();
260 $shortcode = '';
261 $password = get_post_meta( $post_id, 'passster_password', true );
262 $atts['password'] = $password;
263 $shortcode = '[passster password="' . $password . '" protection="full" ';
264 if ( !empty( $redirection ) ) {
265 $shortcode .= 'redirect="' . $redirection . '" ';
266 }
267 if ( !empty( $headline ) ) {
268 $shortcode .= 'headline="' . $headline . '" ';
269 }
270 if ( !empty( $instruction ) ) {
271 $shortcode .= 'instruction="' . base64_encode( $instruction ) . '" ';
272 }
273 if ( !empty( $placeholder ) ) {
274 $shortcode .= 'placeholder="' . $placeholder . '" ';
275 }
276 if ( !empty( $button ) ) {
277 $shortcode .= 'button="' . $button . '" ';
278 }
279 if ( !empty( $id ) ) {
280 $shortcode .= 'id="' . $id . '" ';
281 }
282 $shortcode .= ']{content}[/passster]';
283 // check if valid before restrict anything.
284 $valid = PS_Conditional::is_valid( $atts );
285 if ( $valid ) {
286 return $content;
287 }
288 // replace placeholder with content.
289 $shortcode = str_replace( '{content}', $content, $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;
297 }
298
299 /**
300 * Redirect if global protection is activated and no password is set.
301 *
302 * @return void
303 * @throws Exception
304 */
305 public function check_global_proctection() {
306 $options = get_option( 'passster' );
307 $post_id = get_queried_object_id();
308 if ( !$post_id ) {
309 return;
310 }
311 // Allow Elementor editing the page.
312 $elementor_preview = filter_input( INPUT_GET, 'elementor-preview', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
313 // Allow Live Canvas Editor.
314 $live_canvas_preview = filter_input( INPUT_GET, 'lc_action_launch_editing', FILTER_SANITIZE_FULL_SPECIAL_CHARS );
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 }
319 }
320 if ( !isset( $options['global_protection_id'] ) ) {
321 return;
322 }
323 if ( !isset( $options['activate_global_protection'] ) ) {
324 return;
325 }
326 // Build $atts array based on protection settings.
327 $post_id = esc_html( $options['global_protection_id'] );
328 $is_active = esc_html( $options['activate_global_protection'] );
329 $atts = array();
330 $password = get_post_meta( $post_id, 'passster_password', true );
331 $atts['password'] = $password;
332 if ( !empty( $post_id ) ) {
333 if ( $is_active ) {
334 if ( is_page( $post_id ) || is_single( $post_id ) ) {
335 return;
336 }
337 // Check excluded pages.
338 if ( isset( $options['exclude_pages'] ) ) {
339 foreach ( $options['exclude_pages'] as $excluded_page_id ) {
340 if ( is_page( $excluded_page_id ) ) {
341 return;
342 }
343 }
344 }
345 // Check if cookie is set.
346 $cookie = esc_html( $_COOKIE['passster'] );
347 if ( !PS_Conditional::is_valid( $atts ) ) {
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 }
353 wp_redirect( esc_url_raw( $global_protection_url ) );
354 exit;
355 }
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 );
442 }
443 }
444
445 }
446