true,
'hierarchical' => true,
), 'names' );
}
/**
* Register term meta fields.
*
* @return void
*/
public function register_term_meta() {
$taxonomies = $this->get_taxonomies();
foreach ( $taxonomies as $taxonomy ) {
register_term_meta( $taxonomy, 'passster_activate_protection', array(
'type' => 'boolean',
'single' => true,
'show_in_rest' => false,
'sanitize_callback' => 'rest_sanitize_boolean',
) );
register_term_meta( $taxonomy, 'passster_password', array(
'type' => 'string',
'single' => true,
'show_in_rest' => false,
'sanitize_callback' => 'sanitize_text_field',
) );
register_term_meta( $taxonomy, 'passster_redirect_url', array(
'type' => 'string',
'single' => true,
'show_in_rest' => false,
'sanitize_callback' => 'esc_url_raw',
) );
register_term_meta( $taxonomy, 'passster_headline', array(
'type' => 'string',
'single' => true,
'show_in_rest' => false,
'sanitize_callback' => 'sanitize_text_field',
) );
register_term_meta( $taxonomy, 'passster_instruction', array(
'type' => 'string',
'single' => true,
'show_in_rest' => false,
'sanitize_callback' => 'wp_kses_post',
) );
register_term_meta( $taxonomy, 'passster_placeholder', array(
'type' => 'string',
'single' => true,
'show_in_rest' => false,
'sanitize_callback' => 'sanitize_text_field',
) );
register_term_meta( $taxonomy, 'passster_button', array(
'type' => 'string',
'single' => true,
'show_in_rest' => false,
'sanitize_callback' => 'sanitize_text_field',
) );
register_term_meta( $taxonomy, 'passster_activate_overwrite_defaults', array(
'type' => 'boolean',
'single' => true,
'show_in_rest' => false,
'sanitize_callback' => 'rest_sanitize_boolean',
) );
register_term_meta( $taxonomy, 'passster_activate_misc_settings', array(
'type' => 'boolean',
'single' => true,
'show_in_rest' => false,
'sanitize_callback' => 'rest_sanitize_boolean',
) );
}
}
/**
* Register hooks for each taxonomy (form fields, columns, row actions).
*
* @return void
*/
public function register_taxonomy_hooks() {
$taxonomies = $this->get_taxonomies();
foreach ( $taxonomies as $taxonomy ) {
// Add/Edit form fields.
add_action( "{$taxonomy}_add_form_fields", array($this, 'add_form_fields') );
add_action(
"{$taxonomy}_edit_form_fields",
array($this, 'edit_form_fields'),
10,
2
);
// Save term meta (only on create; edit uses React + REST API).
add_action( "created_{$taxonomy}", array($this, 'save_term_meta') );
// Admin columns.
add_filter( "manage_edit-{$taxonomy}_columns", array($this, 'add_columns') );
add_filter(
"manage_{$taxonomy}_custom_column",
array($this, 'render_column'),
10,
3
);
// Row actions.
add_filter(
"{$taxonomy}_row_actions",
array($this, 'add_row_actions'),
10,
2
);
}
// Quick edit modal UI.
add_action( 'admin_footer-edit-tags.php', array($this, 'quick_edit_ui') );
}
// -------------------------------------------------------------------------
// Add Form Fields (Add New Term screen)
// -------------------------------------------------------------------------
/**
* Render React container on the "Add New" term form.
* The actual UI is rendered by TermMetabox.jsx.
* Hidden inputs are managed by React to submit data with the form.
*
* @param string $taxonomy Current taxonomy slug.
* @return void
*/
public function add_form_fields( $taxonomy ) {
?>
|
|
|
|
base, array('term', 'edit-tags'), true ) ) {
return;
}
$taxonomies = $this->get_taxonomies();
if ( !in_array( $screen->taxonomy, $taxonomies, true ) ) {
return;
}
wp_enqueue_script(
'passster-term-meta-settings',
PASSSTER_URL . '/inc/admin/build/index.js',
array(
'wp-api',
'wp-components',
'wp-element',
'wp-api-fetch',
'wp-data',
'wp-i18n'
),
PASSSTER_VERSION,
true
);
// On edit (term) page, get the term ID. On add new (edit-tags) page, term_id is 0.
if ( 'term' === $screen->base ) {
$term_id = ( isset( $_GET['tag_ID'] ) ? absint( $_GET['tag_ID'] ) : 0 );
} else {
$term_id = 0;
}
$options = get_option( 'passster' );
$meta = array(
'passster_activate_protection' => get_term_meta( $term_id, 'passster_activate_protection', true ),
'passster_protection_type' => get_term_meta( $term_id, 'passster_protection_type', true ),
'passster_password' => get_term_meta( $term_id, 'passster_password', true ),
'passster_headline' => get_term_meta( $term_id, 'passster_headline', true ),
'passster_instruction' => get_term_meta( $term_id, 'passster_instruction', true ),
'passster_placeholder' => get_term_meta( $term_id, 'passster_placeholder', true ),
'passster_button' => get_term_meta( $term_id, 'passster_button', true ),
'passster_redirect_url' => get_term_meta( $term_id, 'passster_redirect_url', true ),
'passster_activate_overwrite_defaults' => get_term_meta( $term_id, 'passster_activate_overwrite_defaults', true ),
'passster_activate_misc_settings' => get_term_meta( $term_id, 'passster_activate_misc_settings', true ),
);
$args = array(
'screen' => 'term-meta',
'is_pro' => \passster_fs()->is_plan_or_trial__premium_only( 'pro' ),
'meta' => $meta,
'term_id' => $term_id,
);
if ( isset( $options['password_length'] ) ) {
$args['password_length'] = esc_html( $options['password_length'] );
} else {
$args['password_length'] = 12;
}
if ( isset( $options['include_uppercase'] ) ) {
$args['include_uppercase'] = esc_html( $options['include_uppercase'] );
} else {
$args['include_uppercase'] = true;
}
if ( isset( $options['include_numbers'] ) ) {
$args['include_numbers'] = esc_html( $options['include_numbers'] );
} else {
$args['include_numbers'] = true;
}
if ( isset( $options['include_symbols'] ) ) {
$args['include_symbols'] = esc_html( $options['include_symbols'] );
} else {
$args['include_symbols'] = true;
}
wp_localize_script( 'passster-term-meta-settings', 'options', $args );
if ( function_exists( 'wp_set_script_translations' ) ) {
wp_set_script_translations( 'passster-term-meta-settings', 'content-protector', PASSSTER_PATH . '/languages' );
}
wp_enqueue_style( 'passster-term-meta-style', PASSSTER_URL . '/inc/admin/build/index.css', array('wp-components') );
}
// -------------------------------------------------------------------------
// REST API Endpoints for Term Meta
// -------------------------------------------------------------------------
/**
* Register REST API routes for term meta.
*
* @return void
*/
public function rest_api_init() {
register_rest_route( 'passster/v1', '/term-meta', array(
'methods' => 'POST',
'callback' => array($this, 'save_term_meta_rest'),
'permission_callback' => function () {
return current_user_can( apply_filters( 'passster_user_capability', 'manage_options' ) );
},
) );
register_rest_route( 'passster/v1', '/term-meta', array(
'methods' => 'GET',
'callback' => array($this, 'get_term_meta_rest'),
'permission_callback' => function () {
return current_user_can( apply_filters( 'passster_user_capability', 'manage_options' ) );
},
) );
register_rest_route( 'passster/v1', '/term-password-lists', array(
'methods' => 'GET',
'callback' => array($this, 'get_term_password_lists_rest'),
'permission_callback' => function () {
return current_user_can( apply_filters( 'passster_user_capability', 'manage_options' ) );
},
) );
}
/**
* Save term meta via REST API.
*
* @param \WP_REST_Request $request Request object.
* @return string
*/
public function save_term_meta_rest( \WP_REST_Request $request ) {
$params = $request->get_params();
$term_id = ( isset( $params['term_id'] ) ? absint( $params['term_id'] ) : 0 );
$meta_key = ( isset( $params['meta_key'] ) ? sanitize_key( $params['meta_key'] ) : '' );
if ( !$term_id || !$meta_key ) {
return wp_json_encode( array(
'status' => 400,
'message' => 'Missing term_id or meta_key',
) );
}
if ( isset( $params['meta_value'] ) ) {
if ( 'passster_instruction' === $meta_key ) {
$meta_value = wp_kses_post( $params['meta_value'] );
} else {
$meta_value = sanitize_text_field( $params['meta_value'] );
}
update_term_meta( $term_id, $meta_key, $meta_value );
} elseif ( isset( $params['delete'] ) ) {
delete_term_meta( $term_id, $meta_key );
}
return wp_json_encode( array(
'status' => 200,
'message' => 'Ok',
) );
}
/**
* Get term meta via REST API.
*
* @param \WP_REST_Request $request Request object.
* @return string
*/
public function get_term_meta_rest( \WP_REST_Request $request ) {
$params = $request->get_params();
$term_id = ( isset( $params['term_id'] ) ? absint( $params['term_id'] ) : 0 );
$meta_key = ( isset( $params['meta_key'] ) ? sanitize_key( $params['meta_key'] ) : '' );
if ( !$term_id || !$meta_key ) {
return wp_json_encode( array(
'status' => 400,
'message' => 'Missing term_id or meta_key',
'data' => '',
) );
}
$meta = get_term_meta( $term_id, $meta_key, true );
if ( !empty( $meta ) ) {
return wp_json_encode( array(
'status' => 200,
'message' => 'Ok',
'data' => $meta,
) );
}
return wp_json_encode( array(
'status' => 400,
'message' => 'Empty value',
'data' => '',
) );
}
/**
* Get password lists for term meta dropdowns via REST API.
*
* @return array
*/
public function get_term_password_lists_rest() {
$lists = $this->get_password_lists();
$fetched_lists = array();
foreach ( $lists as $list ) {
$item = new \stdClass();
$item->title = $list->post_title;
$item->id = $list->ID;
$fetched_lists[] = $item;
}
return $fetched_lists;
}
// -------------------------------------------------------------------------
// Save Term Meta
// -------------------------------------------------------------------------
/**
* Save term meta on create/edit.
*
* @param int $term_id Term ID.
* @return void
*/
public function save_term_meta( $term_id ) {
if ( !current_user_can( 'manage_categories' ) ) {
return;
}
$activate = ( !empty( $_POST['passster_activate_protection'] ) ? 1 : 0 );
update_term_meta( $term_id, 'passster_activate_protection', $activate );
if ( $activate ) {
if ( isset( $_POST['passster_password'] ) ) {
update_term_meta( $term_id, 'passster_password', sanitize_text_field( wp_unslash( $_POST['passster_password'] ) ) );
}
if ( isset( $_POST['passster_redirect_url'] ) ) {
update_term_meta( $term_id, 'passster_redirect_url', esc_url_raw( wp_unslash( $_POST['passster_redirect_url'] ) ) );
}
// Overwrite defaults.
if ( isset( $_POST['passster_headline'] ) ) {
update_term_meta( $term_id, 'passster_headline', sanitize_text_field( wp_unslash( $_POST['passster_headline'] ) ) );
}
if ( isset( $_POST['passster_instruction'] ) ) {
update_term_meta( $term_id, 'passster_instruction', wp_kses_post( wp_unslash( $_POST['passster_instruction'] ) ) );
}
if ( isset( $_POST['passster_placeholder'] ) ) {
update_term_meta( $term_id, 'passster_placeholder', sanitize_text_field( wp_unslash( $_POST['passster_placeholder'] ) ) );
}
if ( isset( $_POST['passster_button'] ) ) {
update_term_meta( $term_id, 'passster_button', sanitize_text_field( wp_unslash( $_POST['passster_button'] ) ) );
}
}
}
// -------------------------------------------------------------------------
// Admin Columns
// -------------------------------------------------------------------------
/**
* Add columns to taxonomy list table.
*
* @param array $columns Existing columns.
* @return array
*/
public function add_columns( array $columns ) : array {
$columns['passster_password'] = __( 'Password', 'content-protector' );
return $columns;
}
/**
* Render column content for taxonomy list table.
*
* @param string $content Column content.
* @param string $column_name Column name.
* @param int $term_id Term ID.
* @return string
*/
public function render_column( $content, $column_name, $term_id ) {
$activate = get_term_meta( $term_id, 'passster_activate_protection', true );
if ( !$activate ) {
if ( 'passster_password' === $column_name || 'passster_protection_type' === $column_name ) {
return '-';
}
return $content;
}
$protection_type = ( get_term_meta( $term_id, 'passster_protection_type', true ) ?: 'password' );
switch ( $column_name ) {
case 'passster_password':
return esc_html( get_term_meta( $term_id, 'passster_password', true ) );
break;
case 'passster_protection_type':
$types = array(
'password' => __( 'Password', 'content-protector' ),
'passwords' => __( 'Passwords', 'content-protector' ),
'password_list' => __( 'Password List', 'content-protector' ),
'password_lists' => __( 'Password Lists', 'content-protector' ),
'recaptcha' => __( 'reCaptcha', 'content-protector' ),
'turnstile' => __( 'Turnstile', 'content-protector' ),
);
return esc_html( $types[$protection_type] ?? $protection_type );
}
return $content;
}
// -------------------------------------------------------------------------
// Row Actions
// -------------------------------------------------------------------------
/**
* Add Quick Settings link to term row actions.
*
* @param array $actions Existing actions.
* @param \WP_Term $term Term object.
* @return array
*/
public function add_row_actions( $actions, $term ) {
if ( current_user_can( 'manage_categories' ) ) {
$actions['passster_quick_settings'] = sprintf(
'%s',
$term->term_id,
wp_create_nonce( 'passster_quick_edit_cat_' . $term->term_id ),
__( 'Quick Settings', 'content-protector' )
);
}
return $actions;
}
// -------------------------------------------------------------------------
// Quick Edit Modal UI
// -------------------------------------------------------------------------
/**
* Output quick edit modal HTML, CSS and JS.
*
* @return void
*/
public function quick_edit_ui() {
$screen = get_current_screen();
if ( !$screen ) {
return;
}
$taxonomies = $this->get_taxonomies();
if ( !in_array( $screen->taxonomy, $taxonomies, true ) ) {
return;
}
$is_pro = \passster_fs()->is_plan_or_trial__premium_only( 'pro' );
$lists = ( $is_pro ? $this->get_password_lists() : array() );
?>
__( 'Security check failed.', 'content-protector' ),
) );
}
if ( !current_user_can( 'manage_categories' ) ) {
wp_send_json_error( array(
'message' => __( 'Permission denied.', 'content-protector' ),
) );
}
$term = get_term( $term_id );
if ( !$term || is_wp_error( $term ) ) {
wp_send_json_error( array(
'message' => __( 'Invalid term.', 'content-protector' ),
) );
}
if ( 'get' === $action ) {
$data = array(
'term_id' => $term_id,
'activate' => (bool) get_term_meta( $term_id, 'passster_activate_protection', true ),
'password' => get_term_meta( $term_id, 'passster_password', true ),
'protection_type' => ( get_term_meta( $term_id, 'passster_protection_type', true ) ?: 'password' ),
'passwords' => get_term_meta( $term_id, 'passster_passwords', true ),
'password_list' => get_term_meta( $term_id, 'passster_password_list', true ),
'password_lists' => get_term_meta( $term_id, 'passster_password_lists', true ),
);
wp_send_json_success( $data );
}
if ( 'save' === $action ) {
$activate = ( isset( $_POST['activate'] ) ? absint( $_POST['activate'] ) : 0 );
update_term_meta( $term_id, 'passster_activate_protection', $activate );
if ( $activate ) {
$password = ( isset( $_POST['password'] ) ? sanitize_text_field( wp_unslash( $_POST['password'] ) ) : '' );
update_term_meta( $term_id, 'passster_password', $password );
}
wp_send_json_success( array(
'message' => __( 'Settings saved.', 'content-protector' ),
) );
}
wp_send_json_error( array(
'message' => __( 'Invalid action.', 'content-protector' ),
) );
}
// -------------------------------------------------------------------------
// Frontend: Protect Posts in Protected Categories
// -------------------------------------------------------------------------
/**
* Filter the_content for posts belonging to a protected category.
* Runs at priority 11, after PS_Public (priority 10).
*
* @param string $content Post content.
* @return string
*/
public function filter_content_by_category( $content ) {
if ( is_admin() ) {
return $content;
}
if ( is_category() || is_tax() ) {
return $content;
}
$post_id = get_the_id();
if ( !$post_id ) {
return $content;
}
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
return $content;
}
$term_data = $this->get_active_category_lock( $post_id );
if ( !$term_data ) {
return $content;
}
$shortcode = $this->build_shortcode_from_term( $term_data['term_id'], $content );
return do_shortcode( $shortcode );
}
/**
* @param int $post_id Post ID.
* @return array|null
*/
public function get_active_category_lock( int $post_id ) {
$post_protection = get_post_meta( $post_id, 'passster_activate_protection', true );
if ( $post_protection ) {
return null;
}
$term_data = $this->get_protected_term_for_post( $post_id );
if ( !$term_data ) {
return null;
}
$atts = $this->build_atts_from_term( $term_data['term_id'] );
if ( PS_Conditional::is_valid( $atts ) ) {
return null;
}
return $term_data;
}
/**
* Protect category archive pages.
*
* @return void
*/
public function protect_category_archive() {
if ( is_admin() ) {
return;
}
// WooCommerce single product: if the product belongs to a locked product_cat,
// suppress all WooCommerce product hooks and show the password form inline.
if ( class_exists( 'WooCommerce' ) && is_singular( 'product' ) ) {
$post_id = get_the_ID();
$term_data = ( $post_id ? $this->get_protected_term_for_post( $post_id ) : null );
if ( $term_data && 'product_cat' === $term_data['taxonomy'] ) {
$atts = $this->build_atts_from_term( $term_data['term_id'] );
if ( !PS_Conditional::is_valid( $atts ) ) {
$shortcode = $this->build_shortcode_from_term( $term_data['term_id'], '' );
self::$woo_protected_form = do_shortcode( $shortcode );
// Replace the entire single-product template with the protected form.
add_filter( 'template_include', array($this, 'use_protected_category_template'), 99 );
}
}
return;
}
if ( !is_category() && !is_tax() ) {
return;
}
$term = get_queried_object();
if ( !$term || !is_a( $term, 'WP_Term' ) ) {
return;
}
// Only handle hierarchical taxonomies.
$taxonomy_obj = get_taxonomy( $term->taxonomy );
if ( !$taxonomy_obj || !$taxonomy_obj->hierarchical || !$taxonomy_obj->public ) {
return;
}
// Check the term itself and then its ancestors for protection.
$protected_term_id = $this->get_active_protected_term_id( $term );
if ( !$protected_term_id ) {
return;
}
// Use the protected term (may be an ancestor) for all subsequent meta lookups.
$term = get_term( $protected_term_id );
if ( !$term || is_wp_error( $term ) ) {
return;
}
$atts = $this->build_atts_from_term( $term->term_id );
if ( PS_Conditional::is_valid( $atts ) ) {
return;
}
// Check for redirect.
$redirect = get_term_meta( $term->term_id, 'passster_redirect_url', true );
if ( !empty( $redirect ) ) {
wp_redirect( esc_url_raw( $redirect ) );
exit;
}
$shortcode = $this->build_shortcode_from_term( $term->term_id, '' );
$form_html = do_shortcode( $shortcode );
// WooCommerce product category archives use their own template system and never
// call the_content() on the main query posts, so the virtual-post trick doesn't work.
// Instead, inject the form via WooCommerce's own template hooks and suppress the product loop.
if ( class_exists( 'WooCommerce' ) && 'product_cat' === $term->taxonomy ) {
self::$woo_protected_form = $form_html;
// Replace WooCommerce's entire archive template with a minimal one that shows only the form.
add_filter( 'template_include', array($this, 'use_protected_category_template'), 99 );
return;
}
self::$protected_form = $form_html;
if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
// FSE/block themes: let the block template render normally; replace only the
// core/query block (the post loop) with the password form. This preserves the
// theme's own header/footer block templates.
add_filter(
'render_block',
array($this, 'replace_query_block_with_form'),
10,
2
);
} else {
// Classic themes: swap the archive template for a minimal protected-category
// template that calls get_header()/get_footer() from the active theme.
add_filter( 'template_include', array($this, 'use_protected_regular_category_template'), 99 );
}
}
// -------------------------------------------------------------------------
// Helper Methods
// -------------------------------------------------------------------------
/**
* Check a term and its ancestors for active protection.
*
* @param \WP_Term $term Term to check (and walk up from).
* @return int|null The protected term ID (itself or an ancestor), or null if none.
*/
public function get_active_protected_term_id( \WP_Term $term ) {
$ids_to_check = array_merge( array($term->term_id), get_ancestors( $term->term_id, $term->taxonomy, 'taxonomy' ) );
foreach ( $ids_to_check as $check_id ) {
if ( get_term_meta( $check_id, 'passster_activate_protection', true ) ) {
return $check_id;
}
}
return null;
}
/**
* Get the first protected term for a given post.
*
* @param int $post_id Post ID.
* @return array|null Array with 'term_id' and 'taxonomy', or null.
*/
public function get_protected_term_for_post( int $post_id ) {
$taxonomies = $this->get_taxonomies();
foreach ( $taxonomies as $taxonomy ) {
$terms = get_the_terms( $post_id, $taxonomy );
if ( !$terms || is_wp_error( $terms ) ) {
continue;
}
foreach ( $terms as $term ) {
// Check the term itself, then walk up its ancestor chain.
$protected_term_id = $this->get_active_protected_term_id( $term );
if ( $protected_term_id ) {
return array(
'term_id' => $protected_term_id,
'taxonomy' => $taxonomy,
);
}
}
}
return null;
}
/**
* Build $atts array from term meta (for PS_Conditional::is_valid).
*
* @param int $term_id Term ID.
* @return array
*/
private function build_atts_from_term( int $term_id ) : array {
$atts = array();
$atts['password'] = get_term_meta( $term_id, 'passster_password', true );
return $atts;
}
/**
* Build a passster shortcode string from term meta.
*
* @param int $term_id Term ID.
* @param string $content Content to wrap.
* @return string
*/
private function build_shortcode_from_term( int $term_id, string $content ) : string {
$options = get_option( 'passster' );
$shortcode = '[passster ';
$password = get_term_meta( $term_id, 'passster_password', true );
$shortcode .= 'password="' . esc_attr( $password ) . '" ';
$shortcode .= 'protection="full" ';
$shortcode .= 'term_id="' . $term_id . '" ';
// Redirect.
$redirect = get_term_meta( $term_id, 'passster_redirect_url', true );
if ( !empty( $redirect ) ) {
$shortcode .= 'redirect="' . esc_url( $redirect ) . '" ';
}
// Overwrite defaults.
$headline = get_term_meta( $term_id, 'passster_headline', true );
if ( !empty( $headline ) ) {
$shortcode .= 'headline="' . esc_attr( $headline ) . '" ';
}
$instruction = get_term_meta( $term_id, 'passster_instruction', true );
if ( !empty( $instruction ) ) {
$shortcode .= 'instruction="' . base64_encode( $instruction ) . '" ';
}
$placeholder = get_term_meta( $term_id, 'passster_placeholder', true );
if ( !empty( $placeholder ) ) {
$shortcode .= 'placeholder="' . esc_attr( $placeholder ) . '" ';
}
$button = get_term_meta( $term_id, 'passster_button', true );
if ( !empty( $button ) ) {
$shortcode .= 'button="' . esc_attr( $button ) . '" ';
}
$shortcode .= ']' . $content . '[/passster]';
return $shortcode;
}
/**
* Get published password lists.
*
* @return array
*/
private function get_password_lists() : array {
return get_posts( array(
'post_type' => 'password_lists',
'post_status' => 'publish',
'posts_per_page' => -1,
) );
}
}