PluginProbe
Passster – Password Protect Pages and Content / 4.3.14
Passster – Password Protect Pages and Content v4.3.14
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 4.2.16 All 47 releases
content-protector / inc / class-ps-category-lock.php

class-ps-category-lock.php in Passster – Password Protect Pages and Content 4.3.14, at inc/class-ps-category-lock.php

1,164 lines 45.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 /**
6 * Class to handle category/taxonomy protection.
7 * Adds password protection to hierarchical taxonomy terms.
8 */
9 class PS_Category_Lock {
10 /**
11 * Contains instance or null
12 *
13 * @var object|null
14 */
15 private static $instance = null;
16
17 /**
18 * Rendered form HTML to pass to the protected WooCommerce template.
19 *
20 * @var string
21 */
22 private static $woo_protected_form = '';
23
24 /**
25 * Rendered form HTML to pass to the protected regular category template.
26 *
27 * @var string
28 */
29 private static $protected_form = '';
30
31 /**
32 * Get the stored protected form HTML (used by the WooCommerce override template).
33 *
34 * @return string
35 */
36 public static function get_woo_protected_form() : string {
37 return self::$woo_protected_form;
38 }
39
40 /**
41 * Get the stored protected form HTML (used by the regular category override template).
42 *
43 * @return string
44 */
45 public static function get_protected_form() : string {
46 return self::$protected_form;
47 }
48
49 /**
50 * Returns instance of PS_Category_Lock.
51 *
52 * @return object
53 */
54 public static function get_instance() {
55 if ( null === self::$instance ) {
56 self::$instance = new self();
57 }
58 return self::$instance;
59 }
60
61 /**
62 * Constructor.
63 */
64 public function __construct() {
65 add_action( 'init', array($this, 'register_term_meta'), 99 );
66 add_action( 'init', array($this, 'register_taxonomy_hooks'), 99 );
67 // REST API endpoints for React metabox.
68 add_action( 'rest_api_init', array($this, 'rest_api_init') );
69 // Enqueue React scripts on term edit pages.
70 add_action( 'admin_enqueue_scripts', array($this, 'add_term_meta_scripts') );
71 // AJAX handler for quick edit modal.
72 add_action( 'wp_ajax_passster_quick_edit_category', array($this, 'ajax_quick_edit_category') );
73 // Frontend protection.
74 add_filter( 'the_content', array($this, 'filter_content_by_category'), 11 );
75 add_filter( 'get_the_excerpt', array($this, 'filter_content_by_category'), 11 );
76 add_action( 'template_redirect', array($this, 'protect_category_archive') );
77 }
78
79 /**
80 * Swap WooCommerce's archive template for a minimal protected-category template.
81 *
82 * @param string $template Current template path.
83 * @return string
84 */
85 public function use_protected_category_template( string $template ) : string {
86 return plugin_dir_path( __FILE__ ) . 'templates/woo-category-protected.php';
87 }
88
89 /**
90 * Swap the archive template for the protected regular-category template (classic themes only).
91 *
92 * @param string $template Current template path.
93 * @return string
94 */
95 public function use_protected_regular_category_template( string $template ) : string {
96 return plugin_dir_path( __FILE__ ) . 'templates/category-protected.php';
97 }
98
99 /**
100 * Replace the core/query block output with the password form (FSE/block themes only).
101 *
102 * @param string $block_content Rendered block HTML.
103 * @param array $block Block data.
104 * @return string
105 */
106 public function replace_query_block_with_form( string $block_content, array $block ) : string {
107 if ( 'core/query' !== $block['blockName'] ) {
108 return $block_content;
109 }
110 return self::$protected_form;
111 }
112
113 /**
114 * Get all public hierarchical taxonomies.
115 *
116 * @return array
117 */
118 private function get_taxonomies() : array {
119 return get_taxonomies( array(
120 'public' => true,
121 'hierarchical' => true,
122 ), 'names' );
123 }
124
125 /**
126 * Register term meta fields.
127 *
128 * @return void
129 */
130 public function register_term_meta() {
131 $taxonomies = $this->get_taxonomies();
132 foreach ( $taxonomies as $taxonomy ) {
133 register_term_meta( $taxonomy, 'passster_activate_protection', array(
134 'type' => 'boolean',
135 'single' => true,
136 'show_in_rest' => false,
137 'sanitize_callback' => 'rest_sanitize_boolean',
138 ) );
139 register_term_meta( $taxonomy, 'passster_password', array(
140 'type' => 'string',
141 'single' => true,
142 'show_in_rest' => false,
143 'sanitize_callback' => 'sanitize_text_field',
144 ) );
145 register_term_meta( $taxonomy, 'passster_redirect_url', array(
146 'type' => 'string',
147 'single' => true,
148 'show_in_rest' => false,
149 'sanitize_callback' => 'esc_url_raw',
150 ) );
151 register_term_meta( $taxonomy, 'passster_headline', array(
152 'type' => 'string',
153 'single' => true,
154 'show_in_rest' => false,
155 'sanitize_callback' => 'sanitize_text_field',
156 ) );
157 register_term_meta( $taxonomy, 'passster_instruction', array(
158 'type' => 'string',
159 'single' => true,
160 'show_in_rest' => false,
161 'sanitize_callback' => 'wp_kses_post',
162 ) );
163 register_term_meta( $taxonomy, 'passster_placeholder', array(
164 'type' => 'string',
165 'single' => true,
166 'show_in_rest' => false,
167 'sanitize_callback' => 'sanitize_text_field',
168 ) );
169 register_term_meta( $taxonomy, 'passster_button', array(
170 'type' => 'string',
171 'single' => true,
172 'show_in_rest' => false,
173 'sanitize_callback' => 'sanitize_text_field',
174 ) );
175 register_term_meta( $taxonomy, 'passster_activate_overwrite_defaults', array(
176 'type' => 'boolean',
177 'single' => true,
178 'show_in_rest' => false,
179 'sanitize_callback' => 'rest_sanitize_boolean',
180 ) );
181 register_term_meta( $taxonomy, 'passster_activate_misc_settings', array(
182 'type' => 'boolean',
183 'single' => true,
184 'show_in_rest' => false,
185 'sanitize_callback' => 'rest_sanitize_boolean',
186 ) );
187 }
188 }
189
190 /**
191 * Register hooks for each taxonomy (form fields, columns, row actions).
192 *
193 * @return void
194 */
195 public function register_taxonomy_hooks() {
196 $taxonomies = $this->get_taxonomies();
197 foreach ( $taxonomies as $taxonomy ) {
198 // Add/Edit form fields.
199 add_action( "{$taxonomy}_add_form_fields", array($this, 'add_form_fields') );
200 add_action(
201 "{$taxonomy}_edit_form_fields",
202 array($this, 'edit_form_fields'),
203 10,
204 2
205 );
206 // Save term meta (only on create; edit uses React + REST API).
207 add_action( "created_{$taxonomy}", array($this, 'save_term_meta') );
208 // Admin columns.
209 add_filter( "manage_edit-{$taxonomy}_columns", array($this, 'add_columns') );
210 add_filter(
211 "manage_{$taxonomy}_custom_column",
212 array($this, 'render_column'),
213 10,
214 3
215 );
216 // Row actions.
217 add_filter(
218 "{$taxonomy}_row_actions",
219 array($this, 'add_row_actions'),
220 10,
221 2
222 );
223 }
224 // Quick edit modal UI.
225 add_action( 'admin_footer-edit-tags.php', array($this, 'quick_edit_ui') );
226 }
227
228 // -------------------------------------------------------------------------
229 // Add Form Fields (Add New Term screen)
230 // -------------------------------------------------------------------------
231 /**
232 * Render React container on the "Add New" term form.
233 * The actual UI is rendered by TermMetabox.jsx.
234 * Hidden inputs are managed by React to submit data with the form.
235 *
236 * @param string $taxonomy Current taxonomy slug.
237 * @return void
238 */
239 public function add_form_fields( $taxonomy ) {
240 ?>
241 <div class="form-field passster-term-fields">
242 <div id="passster-term-metabox-add"></div>
243 </div>
244 <?php
245 }
246
247 // -------------------------------------------------------------------------
248 // Edit Form Fields (Edit Term screen) — React-based UI
249 // -------------------------------------------------------------------------
250 /**
251 * Render React container on the "Edit" term form.
252 * The actual UI is rendered by TermMetabox.jsx via the React app.
253 *
254 * @param \WP_Term $term Current term object.
255 * @param string $taxonomy Current taxonomy slug.
256 * @return void
257 */
258 public function edit_form_fields( $term, $taxonomy ) {
259 ?>
260 <tr class="form-field">
261 <th scope="row" colspan="2">
262 <h3 style="margin:0;"><?php
263 esc_html_e( 'Passster Protection', 'content-protector' );
264 ?></h3>
265 </th>
266 </tr>
267 <tr class="form-field">
268 <td colspan="2">
269 <div id="passster-term-metabox"></div>
270 </td>
271 </tr>
272 <?php
273 }
274
275 // -------------------------------------------------------------------------
276 // Enqueue React Scripts for Term Edit
277 // -------------------------------------------------------------------------
278 /**
279 * Enqueue React scripts on term edit pages.
280 *
281 * @return void
282 */
283 public function add_term_meta_scripts() {
284 $screen = get_current_screen();
285 if ( !$screen || !in_array( $screen->base, array('term', 'edit-tags'), true ) ) {
286 return;
287 }
288 $taxonomies = $this->get_taxonomies();
289 if ( !in_array( $screen->taxonomy, $taxonomies, true ) ) {
290 return;
291 }
292 wp_enqueue_script(
293 'passster-term-meta-settings',
294 PASSSTER_URL . '/inc/admin/build/index.js',
295 array(
296 'wp-api',
297 'wp-components',
298 'wp-element',
299 'wp-api-fetch',
300 'wp-data',
301 'wp-i18n'
302 ),
303 PASSSTER_VERSION,
304 true
305 );
306 // On edit (term) page, get the term ID. On add new (edit-tags) page, term_id is 0.
307 if ( 'term' === $screen->base ) {
308 $term_id = ( isset( $_GET['tag_ID'] ) ? absint( $_GET['tag_ID'] ) : 0 );
309 } else {
310 $term_id = 0;
311 }
312 $options = get_option( 'passster' );
313 $meta = array(
314 'passster_activate_protection' => get_term_meta( $term_id, 'passster_activate_protection', true ),
315 'passster_protection_type' => get_term_meta( $term_id, 'passster_protection_type', true ),
316 'passster_password' => get_term_meta( $term_id, 'passster_password', true ),
317 'passster_headline' => get_term_meta( $term_id, 'passster_headline', true ),
318 'passster_instruction' => get_term_meta( $term_id, 'passster_instruction', true ),
319 'passster_placeholder' => get_term_meta( $term_id, 'passster_placeholder', true ),
320 'passster_button' => get_term_meta( $term_id, 'passster_button', true ),
321 'passster_redirect_url' => get_term_meta( $term_id, 'passster_redirect_url', true ),
322 'passster_activate_overwrite_defaults' => get_term_meta( $term_id, 'passster_activate_overwrite_defaults', true ),
323 'passster_activate_misc_settings' => get_term_meta( $term_id, 'passster_activate_misc_settings', true ),
324 );
325 $args = array(
326 'screen' => 'term-meta',
327 'is_pro' => \passster_fs()->is_plan_or_trial__premium_only( 'pro' ),
328 'meta' => $meta,
329 'term_id' => $term_id,
330 );
331 if ( isset( $options['password_length'] ) ) {
332 $args['password_length'] = esc_html( $options['password_length'] );
333 } else {
334 $args['password_length'] = 12;
335 }
336 if ( isset( $options['include_uppercase'] ) ) {
337 $args['include_uppercase'] = esc_html( $options['include_uppercase'] );
338 } else {
339 $args['include_uppercase'] = true;
340 }
341 if ( isset( $options['include_numbers'] ) ) {
342 $args['include_numbers'] = esc_html( $options['include_numbers'] );
343 } else {
344 $args['include_numbers'] = true;
345 }
346 if ( isset( $options['include_symbols'] ) ) {
347 $args['include_symbols'] = esc_html( $options['include_symbols'] );
348 } else {
349 $args['include_symbols'] = true;
350 }
351 wp_localize_script( 'passster-term-meta-settings', 'options', $args );
352 if ( function_exists( 'wp_set_script_translations' ) ) {
353 wp_set_script_translations( 'passster-term-meta-settings', 'content-protector', PASSSTER_PATH . '/languages' );
354 }
355 wp_enqueue_style( 'passster-term-meta-style', PASSSTER_URL . '/inc/admin/build/index.css', array('wp-components') );
356 }
357
358 // -------------------------------------------------------------------------
359 // REST API Endpoints for Term Meta
360 // -------------------------------------------------------------------------
361 /**
362 * Register REST API routes for term meta.
363 *
364 * @return void
365 */
366 public function rest_api_init() {
367 register_rest_route( 'passster/v1', '/term-meta', array(
368 'methods' => 'POST',
369 'callback' => array($this, 'save_term_meta_rest'),
370 'permission_callback' => function () {
371 return current_user_can( apply_filters( 'passster_user_capability', 'manage_options' ) );
372 },
373 ) );
374 register_rest_route( 'passster/v1', '/term-meta', array(
375 'methods' => 'GET',
376 'callback' => array($this, 'get_term_meta_rest'),
377 'permission_callback' => function () {
378 return current_user_can( apply_filters( 'passster_user_capability', 'manage_options' ) );
379 },
380 ) );
381 register_rest_route( 'passster/v1', '/term-password-lists', array(
382 'methods' => 'GET',
383 'callback' => array($this, 'get_term_password_lists_rest'),
384 'permission_callback' => function () {
385 return current_user_can( apply_filters( 'passster_user_capability', 'manage_options' ) );
386 },
387 ) );
388 }
389
390 /**
391 * Save term meta via REST API.
392 *
393 * @param \WP_REST_Request $request Request object.
394 * @return string
395 */
396 public function save_term_meta_rest( \WP_REST_Request $request ) {
397 $params = $request->get_params();
398 $term_id = ( isset( $params['term_id'] ) ? absint( $params['term_id'] ) : 0 );
399 $meta_key = ( isset( $params['meta_key'] ) ? sanitize_key( $params['meta_key'] ) : '' );
400 if ( !$term_id || !$meta_key ) {
401 return wp_json_encode( array(
402 'status' => 400,
403 'message' => 'Missing term_id or meta_key',
404 ) );
405 }
406 if ( isset( $params['meta_value'] ) ) {
407 if ( 'passster_instruction' === $meta_key ) {
408 $meta_value = wp_kses_post( $params['meta_value'] );
409 } else {
410 $meta_value = sanitize_text_field( $params['meta_value'] );
411 }
412 update_term_meta( $term_id, $meta_key, $meta_value );
413 } elseif ( isset( $params['delete'] ) ) {
414 delete_term_meta( $term_id, $meta_key );
415 }
416 return wp_json_encode( array(
417 'status' => 200,
418 'message' => 'Ok',
419 ) );
420 }
421
422 /**
423 * Get term meta via REST API.
424 *
425 * @param \WP_REST_Request $request Request object.
426 * @return string
427 */
428 public function get_term_meta_rest( \WP_REST_Request $request ) {
429 $params = $request->get_params();
430 $term_id = ( isset( $params['term_id'] ) ? absint( $params['term_id'] ) : 0 );
431 $meta_key = ( isset( $params['meta_key'] ) ? sanitize_key( $params['meta_key'] ) : '' );
432 if ( !$term_id || !$meta_key ) {
433 return wp_json_encode( array(
434 'status' => 400,
435 'message' => 'Missing term_id or meta_key',
436 'data' => '',
437 ) );
438 }
439 $meta = get_term_meta( $term_id, $meta_key, true );
440 if ( !empty( $meta ) ) {
441 return wp_json_encode( array(
442 'status' => 200,
443 'message' => 'Ok',
444 'data' => $meta,
445 ) );
446 }
447 return wp_json_encode( array(
448 'status' => 400,
449 'message' => 'Empty value',
450 'data' => '',
451 ) );
452 }
453
454 /**
455 * Get password lists for term meta dropdowns via REST API.
456 *
457 * @return array
458 */
459 public function get_term_password_lists_rest() {
460 $lists = $this->get_password_lists();
461 $fetched_lists = array();
462 foreach ( $lists as $list ) {
463 $item = new \stdClass();
464 $item->title = $list->post_title;
465 $item->id = $list->ID;
466 $fetched_lists[] = $item;
467 }
468 return $fetched_lists;
469 }
470
471 // -------------------------------------------------------------------------
472 // Save Term Meta
473 // -------------------------------------------------------------------------
474 /**
475 * Save term meta on create/edit.
476 *
477 * @param int $term_id Term ID.
478 * @return void
479 */
480 public function save_term_meta( $term_id ) {
481 if ( !current_user_can( 'manage_categories' ) ) {
482 return;
483 }
484 $activate = ( !empty( $_POST['passster_activate_protection'] ) ? 1 : 0 );
485 update_term_meta( $term_id, 'passster_activate_protection', $activate );
486 if ( $activate ) {
487 if ( isset( $_POST['passster_password'] ) ) {
488 update_term_meta( $term_id, 'passster_password', sanitize_text_field( wp_unslash( $_POST['passster_password'] ) ) );
489 }
490 if ( isset( $_POST['passster_redirect_url'] ) ) {
491 update_term_meta( $term_id, 'passster_redirect_url', esc_url_raw( wp_unslash( $_POST['passster_redirect_url'] ) ) );
492 }
493 // Overwrite defaults.
494 if ( isset( $_POST['passster_headline'] ) ) {
495 update_term_meta( $term_id, 'passster_headline', sanitize_text_field( wp_unslash( $_POST['passster_headline'] ) ) );
496 }
497 if ( isset( $_POST['passster_instruction'] ) ) {
498 update_term_meta( $term_id, 'passster_instruction', wp_kses_post( wp_unslash( $_POST['passster_instruction'] ) ) );
499 }
500 if ( isset( $_POST['passster_placeholder'] ) ) {
501 update_term_meta( $term_id, 'passster_placeholder', sanitize_text_field( wp_unslash( $_POST['passster_placeholder'] ) ) );
502 }
503 if ( isset( $_POST['passster_button'] ) ) {
504 update_term_meta( $term_id, 'passster_button', sanitize_text_field( wp_unslash( $_POST['passster_button'] ) ) );
505 }
506 }
507 }
508
509 // -------------------------------------------------------------------------
510 // Admin Columns
511 // -------------------------------------------------------------------------
512 /**
513 * Add columns to taxonomy list table.
514 *
515 * @param array $columns Existing columns.
516 * @return array
517 */
518 public function add_columns( array $columns ) : array {
519 $columns['passster_password'] = __( 'Password', 'content-protector' );
520 return $columns;
521 }
522
523 /**
524 * Render column content for taxonomy list table.
525 *
526 * @param string $content Column content.
527 * @param string $column_name Column name.
528 * @param int $term_id Term ID.
529 * @return string
530 */
531 public function render_column( $content, $column_name, $term_id ) {
532 $activate = get_term_meta( $term_id, 'passster_activate_protection', true );
533 if ( !$activate ) {
534 if ( 'passster_password' === $column_name || 'passster_protection_type' === $column_name ) {
535 return '-';
536 }
537 return $content;
538 }
539 $protection_type = ( get_term_meta( $term_id, 'passster_protection_type', true ) ?: 'password' );
540 switch ( $column_name ) {
541 case 'passster_password':
542 return esc_html( get_term_meta( $term_id, 'passster_password', true ) );
543 break;
544 case 'passster_protection_type':
545 $types = array(
546 'password' => __( 'Password', 'content-protector' ),
547 'passwords' => __( 'Passwords', 'content-protector' ),
548 'password_list' => __( 'Password List', 'content-protector' ),
549 'password_lists' => __( 'Password Lists', 'content-protector' ),
550 'recaptcha' => __( 'reCaptcha', 'content-protector' ),
551 'turnstile' => __( 'Turnstile', 'content-protector' ),
552 );
553 return esc_html( $types[$protection_type] ?? $protection_type );
554 }
555 return $content;
556 }
557
558 // -------------------------------------------------------------------------
559 // Row Actions
560 // -------------------------------------------------------------------------
561 /**
562 * Add Quick Settings link to term row actions.
563 *
564 * @param array $actions Existing actions.
565 * @param \WP_Term $term Term object.
566 * @return array
567 */
568 public function add_row_actions( $actions, $term ) {
569 if ( current_user_can( 'manage_categories' ) ) {
570 $actions['passster_quick_settings'] = sprintf(
571 '<a href="#" class="passster-category-quick-settings" data-term-id="%d" data-nonce="%s">%s</a>',
572 $term->term_id,
573 wp_create_nonce( 'passster_quick_edit_cat_' . $term->term_id ),
574 __( 'Quick Settings', 'content-protector' )
575 );
576 }
577 return $actions;
578 }
579
580 // -------------------------------------------------------------------------
581 // Quick Edit Modal UI
582 // -------------------------------------------------------------------------
583 /**
584 * Output quick edit modal HTML, CSS and JS.
585 *
586 * @return void
587 */
588 public function quick_edit_ui() {
589 $screen = get_current_screen();
590 if ( !$screen ) {
591 return;
592 }
593 $taxonomies = $this->get_taxonomies();
594 if ( !in_array( $screen->taxonomy, $taxonomies, true ) ) {
595 return;
596 }
597 $is_pro = \passster_fs()->is_plan_or_trial__premium_only( 'pro' );
598 $lists = ( $is_pro ? $this->get_password_lists() : array() );
599 ?>
600 <div id="passster-cat-quick-edit-modal" style="display:none;">
601 <div class="passster-cat-qe-backdrop"></div>
602 <div class="passster-cat-qe-panel">
603 <div class="passster-cat-qe-header">
604 <h3><?php
605 esc_html_e( 'Passster - Quick Settings', 'content-protector' );
606 ?></h3>
607 <button type="button" class="passster-cat-qe-close">&times;</button>
608 </div>
609 <div class="passster-cat-qe-body">
610 <input type="hidden" id="passster-cat-qe-term-id" value="">
611 <input type="hidden" id="passster-cat-qe-nonce" value="">
612
613 <p class="passster-cat-qe-field">
614 <label>
615 <input type="checkbox" id="passster-cat-qe-activate">
616 <?php
617 esc_html_e( 'Activate Protection', 'content-protector' );
618 ?>
619 </label>
620 </p>
621
622 <div class="passster-cat-qe-settings" style="display:none;">
623 <?php
624 if ( $is_pro ) {
625 ?>
626 <p class="passster-cat-qe-field">
627 <label for="passster-cat-qe-type"><?php
628 esc_html_e( 'Protection Type', 'content-protector' );
629 ?></label>
630 <select id="passster-cat-qe-type">
631 <option value="password"><?php
632 esc_html_e( 'Password', 'content-protector' );
633 ?></option>
634 <option value="passwords"><?php
635 esc_html_e( 'Passwords', 'content-protector' );
636 ?></option>
637 <option value="password_list"><?php
638 esc_html_e( 'Password List', 'content-protector' );
639 ?></option>
640 <option value="password_lists"><?php
641 esc_html_e( 'Password Lists', 'content-protector' );
642 ?></option>
643 <option value="recaptcha">reCAPTCHA</option>
644 <option value="turnstile">Turnstile</option>
645 </select>
646 </p>
647 <?php
648 }
649 ?>
650
651 <p class="passster-cat-qe-field passster-cat-qe-password-field">
652 <label for="passster-cat-qe-password"><?php
653 esc_html_e( 'Password', 'content-protector' );
654 ?></label>
655 <input type="text" id="passster-cat-qe-password" value="">
656 </p>
657
658 <?php
659 if ( $is_pro ) {
660 ?>
661 <p class="passster-cat-qe-field passster-cat-qe-passwords-field" style="display:none;">
662 <label for="passster-cat-qe-passwords"><?php
663 esc_html_e( 'Passwords (comma-separated)', 'content-protector' );
664 ?></label>
665 <input type="text" id="passster-cat-qe-passwords" value="">
666 </p>
667
668 <p class="passster-cat-qe-field passster-cat-qe-list-field" style="display:none;">
669 <label for="passster-cat-qe-list"><?php
670 esc_html_e( 'Password List', 'content-protector' );
671 ?></label>
672 <select id="passster-cat-qe-list">
673 <option value="0"><?php
674 esc_html_e( '— Select —', 'content-protector' );
675 ?></option>
676 <?php
677 foreach ( $lists as $list ) {
678 ?>
679 <option value="<?php
680 echo esc_attr( $list->ID );
681 ?>"><?php
682 echo esc_html( $list->post_title );
683 ?></option>
684 <?php
685 }
686 ?>
687 </select>
688 </p>
689
690 <p class="passster-cat-qe-field passster-cat-qe-lists-field" style="display:none;">
691 <label for="passster-cat-qe-lists"><?php
692 esc_html_e( 'Password Lists (comma-separated IDs)', 'content-protector' );
693 ?></label>
694 <input type="text" id="passster-cat-qe-lists" value="">
695 </p>
696 <?php
697 }
698 ?>
699 </div>
700 </div>
701 <div class="passster-cat-qe-footer">
702 <button type="button" class="button passster-cat-qe-cancel"><?php
703 esc_html_e( 'Cancel', 'content-protector' );
704 ?></button>
705 <button type="button" class="button button-primary passster-cat-qe-save"><?php
706 esc_html_e( 'Save', 'content-protector' );
707 ?></button>
708 <span class="spinner"></span>
709 </div>
710 </div>
711 </div>
712
713 <style>
714 .passster-cat-qe-backdrop {
715 position: fixed; top: 0; left: 0; right: 0; bottom: 0;
716 background: rgba(0,0,0,0.5); z-index: 100000;
717 }
718 .passster-cat-qe-panel {
719 position: fixed; top: 50%; left: 50%;
720 transform: translate(-50%,-50%);
721 background: #fff; border-radius: 8px;
722 box-shadow: 0 4px 20px rgba(0,0,0,0.2);
723 width: 420px; max-width: 90vw; z-index: 100001;
724 }
725 .passster-cat-qe-header {
726 display: flex; justify-content: space-between; align-items: center;
727 padding: 16px 20px; border-bottom: 1px solid #ddd;
728 }
729 .passster-cat-qe-header h3 { margin: 0; font-size: 16px; }
730 .passster-cat-qe-close {
731 background: none; border: none; font-size: 24px;
732 cursor: pointer; color: #666; padding: 0; line-height: 1;
733 }
734 .passster-cat-qe-close:hover { color: #d63638; }
735 .passster-cat-qe-body { padding: 20px; }
736 .passster-cat-qe-field { margin-bottom: 14px; }
737 .passster-cat-qe-field label { display: block; margin-bottom: 4px; font-weight: 600; }
738 .passster-cat-qe-field input[type="text"],
739 .passster-cat-qe-field input[type="url"],
740 .passster-cat-qe-field select { width: 100%; }
741 .passster-cat-qe-footer {
742 display: flex; justify-content: flex-end; align-items: center;
743 gap: 8px; padding: 16px 20px;
744 border-top: 1px solid #ddd; background: #f6f7f7;
745 border-radius: 0 0 8px 8px;
746 }
747 .passster-cat-qe-footer .spinner { float: none; margin: 0; }
748 </style>
749
750 <script>
751 jQuery(function($) {
752 var $modal = $('#passster-cat-quick-edit-modal');
753 var $activate = $('#passster-cat-qe-activate');
754 var $typeSelect = $('#passster-cat-qe-type');
755 var isPro = <?php
756 echo ( $is_pro ? 'true' : 'false' );
757 ?>;
758
759 function toggleTypeFields() {
760 if (!isPro) return;
761 var type = $typeSelect.val();
762 $('.passster-cat-qe-password-field').toggle(type === 'password');
763 $('.passster-cat-qe-passwords-field').toggle(type === 'passwords');
764 $('.passster-cat-qe-list-field').toggle(type === 'password_list');
765 $('.passster-cat-qe-lists-field').toggle(type === 'password_lists');
766 }
767
768 $activate.on('change', function() {
769 $('.passster-cat-qe-settings').toggle(this.checked);
770 });
771
772 if (isPro) {
773 $typeSelect.on('change', toggleTypeFields);
774 }
775
776 // Open modal.
777 $(document).on('click', '.passster-category-quick-settings', function(e) {
778 e.preventDefault();
779 var termId = $(this).data('term-id');
780 var nonce = $(this).data('nonce');
781
782 $('#passster-cat-qe-term-id').val(termId);
783 $('#passster-cat-qe-nonce').val(nonce);
784
785 $.post(ajaxurl, {
786 action: 'passster_quick_edit_category',
787 term_id: termId,
788 nonce: nonce,
789 edit_action: 'get'
790 }, function(response) {
791 if (response.success) {
792 var data = response.data;
793 $activate.prop('checked', !!data.activate);
794 $('.passster-cat-qe-settings').toggle(!!data.activate);
795 $('#passster-cat-qe-password').val(data.password || '');
796
797 if (isPro) {
798 $typeSelect.val(data.protection_type || 'password');
799 $('#passster-cat-qe-passwords').val(data.passwords || '');
800 $('#passster-cat-qe-list').val(data.password_list || 0);
801 $('#passster-cat-qe-lists').val(data.password_lists || '');
802 toggleTypeFields();
803 }
804
805 $modal.show();
806 }
807 });
808 });
809
810 // Close modal.
811 $modal.on('click', '.passster-cat-qe-close, .passster-cat-qe-cancel, .passster-cat-qe-backdrop', function() {
812 $modal.hide();
813 });
814
815 // Save.
816 $modal.on('click', '.passster-cat-qe-save', function() {
817 var $btn = $(this);
818 var $spinner = $modal.find('.spinner');
819
820 $btn.prop('disabled', true);
821 $spinner.addClass('is-active');
822
823 var postData = {
824 action: 'passster_quick_edit_category',
825 term_id: $('#passster-cat-qe-term-id').val(),
826 nonce: $('#passster-cat-qe-nonce').val(),
827 edit_action: 'save',
828 activate: $activate.is(':checked') ? 1 : 0,
829 password: $('#passster-cat-qe-password').val()
830 };
831
832 if (isPro) {
833 postData.protection_type = $typeSelect.val();
834 postData.passwords = $('#passster-cat-qe-passwords').val();
835 postData.password_list = $('#passster-cat-qe-list').val();
836 postData.password_lists = $('#passster-cat-qe-lists').val();
837 }
838
839 $.post(ajaxurl, postData, function(response) {
840 $btn.prop('disabled', false);
841 $spinner.removeClass('is-active');
842
843 if (response.success) {
844 $modal.hide();
845 location.reload();
846 } else {
847 alert(response.data.message || 'Error saving settings.');
848 }
849 });
850 });
851
852 // ESC to close.
853 $(document).on('keydown', function(e) {
854 if (e.key === 'Escape' && $modal.is(':visible')) {
855 $modal.hide();
856 }
857 });
858 });
859 </script>
860
861 <?php
862 }
863
864 // -------------------------------------------------------------------------
865 // AJAX Quick Edit Handler
866 // -------------------------------------------------------------------------
867 /**
868 * AJAX handler for quick edit category settings.
869 *
870 * @return void
871 */
872 public function ajax_quick_edit_category() {
873 $term_id = ( isset( $_POST['term_id'] ) ? absint( $_POST['term_id'] ) : 0 );
874 $nonce = ( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '' );
875 $action = ( isset( $_POST['edit_action'] ) ? sanitize_text_field( wp_unslash( $_POST['edit_action'] ) ) : 'get' );
876 if ( !wp_verify_nonce( $nonce, 'passster_quick_edit_cat_' . $term_id ) ) {
877 wp_send_json_error( array(
878 'message' => __( 'Security check failed.', 'content-protector' ),
879 ) );
880 }
881 if ( !current_user_can( 'manage_categories' ) ) {
882 wp_send_json_error( array(
883 'message' => __( 'Permission denied.', 'content-protector' ),
884 ) );
885 }
886 $term = get_term( $term_id );
887 if ( !$term || is_wp_error( $term ) ) {
888 wp_send_json_error( array(
889 'message' => __( 'Invalid term.', 'content-protector' ),
890 ) );
891 }
892 if ( 'get' === $action ) {
893 $data = array(
894 'term_id' => $term_id,
895 'activate' => (bool) get_term_meta( $term_id, 'passster_activate_protection', true ),
896 'password' => get_term_meta( $term_id, 'passster_password', true ),
897 'protection_type' => ( get_term_meta( $term_id, 'passster_protection_type', true ) ?: 'password' ),
898 'passwords' => get_term_meta( $term_id, 'passster_passwords', true ),
899 'password_list' => get_term_meta( $term_id, 'passster_password_list', true ),
900 'password_lists' => get_term_meta( $term_id, 'passster_password_lists', true ),
901 );
902 wp_send_json_success( $data );
903 }
904 if ( 'save' === $action ) {
905 $activate = ( isset( $_POST['activate'] ) ? absint( $_POST['activate'] ) : 0 );
906 update_term_meta( $term_id, 'passster_activate_protection', $activate );
907 if ( $activate ) {
908 $password = ( isset( $_POST['password'] ) ? sanitize_text_field( wp_unslash( $_POST['password'] ) ) : '' );
909 update_term_meta( $term_id, 'passster_password', $password );
910 }
911 wp_send_json_success( array(
912 'message' => __( 'Settings saved.', 'content-protector' ),
913 ) );
914 }
915 wp_send_json_error( array(
916 'message' => __( 'Invalid action.', 'content-protector' ),
917 ) );
918 }
919
920 // -------------------------------------------------------------------------
921 // Frontend: Protect Posts in Protected Categories
922 // -------------------------------------------------------------------------
923 /**
924 * Filter the_content for posts belonging to a protected category.
925 * Runs at priority 11, after PS_Public (priority 10).
926 *
927 * @param string $content Post content.
928 * @return string
929 */
930 public function filter_content_by_category( $content ) {
931 if ( is_admin() ) {
932 return $content;
933 }
934 if ( is_category() || is_tax() ) {
935 return $content;
936 }
937 $post_id = get_the_id();
938 if ( !$post_id ) {
939 return $content;
940 }
941 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
942 return $content;
943 }
944 $term_data = $this->get_active_category_lock( $post_id );
945 if ( !$term_data ) {
946 return $content;
947 }
948 $shortcode = $this->build_shortcode_from_term( $term_data['term_id'], $content );
949 return do_shortcode( $shortcode );
950 }
951
952 /**
953 * @param int $post_id Post ID.
954 * @return array|null
955 */
956 public function get_active_category_lock( int $post_id ) {
957 $post_protection = get_post_meta( $post_id, 'passster_activate_protection', true );
958 if ( $post_protection ) {
959 return null;
960 }
961 $term_data = $this->get_protected_term_for_post( $post_id );
962 if ( !$term_data ) {
963 return null;
964 }
965 $atts = $this->build_atts_from_term( $term_data['term_id'] );
966 if ( PS_Conditional::is_valid( $atts ) ) {
967 return null;
968 }
969 return $term_data;
970 }
971
972 /**
973 * Protect category archive pages.
974 *
975 * @return void
976 */
977 public function protect_category_archive() {
978 if ( is_admin() ) {
979 return;
980 }
981 // WooCommerce single product: if the product belongs to a locked product_cat,
982 // suppress all WooCommerce product hooks and show the password form inline.
983 if ( class_exists( 'WooCommerce' ) && is_singular( 'product' ) ) {
984 $post_id = get_the_ID();
985 $term_data = ( $post_id ? $this->get_protected_term_for_post( $post_id ) : null );
986 if ( $term_data && 'product_cat' === $term_data['taxonomy'] ) {
987 $atts = $this->build_atts_from_term( $term_data['term_id'] );
988 if ( !PS_Conditional::is_valid( $atts ) ) {
989 $shortcode = $this->build_shortcode_from_term( $term_data['term_id'], '' );
990 self::$woo_protected_form = do_shortcode( $shortcode );
991 // Replace the entire single-product template with the protected form.
992 add_filter( 'template_include', array($this, 'use_protected_category_template'), 99 );
993 }
994 }
995 return;
996 }
997 if ( !is_category() && !is_tax() ) {
998 return;
999 }
1000 $term = get_queried_object();
1001 if ( !$term || !is_a( $term, 'WP_Term' ) ) {
1002 return;
1003 }
1004 // Only handle hierarchical taxonomies.
1005 $taxonomy_obj = get_taxonomy( $term->taxonomy );
1006 if ( !$taxonomy_obj || !$taxonomy_obj->hierarchical || !$taxonomy_obj->public ) {
1007 return;
1008 }
1009 // Check the term itself and then its ancestors for protection.
1010 $protected_term_id = null;
1011 $ids_to_check = array_merge( array($term->term_id), get_ancestors( $term->term_id, $term->taxonomy, 'taxonomy' ) );
1012 foreach ( $ids_to_check as $check_id ) {
1013 if ( get_term_meta( $check_id, 'passster_activate_protection', true ) ) {
1014 $protected_term_id = $check_id;
1015 break;
1016 }
1017 }
1018 if ( !$protected_term_id ) {
1019 return;
1020 }
1021 // Use the protected term (may be an ancestor) for all subsequent meta lookups.
1022 $term = get_term( $protected_term_id );
1023 if ( !$term || is_wp_error( $term ) ) {
1024 return;
1025 }
1026 $atts = $this->build_atts_from_term( $term->term_id );
1027 if ( PS_Conditional::is_valid( $atts ) ) {
1028 return;
1029 }
1030 // Check for redirect.
1031 $redirect = get_term_meta( $term->term_id, 'passster_redirect_url', true );
1032 if ( !empty( $redirect ) ) {
1033 wp_redirect( esc_url_raw( $redirect ) );
1034 exit;
1035 }
1036 $shortcode = $this->build_shortcode_from_term( $term->term_id, '' );
1037 $form_html = do_shortcode( $shortcode );
1038 // WooCommerce product category archives use their own template system and never
1039 // call the_content() on the main query posts, so the virtual-post trick doesn't work.
1040 // Instead, inject the form via WooCommerce's own template hooks and suppress the product loop.
1041 if ( class_exists( 'WooCommerce' ) && 'product_cat' === $term->taxonomy ) {
1042 self::$woo_protected_form = $form_html;
1043 // Replace WooCommerce's entire archive template with a minimal one that shows only the form.
1044 add_filter( 'template_include', array($this, 'use_protected_category_template'), 99 );
1045 return;
1046 }
1047 self::$protected_form = $form_html;
1048 if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
1049 // FSE/block themes: let the block template render normally; replace only the
1050 // core/query block (the post loop) with the password form. This preserves the
1051 // theme's own header/footer block templates.
1052 add_filter(
1053 'render_block',
1054 array($this, 'replace_query_block_with_form'),
1055 10,
1056 2
1057 );
1058 } else {
1059 // Classic themes: swap the archive template for a minimal protected-category
1060 // template that calls get_header()/get_footer() from the active theme.
1061 add_filter( 'template_include', array($this, 'use_protected_regular_category_template'), 99 );
1062 }
1063 }
1064
1065 // -------------------------------------------------------------------------
1066 // Helper Methods
1067 // -------------------------------------------------------------------------
1068 /**
1069 * Get the first protected term for a given post.
1070 *
1071 * @param int $post_id Post ID.
1072 * @return array|null Array with 'term_id' and 'taxonomy', or null.
1073 */
1074 public function get_protected_term_for_post( int $post_id ) {
1075 $taxonomies = $this->get_taxonomies();
1076 foreach ( $taxonomies as $taxonomy ) {
1077 $terms = get_the_terms( $post_id, $taxonomy );
1078 if ( !$terms || is_wp_error( $terms ) ) {
1079 continue;
1080 }
1081 foreach ( $terms as $term ) {
1082 // Check the term itself, then walk up its ancestor chain.
1083 $ids_to_check = array_merge( array($term->term_id), get_ancestors( $term->term_id, $taxonomy, 'taxonomy' ) );
1084 foreach ( $ids_to_check as $check_id ) {
1085 $activate = get_term_meta( $check_id, 'passster_activate_protection', true );
1086 if ( $activate ) {
1087 return array(
1088 'term_id' => $check_id,
1089 'taxonomy' => $taxonomy,
1090 );
1091 }
1092 }
1093 }
1094 }
1095 return null;
1096 }
1097
1098 /**
1099 * Build $atts array from term meta (for PS_Conditional::is_valid).
1100 *
1101 * @param int $term_id Term ID.
1102 * @return array
1103 */
1104 private function build_atts_from_term( int $term_id ) : array {
1105 $atts = array();
1106 $atts['password'] = get_term_meta( $term_id, 'passster_password', true );
1107 return $atts;
1108 }
1109
1110 /**
1111 * Build a passster shortcode string from term meta.
1112 *
1113 * @param int $term_id Term ID.
1114 * @param string $content Content to wrap.
1115 * @return string
1116 */
1117 private function build_shortcode_from_term( int $term_id, string $content ) : string {
1118 $options = get_option( 'passster' );
1119 $shortcode = '[passster ';
1120 $password = get_term_meta( $term_id, 'passster_password', true );
1121 $shortcode .= 'password="' . esc_attr( $password ) . '" ';
1122 $shortcode .= 'protection="full" ';
1123 $shortcode .= 'term_id="' . $term_id . '" ';
1124 // Redirect.
1125 $redirect = get_term_meta( $term_id, 'passster_redirect_url', true );
1126 if ( !empty( $redirect ) ) {
1127 $shortcode .= 'redirect="' . esc_url( $redirect ) . '" ';
1128 }
1129 // Overwrite defaults.
1130 $headline = get_term_meta( $term_id, 'passster_headline', true );
1131 if ( !empty( $headline ) ) {
1132 $shortcode .= 'headline="' . esc_attr( $headline ) . '" ';
1133 }
1134 $instruction = get_term_meta( $term_id, 'passster_instruction', true );
1135 if ( !empty( $instruction ) ) {
1136 $shortcode .= 'instruction="' . base64_encode( $instruction ) . '" ';
1137 }
1138 $placeholder = get_term_meta( $term_id, 'passster_placeholder', true );
1139 if ( !empty( $placeholder ) ) {
1140 $shortcode .= 'placeholder="' . esc_attr( $placeholder ) . '" ';
1141 }
1142 $button = get_term_meta( $term_id, 'passster_button', true );
1143 if ( !empty( $button ) ) {
1144 $shortcode .= 'button="' . esc_attr( $button ) . '" ';
1145 }
1146 $shortcode .= ']' . $content . '[/passster]';
1147 return $shortcode;
1148 }
1149
1150 /**
1151 * Get published password lists.
1152 *
1153 * @return array
1154 */
1155 private function get_password_lists() : array {
1156 return get_posts( array(
1157 'post_type' => 'password_lists',
1158 'post_status' => 'publish',
1159 'posts_per_page' => -1,
1160 ) );
1161 }
1162
1163 }
1164