PluginProbe
Passster – Password Protect Pages and Content / 4.3.17
Passster – Password Protect Pages and Content v4.3.17
4.3.17 4.3.16 4.3.15 4.3.14 4.3.12 4.3.13 4.3.11 4.3.10 4.3.9 4.3.8 4.3.7 4.3.6 4.3.5 trunk 3.5.4 3.5.5.2 3.5.5.8 3.5.5.9 4.0 4.1.4 4.2.10 4.2.11 4.2.12 4.2.13 4.2.14 All 49 releases
← All changes | inc/admin/inc/class-ps-admin.php +725 -105 4.0 → 4.3.17 View file →
@@ -1,66 +1,156 @@
1 1 <?php
2 2
3 3 namespace passster;
4 4
5 -class PS_Admin
6 -{
5 +class PS_Admin {
7 6 /**
8 7 * Contains instance or null
9 8 *
10 9 * @var object|null
11 10 */
12 - private static $instance = null ;
11 + private static $instance = null;
12 +
13 13 /**
14 14 * Returns instance of PS_Admin.
15 15 *
16 16 * @return object
17 17 */
18 - public static function get_instance()
19 - {
18 + public static function get_instance() {
20 19 if ( null === self::$instance ) {
21 20 self::$instance = new self();
22 21 }
23 22 return self::$instance;
24 23 }
25 -
26 - public function __construct()
27 - {
24 +
25 + public function __construct() {
28 26 // Check permissions before include settings.
29 - if ( !current_user_can( apply_filters( 'passster_user_capability', 'manage_options' ) ) ) {
30 - return;
31 - }
32 - add_action( 'init', array( $this, 'register_password_areas' ) );
33 - add_filter( 'manage_protected_areas_posts_columns', array( $this, 'set_area_columns' ) );
34 - add_filter( 'manage_post_posts_columns', array( $this, 'set_post_columns' ) );
35 - add_filter( 'manage_page_posts_columns', array( $this, 'set_post_columns' ) );
27 + add_action( 'admin_menu', array($this, 'remove_custom_fields_metabox') );
28 + add_action( 'init', array($this, 'register_password_areas') );
29 + add_filter( 'manage_protected_areas_posts_columns', array($this, 'set_area_columns') );
30 + // Register columns for all public post types (post, page, and custom).
31 + add_action( 'init', array($this, 'register_post_type_columns'), 99 );
36 32 add_action(
37 - 'manage_post_posts_custom_column',
38 - array( $this, 'set_post_columns_content' ),
33 + 'manage_protected_areas_posts_custom_column',
34 + array($this, 'set_area_columns_content'),
39 35 10,
40 36 2
41 37 );
42 - add_action(
43 - 'manage_page_posts_custom_column',
44 - array( $this, 'set_post_columns_content' ),
38 + // Modify row actions and edit links for block-sourced areas.
39 + add_filter(
40 + 'post_row_actions',
41 + array($this, 'modify_area_row_actions'),
45 42 10,
46 43 2
47 44 );
45 + add_filter(
46 + 'get_edit_post_link',
47 + array($this, 'modify_area_edit_link'),
48 + 10,
49 + 3
50 + );
51 + // Quick edit AJAX handler.
52 + add_action( 'wp_ajax_passster_quick_edit_area', array($this, 'ajax_quick_edit_area') );
53 + // Quick edit UI assets.
54 + add_action( 'admin_footer-edit.php', array($this, 'quick_edit_ui') );
55 + // Generate shortcode meta on every save so it appears in the list table.
48 56 add_action(
49 - 'manage_protected_areas_posts_custom_column',
50 - array( $this, 'set_area_columns_content' ),
51 - 10,
52 - 2
57 + 'save_post_protected_areas',
58 + array($this, 'save_area_shortcode'),
59 + 20,
60 + 1
53 61 );
54 62 }
55 -
63 +
56 64 /**
65 + * Register Password column for all public post types.
66 + *
67 + * @return void
68 + */
69 + public function register_post_type_columns() {
70 + $post_types = get_post_types( array(
71 + 'public' => true,
72 + ), 'names' );
73 + foreach ( $post_types as $post_type ) {
74 + if ( 'attachment' === $post_type || 'protected_areas' === $post_type ) {
75 + continue;
76 + }
77 + add_filter( "manage_{$post_type}_posts_columns", array($this, 'set_post_columns') );
78 + add_action(
79 + "manage_{$post_type}_posts_custom_column",
80 + array($this, 'set_post_columns_content'),
81 + 10,
82 + 2
83 + );
84 + }
85 + }
86 +
87 + /**
88 + * Remove Custom Fields meta box
89 + */
90 + public function remove_custom_fields_metabox() {
91 + $post_types = get_post_types( array(
92 + 'public' => true,
93 + 'exclude_from_search' => false,
94 + ), 'names' );
95 + remove_meta_box( 'postcustom', $post_types, 'normal' );
96 + }
97 +
98 + /**
99 + * Generate and save shortcode meta on post save so it appears in the list table.
100 + *
101 + * @param int $post_id Post ID.
102 + */
103 + public function save_area_shortcode( $post_id ) {
104 + if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
105 + return;
106 + }
107 + if ( wp_is_post_revision( $post_id ) ) {
108 + return;
109 + }
110 + $source = get_post_meta( $post_id, '_passster_source', true );
111 + if ( 'block' === $source ) {
112 + return;
113 + }
114 + $protection_type = get_post_meta( $post_id, 'passster_protection_type', true );
115 + $password = get_post_meta( $post_id, 'passster_password', true );
116 + $passwords = get_post_meta( $post_id, 'passster_passwords', true );
117 + $password_list = get_post_meta( $post_id, 'passster_password_list', true );
118 + $shortcode = '';
119 + switch ( $protection_type ) {
120 + case 'passwords':
121 + if ( !empty( $passwords ) ) {
122 + $shortcode = '[passster passwords="' . esc_attr( $passwords ) . '" area="' . $post_id . '"]';
123 + }
124 + break;
125 + case 'password_list':
126 + if ( !empty( $password_list ) ) {
127 + $shortcode = '[passster password_list="' . esc_attr( $password_list ) . '" area="' . $post_id . '"]';
128 + }
129 + break;
130 + case 'recaptcha':
131 + $shortcode = '[passster recaptcha="true" area="' . $post_id . '"]';
132 + break;
133 + case 'turnstile':
134 + $shortcode = '[passster turnstile="true" area="' . $post_id . '"]';
135 + break;
136 + default:
137 + if ( !empty( $password ) ) {
138 + $shortcode = '[passster password="' . esc_attr( $password ) . '" area="' . $post_id . '"]';
139 + }
140 + break;
141 + }
142 + if ( !empty( $shortcode ) ) {
143 + update_post_meta( $post_id, 'passster_area_shortcode', $shortcode );
144 + }
145 + }
146 +
147 + /**
57 148 * Register post type "protected areas"
58 149 *
59 150 * @return void
60 151 */
61 - public function register_password_areas()
62 - {
152 + public function register_password_areas() {
63 153 $labels = array(
64 154 'name' => _x( 'Protected Areas', 'post type general name', 'content-protector' ),
65 155 'singular_name' => _x( 'Protected Area', 'post type singular name', 'content-protector' ),
66 156 'menu_name' => _x( 'Protected Areas', 'admin menu', 'content-protector' ),
@@ -76,39 +166,36 @@
76 166 'not_found' => __( 'No Protected Areas found.', 'content-protector' ),
77 167 'not_found_in_trash' => __( 'No Protected Areas found in Trash.', 'content-protector' ),
78 168 );
79 169 $args = array(
80 - 'labels' => $labels,
81 - 'description' => __( 'Manageable protected areas', 'content-protector' ),
82 - 'public' => false,
83 - 'publicly_queryable' => false,
84 - 'show_ui' => true,
85 - 'show_in_menu' => 'passster',
86 - 'query_var' => true,
87 - 'rewrite' => false,
88 - 'capability_type' => 'post',
89 - 'has_archive' => false,
90 - 'hierarchical' => false,
91 - 'menu_position' => null,
92 - 'supports' => array( 'title', 'editor' ),
93 - 'show_in_rest' => true,
170 + 'labels' => $labels,
171 + 'description' => __( 'Manageable protected areas', 'content-protector' ),
172 + 'public' => false,
173 + 'publicly_queryable' => false,
174 + 'show_ui' => true,
175 + 'show_in_menu' => 'passster',
176 + 'query_var' => true,
177 + 'rewrite' => false,
178 + 'capability_type' => 'post',
179 + 'has_archive' => false,
180 + 'hierarchical' => false,
181 + 'menu_position' => null,
182 + 'supports' => array('title', 'editor'),
183 + 'show_in_rest' => true,
184 + 'rest_controller_class' => PS_Protected_Areas_Rest_Controller::class,
94 185 );
95 -
96 - if ( current_user_can( 'administrator' ) && (is_plugin_active( 'elementor/elementor.php' ) || is_plugin_active( 'livecanvas/livecanvas-plugin-index.php' ) || is_plugin_active( 'divi-builder/divi-builder.php' )) ) {
186 + if ( current_user_can( 'administrator' ) && (is_plugin_active( 'elementor/elementor.php' ) || is_plugin_active( 'fusion-builder/fusion-builder.php' ) || is_plugin_active( 'livecanvas/livecanvas-plugin-index.php' ) || is_plugin_active( 'divi-builder/divi-builder.php' ) || is_plugin_active( 'oxygen/functions.php' )) ) {
97 187 $args['public'] = true;
98 188 $args['publicly_queryable'] = true;
99 189 }
100 -
101 190 $theme = wp_get_theme();
102 -
103 - if ( current_user_can( 'administrator' ) && 'Divi' == $theme->name || 'Divi' == $theme->parent_theme ) {
191 + if ( current_user_can( 'administrator' ) && ('Divi' == $theme->name || 'Divi' == $theme->parent_theme) ) {
104 192 $args['public'] = true;
105 193 $args['publicly_queryable'] = true;
106 194 }
107 -
108 195 register_post_type( 'protected_areas', $args );
109 196 }
110 -
197 +
111 198 /**
112 199 * Set column headers for pages/post
113 200 *
114 201 * @param array $columns array of columns.
@@ -114,14 +201,13 @@
114 201 * @param array $columns array of columns.
115 202 *
116 203 * @return array
117 204 */
118 - public function set_post_columns( array $columns ) : array
119 - {
120 - $columns['protected'] = __( 'Password Protection', 'content-protector' );
205 + public function set_post_columns( array $columns ) : array {
206 + $columns['protected'] = __( 'Password', 'content-protector' );
121 207 return $columns;
122 208 }
123 -
209 +
124 210 /**
125 211 * Add content to registered columns for posts/pages
126 212 *
127 213 * @param string $column name of the column.
@@ -128,29 +214,34 @@
128 214 * @param int $post_id current id.
129 215 *
130 216 * @return void
131 217 */
132 - public function set_post_columns_content( string $column, int $post_id )
133 - {
218 + public function set_post_columns_content( string $column, int $post_id ) {
134 219 switch ( $column ) {
135 220 case 'protected':
136 221 $protected = get_post_meta( $post_id, 'passster_activate_protection', true );
137 -
138 222 if ( $protected ) {
139 - ?>
140 - <div style="background:#007cba;color:#fff;padding:10px 15px;text-align:center;font-weight:bold;max-width: 120px;margin: 10px 0;}">
141 - <span style="position:relative;top: -2px;}" class="dashicons dashicons-lock"></span>
142 - <?php
143 - esc_html_e( 'Protected', 'content-protector' );
144 - ?>
145 - </div>
146 - <?php
223 + $protection_type = get_post_meta( $post_id, 'passster_protection_type', true );
224 + $password = get_post_meta( $post_id, 'passster_password', true );
225 + echo esc_html( $password );
226 + } else {
227 + // Check category-level protection.
228 + $term_data = PS_Category_Lock::get_instance()->get_protected_term_for_post( $post_id );
229 + if ( $term_data ) {
230 + $term = get_term( $term_data['term_id'] );
231 + $edit_url = get_edit_term_link( $term_data['term_id'], $term_data['taxonomy'] );
232 + echo '<span class="dashicons dashicons-category" style="font-size:14px;width:14px;height:14px;color:#007cba;vertical-align:middle;"></span> ';
233 + echo '<a href="' . esc_url( $edit_url ) . '" title="' . esc_attr__( 'Edit category protection', 'content-protector' ) . '">';
234 + echo esc_html( $term->name );
235 + echo '</a><br>';
236 + $protection_type = ( get_term_meta( $term_data['term_id'], 'passster_protection_type', true ) ?: 'password' );
237 + echo esc_html( get_term_meta( $term_data['term_id'], 'passster_password', true ) );
238 + }
147 239 }
148 -
149 240 break;
150 241 }
151 242 }
152 -
243 +
153 244 /**
154 245 * Set column headers password lists post type
155 246 *
156 247 * @param array $columns array of columns.
@@ -156,18 +247,18 @@
156 247 * @param array $columns array of columns.
157 248 *
158 249 * @return array
159 250 */
160 - public function set_area_columns( array $columns ) : array
161 - {
251 + public function set_area_columns( array $columns ) : array {
252 + $columns['source'] = __( 'Source', 'content-protector' );
162 253 $columns['shortcode'] = __( 'Shortcode', 'content-protector' );
163 254 $columns['protection-type'] = __( 'Protection Type', 'content-protector' );
164 255 $columns['password'] = __( 'Password', 'content-protector' );
165 256 $columns['redirect'] = __( 'Redirect', 'content-protector' );
166 - unset( $columns['date'] );
257 + unset($columns['date']);
167 258 return $columns;
168 259 }
169 -
260 +
170 261 /**
171 262 * Add content to registered columns for password list post type.
172 263 *
173 264 * @param string $column name of the column.
@@ -174,24 +265,59 @@
174 265 * @param int $post_id current id.
175 266 *
176 267 * @return void
177 268 */
178 - public function set_area_columns_content( string $column, int $post_id )
179 - {
269 + public function set_area_columns_content( string $column, int $post_id ) {
180 270 switch ( $column ) {
271 + case 'source':
272 + $source = get_post_meta( $post_id, '_passster_source', true );
273 + if ( 'block' === $source ) {
274 + $source_post_id = get_post_meta( $post_id, '_passster_source_post_id', true );
275 + $source_post = get_post( $source_post_id );
276 + if ( $source_post ) {
277 + $block_id = get_post_meta( $post_id, '_passster_block_id', true );
278 + $params = array(
279 + 'post' => absint( $source_post_id ),
280 + 'action' => 'edit',
281 + );
282 + if ( $block_id ) {
283 + $params['passster_focus_block'] = $block_id;
284 + }
285 + $edit_link = add_query_arg( $params, admin_url( 'post.php' ) );
286 + echo '<span class="dashicons dashicons-block-default" style="color: #007cba;"></span> ';
287 + printf(
288 + '<a href="%s" title="%s">%s</a>',
289 + esc_url( $edit_link ),
290 + esc_attr__( 'Edit source post', 'content-protector' ),
291 + esc_html( $source_post->post_title )
292 + );
293 + } else {
294 + echo '<span class="dashicons dashicons-block-default"></span> ';
295 + esc_html_e( 'Block (orphaned)', 'content-protector' );
296 + }
297 + } else {
298 + echo '<span class="dashicons dashicons-admin-page" style="color: #757575;"></span> ';
299 + esc_html_e( 'Standalone', 'content-protector' );
300 + }
301 + break;
181 302 case 'shortcode':
303 + $source = get_post_meta( $post_id, '_passster_source', true );
182 304 $shortcode = get_post_meta( $post_id, 'passster_area_shortcode', true );
305 + // Block-sourced areas don't need shortcode (embedded in post).
306 + if ( 'block' === $source ) {
307 + echo '<span style="color: #757575;">' . esc_html__( 'Embedded in block', 'content-protector' ) . '</span>';
308 + break;
309 + }
183 310 ?>
184 311 <?php
185 -
186 - if ( !empty($shortcode) ) {
312 + if ( !empty( $shortcode ) ) {
187 313 ?>
188 314 <button type="button"
189 315 class="components-button components-clipboard-button is-primary area-<?php
190 - echo esc_html( $post_id ) ;
316 + echo esc_html( $post_id );
191 317 ?>"
192 318 data-shortcode="<?php
193 - echo esc_html( $shortcode ) ;
319 + echo esc_html( $shortcode );
194 320 ?>"
195 321 onclick="copyShortcode(this)"
196 322 >
197 323 <?php
@@ -247,9 +373,8 @@
247 373 }
248 374 </style>
249 375 <?php
250 376 }
251 -
252 377 ?>
253 378 <?php
254 379 break;
255 380 case 'protection-type':
@@ -254,39 +379,49 @@
254 379 break;
255 380 case 'protection-type':
256 381 $type = get_post_meta( $post_id, 'passster_protection_type', true );
257 382 $protection_types = array(
258 - 'password' => __( 'Password', 'content-protector' ),
259 - 'password_list' => __( 'Password List', 'content-protector' ),
260 - 'passwords' => __( 'Passwords', 'content-protector' ),
261 - 'recaptcha' => __( 'ReCaptcha', 'content-protector' ),
383 + 'password' => __( 'Password', 'content-protector' ),
384 + 'password_list' => __( 'Password List', 'content-protector' ),
385 + 'password_lists' => __( 'Password Lists', 'content-protector' ),
386 + 'passwords' => __( 'Passwords', 'content-protector' ),
387 + 'recaptcha' => __( 'reCaptcha', 'content-protector' ),
388 + 'turnstile' => __( 'Turnstile', 'content-protector' ),
262 389 );
263 - echo esc_html( $protection_types[$type] ) ;
390 + echo esc_html( $protection_types[$type] );
264 391 break;
265 392 case 'password':
266 393 $type = get_post_meta( $post_id, 'passster_protection_type', true );
267 394 $protection_types = array(
268 - 'password' => get_post_meta( $post_id, 'passster_password', true ),
269 - 'password_list' => get_post_meta( $post_id, 'passster_password_list', true ),
270 - 'passwords' => get_post_meta( $post_id, 'passster_passwords', true ),
271 - 'recaptcha' => '-',
395 + 'password' => get_post_meta( $post_id, 'passster_password', true ),
396 + 'password_list' => get_post_meta( $post_id, 'passster_password_list', true ),
397 + 'password_lists' => get_post_meta( $post_id, 'passster_password_lists', true ),
398 + 'passwords' => get_post_meta( $post_id, 'passster_passwords', true ),
399 + 'recaptcha' => '-',
400 + 'turnstile' => '-',
272 401 );
273 402 // Build the edit link if it's a password list.
274 -
275 403 if ( 'password_list' === $type ) {
276 404 $edit_link = get_edit_post_link( $protection_types[$type] );
277 405 ?>
278 406 <a target="_blank"
279 407 href="<?php
280 - echo esc_url( $edit_link ) ;
408 + echo esc_url( $edit_link );
281 409 ?>"><?php
282 - echo esc_html( $protection_types[$type] ) ;
410 + echo esc_html( $protection_types[$type] );
283 411 ?></a>
284 412 <?php
413 + } elseif ( 'password_lists' === $type ) {
414 + $password_list_ids = get_post_meta( $post_id, 'passster_password_lists', true );
415 + $lists_string = '';
416 + foreach ( $password_list_ids as $password_list_id ) {
417 + $edit_url = admin_url( 'post.php?post=' . esc_html( $password_list_id ) . '&action=edit', 'https' );
418 + $lists_string .= '<a href="' . esc_url( $edit_url ) . '">' . esc_html( $password_list_id ) . '</a> | ';
419 + }
420 + echo rtrim( $lists_string, " |" );
285 421 } else {
286 - echo esc_html( $protection_types[$type] ) ;
422 + echo esc_html( $protection_types[$type] );
287 423 }
288 -
289 424 break;
290 425 case 'user-restriction':
291 426 $restriction = get_post_meta( $post_id, 'passster_activate_user_restriction', true );
292 427 $type = get_post_meta( $post_id, 'passster_user_restriction_type', true );
@@ -291,36 +426,521 @@
291 426 $restriction = get_post_meta( $post_id, 'passster_activate_user_restriction', true );
292 427 $type = get_post_meta( $post_id, 'passster_user_restriction_type', true );
293 428 $value = get_post_meta( $post_id, 'passster_user_restriction', true );
294 429 // Get available user roles.
295 - global $wp_roles ;
430 + global $wp_roles;
296 431 $roles = $wp_roles->roles;
297 -
298 432 if ( $restriction ) {
299 -
300 433 if ( 'user-role' === $type ) {
301 - echo esc_html( $roles[$value]['name'] ) ;
434 + echo esc_html( $roles[$value]['name'] );
302 435 } elseif ( 'username' === $type ) {
303 - echo esc_html( $value ) ;
436 + echo esc_html( $value );
304 437 } else {
305 - echo '-' ;
438 + echo '-';
306 439 }
307 -
308 440 } else {
309 - echo '-' ;
441 + echo '-';
310 442 }
311 -
312 443 break;
313 444 case 'redirect':
314 445 $redirect = get_post_meta( $post_id, 'passster_redirect_url', true );
315 -
316 - if ( !empty($redirect) ) {
317 - echo '<a href="' . esc_url( $redirect ) . '">' . esc_url( $redirect ) . '</a>' ;
446 + if ( !empty( $redirect ) ) {
447 + echo '<a href="' . esc_url( $redirect ) . '">' . esc_url( $redirect ) . '</a>';
318 448 } else {
319 - echo '-' ;
449 + echo '-';
320 450 }
321 -
322 451 break;
323 452 }
324 453 }
325 454
326 -}
455 + /**
456 + * Output quick edit UI HTML and JS.
457 + *
458 + * @return void
459 + */
460 + public function quick_edit_ui() {
461 + $screen = get_current_screen();
462 + if ( !$screen || 'protected_areas' !== $screen->post_type ) {
463 + return;
464 + }
465 + // Get password lists for dropdown.
466 + $password_lists = get_posts( array(
467 + 'post_type' => 'password_lists',
468 + 'posts_per_page' => -1,
469 + 'post_status' => 'publish',
470 + ) );
471 + ?>
472 + <div id="passster-quick-edit-modal" style="display: none;">
473 + <div class="passster-quick-edit-backdrop"></div>
474 + <div class="passster-quick-edit-panel">
475 + <div class="passster-quick-edit-header">
476 + <h3><?php
477 + esc_html_e( 'Quick Settings', 'content-protector' );
478 + ?></h3>
479 + <button type="button" class="passster-quick-edit-close">&times;</button>
480 + </div>
481 + <div class="passster-quick-edit-body">
482 + <input type="hidden" id="passster-qe-area-id" value="">
483 + <input type="hidden" id="passster-qe-nonce" value="">
484 +
485 + <p class="passster-qe-field">
486 + <label for="passster-qe-protection-type"><?php
487 + esc_html_e( 'Protection Type', 'content-protector' );
488 + ?></label>
489 + <select id="passster-qe-protection-type">
490 + <option value="password"><?php
491 + esc_html_e( 'Single Password', 'content-protector' );
492 + ?></option>
493 + <option value="passwords"><?php
494 + esc_html_e( 'Multiple Passwords', 'content-protector' );
495 + ?></option>
496 + <option value="password_list"><?php
497 + esc_html_e( 'Password List', 'content-protector' );
498 + ?></option>
499 + </select>
500 + </p>
501 +
502 + <p class="passster-qe-field passster-qe-password-field">
503 + <label for="passster-qe-password"><?php
504 + esc_html_e( 'Password', 'content-protector' );
505 + ?></label>
506 + <input type="text" id="passster-qe-password" value="">
507 + </p>
508 +
509 + <p class="passster-qe-field passster-qe-passwords-field" style="display: none;">
510 + <label for="passster-qe-passwords"><?php
511 + esc_html_e( 'Passwords (one per line)', 'content-protector' );
512 + ?></label>
513 + <textarea id="passster-qe-passwords" rows="4"></textarea>
514 + </p>
515 +
516 + <p class="passster-qe-field passster-qe-list-field" style="display: none;">
517 + <label for="passster-qe-password-list"><?php
518 + esc_html_e( 'Password List', 'content-protector' );
519 + ?></label>
520 + <select id="passster-qe-password-list">
521 + <option value="0"><?php
522 + esc_html_e( '— Select —', 'content-protector' );
523 + ?></option>
524 + <?php
525 + foreach ( $password_lists as $list ) {
526 + ?>
527 + <option value="<?php
528 + echo esc_attr( $list->ID );
529 + ?>"><?php
530 + echo esc_html( $list->post_title );
531 + ?></option>
532 + <?php
533 + }
534 + ?>
535 + </select>
536 + </p>
537 +
538 + <p class="passster-qe-source-info" style="display: none;">
539 + <em><?php
540 + esc_html_e( 'This area is synced from a block. Changes will update the block.', 'content-protector' );
541 + ?></em>
542 + </p>
543 + </div>
544 + <div class="passster-quick-edit-footer">
545 + <button type="button" class="button passster-quick-edit-cancel"><?php
546 + esc_html_e( 'Cancel', 'content-protector' );
547 + ?></button>
548 + <button type="button" class="button button-primary passster-quick-edit-save"><?php
549 + esc_html_e( 'Save', 'content-protector' );
550 + ?></button>
551 + <span class="spinner"></span>
552 + </div>
553 + </div>
554 + </div>
555 +
556 + <style>
557 + .passster-quick-edit-backdrop {
558 + position: fixed;
559 + top: 0;
560 + left: 0;
561 + right: 0;
562 + bottom: 0;
563 + background: rgba(0, 0, 0, 0.5);
564 + z-index: 100000;
565 + }
566 + .passster-quick-edit-panel {
567 + position: fixed;
568 + top: 50%;
569 + left: 50%;
570 + transform: translate(-50%, -50%);
571 + background: #fff;
572 + border-radius: 8px;
573 + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
574 + width: 400px;
575 + max-width: 90vw;
576 + z-index: 100001;
577 + }
578 + .passster-quick-edit-header {
579 + display: flex;
580 + justify-content: space-between;
581 + align-items: center;
582 + padding: 16px 20px;
583 + border-bottom: 1px solid #ddd;
584 + }
585 + .passster-quick-edit-header h3 {
586 + margin: 0;
587 + font-size: 16px;
588 + }
589 + .passster-quick-edit-close {
590 + background: none;
591 + border: none;
592 + font-size: 24px;
593 + cursor: pointer;
594 + color: #666;
595 + padding: 0;
596 + line-height: 1;
597 + }
598 + .passster-quick-edit-close:hover {
599 + color: #d63638;
600 + }
601 + .passster-quick-edit-body {
602 + padding: 20px;
603 + }
604 + .passster-qe-field {
605 + margin-bottom: 16px;
606 + }
607 + .passster-qe-field label {
608 + display: block;
609 + margin-bottom: 4px;
610 + font-weight: 600;
611 + }
612 + .passster-qe-field input,
613 + .passster-qe-field select,
614 + .passster-qe-field textarea {
615 + width: 100%;
616 + }
617 + .passster-qe-source-info {
618 + background: #f0f6fc;
619 + padding: 10px;
620 + border-radius: 4px;
621 + color: #1e1e1e;
622 + }
623 + .passster-quick-edit-footer {
624 + display: flex;
625 + justify-content: flex-end;
626 + align-items: center;
627 + gap: 8px;
628 + padding: 16px 20px;
629 + border-top: 1px solid #ddd;
630 + background: #f6f7f7;
631 + border-radius: 0 0 8px 8px;
632 + }
633 + .passster-quick-edit-footer .spinner {
634 + float: none;
635 + margin: 0;
636 + }
637 + </style>
638 +
639 + <script>
640 + jQuery(function($) {
641 + var $modal = $('#passster-quick-edit-modal');
642 + var $typeSelect = $('#passster-qe-protection-type');
643 +
644 + // Toggle fields based on protection type
645 + function toggleFields() {
646 + var type = $typeSelect.val();
647 + $('.passster-qe-password-field').toggle(type === 'password');
648 + $('.passster-qe-passwords-field').toggle(type === 'passwords');
649 + $('.passster-qe-list-field').toggle(type === 'password_list');
650 + }
651 +
652 + $typeSelect.on('change', toggleFields);
653 +
654 + // Open modal
655 + $(document).on('click', '.passster-quick-settings', function(e) {
656 + e.preventDefault();
657 + var areaId = $(this).data('area-id');
658 + var nonce = $(this).data('nonce');
659 +
660 + $('#passster-qe-area-id').val(areaId);
661 + $('#passster-qe-nonce').val(nonce);
662 +
663 + // Load current settings
664 + $.post(ajaxurl, {
665 + action: 'passster_quick_edit_area',
666 + area_id: areaId,
667 + nonce: nonce,
668 + edit_action: 'get'
669 + }, function(response) {
670 + if (response.success) {
671 + var data = response.data;
672 + $typeSelect.val(data.protection_type);
673 + $('#passster-qe-password').val(data.password || '');
674 + $('#passster-qe-passwords').val(data.passwords || '');
675 + $('#passster-qe-password-list').val(data.password_list || 0);
676 + $('.passster-qe-source-info').toggle(data.source === 'block');
677 + toggleFields();
678 + $modal.show();
679 + }
680 + });
681 + });
682 +
683 + // Close modal
684 + $modal.on('click', '.passster-quick-edit-close, .passster-quick-edit-cancel, .passster-quick-edit-backdrop', function() {
685 + $modal.hide();
686 + });
687 +
688 + // Save
689 + $modal.on('click', '.passster-quick-edit-save', function() {
690 + var $btn = $(this);
691 + var $spinner = $modal.find('.spinner');
692 +
693 + $btn.prop('disabled', true);
694 + $spinner.addClass('is-active');
695 +
696 + $.post(ajaxurl, {
697 + action: 'passster_quick_edit_area',
698 + area_id: $('#passster-qe-area-id').val(),
699 + nonce: $('#passster-qe-nonce').val(),
700 + edit_action: 'save',
701 + protection_type: $typeSelect.val(),
702 + password: $('#passster-qe-password').val(),
703 + passwords: $('#passster-qe-passwords').val(),
704 + password_list: $('#passster-qe-password-list').val()
705 + }, function(response) {
706 + $btn.prop('disabled', false);
707 + $spinner.removeClass('is-active');
708 +
709 + if (response.success) {
710 + $modal.hide();
711 + location.reload();
712 + } else {
713 + alert(response.data.message || 'Error saving settings.');
714 + }
715 + });
716 + });
717 +
718 + // ESC to close
719 + $(document).on('keydown', function(e) {
720 + if (e.key === 'Escape' && $modal.is(':visible')) {
721 + $modal.hide();
722 + }
723 + });
724 + });
725 + </script>
726 + <?php
727 + }
728 +
729 + /**
730 + * Modify row actions for Protected Areas.
731 + * Block-sourced areas: Edit goes to source post, add Quick Settings.
732 + *
733 + * @param array $actions Row actions.
734 + * @param WP_Post $post Post object.
735 + *
736 + * @return array Modified actions.
737 + */
738 + public function modify_area_row_actions( $actions, $post ) {
739 + if ( 'protected_areas' !== $post->post_type ) {
740 + return $actions;
741 + }
742 + $source = get_post_meta( $post->ID, '_passster_source', true );
743 + if ( 'block' === $source ) {
744 + $source_post_id = get_post_meta( $post->ID, '_passster_source_post_id', true );
745 + $source_post = get_post( $source_post_id );
746 + if ( $source_post ) {
747 + $block_id = get_post_meta( $post->ID, '_passster_block_id', true );
748 + $edit_url = get_edit_post_link( $source_post_id );
749 + if ( $block_id && $edit_url ) {
750 + $edit_url = add_query_arg( 'passster_focus_block', rawurlencode( $block_id ), $edit_url );
751 + }
752 + // Change Edit to go to source post with the specific block focused.
753 + $actions['edit'] = sprintf(
754 + '<a href="%s" aria-label="%s">%s</a>',
755 + esc_url( $edit_url ),
756 + /* translators: %s: Post title */
757 + esc_attr( sprintf( __( 'Edit block in &#8220;%s&#8221;', 'content-protector' ), $source_post->post_title ) ),
758 + __( 'Edit Block', 'content-protector' )
759 + );
760 + }
761 + // Remove trash for block-sourced (they're synced automatically).
762 + unset($actions['trash']);
763 + }
764 + // Add Quick Settings for all areas.
765 + $actions['quick_settings'] = sprintf(
766 + '<a href="#" class="passster-quick-settings" data-area-id="%d" data-nonce="%s">%s</a>',
767 + $post->ID,
768 + wp_create_nonce( 'passster_quick_edit_' . $post->ID ),
769 + __( 'Quick Settings', 'content-protector' )
770 + );
771 + return $actions;
772 + }
773 +
774 + /**
775 + * Modify edit post link for block-sourced areas.
776 + *
777 + * @param string $link Edit link.
778 + * @param int $post_id Post ID.
779 + * @param string $context Context.
780 + *
781 + * @return string Modified link.
782 + */
783 + public function modify_area_edit_link( $link, $post_id, $context ) {
784 + $post = get_post( $post_id );
785 + if ( !$post || 'protected_areas' !== $post->post_type ) {
786 + return $link;
787 + }
788 + $source = get_post_meta( $post_id, '_passster_source', true );
789 + if ( 'block' === $source ) {
790 + $source_post_id = get_post_meta( $post_id, '_passster_source_post_id', true );
791 + if ( $source_post_id ) {
792 + $block_id = get_post_meta( $post_id, '_passster_block_id', true );
793 + $params = array(
794 + 'post' => absint( $source_post_id ),
795 + 'action' => 'edit',
796 + );
797 + if ( $block_id ) {
798 + $params['passster_focus_block'] = $block_id;
799 + }
800 + $edit_link = add_query_arg( $params, admin_url( 'post.php' ) );
801 + return ( 'display' === $context ? esc_url( $edit_link ) : $edit_link );
802 + }
803 + }
804 + return $link;
805 + }
806 +
807 + /**
808 + * AJAX handler for quick edit area settings.
809 + *
810 + * @return void
811 + */
812 + public function ajax_quick_edit_area() {
813 + $area_id = ( isset( $_POST['area_id'] ) ? absint( $_POST['area_id'] ) : 0 );
814 + $nonce = ( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '' );
815 + $action = ( isset( $_POST['edit_action'] ) ? sanitize_text_field( wp_unslash( $_POST['edit_action'] ) ) : 'get' );
816 + if ( !wp_verify_nonce( $nonce, 'passster_quick_edit_' . $area_id ) ) {
817 + wp_send_json_error( array(
818 + 'message' => __( 'Security check failed.', 'content-protector' ),
819 + ) );
820 + }
821 + if ( !current_user_can( 'edit_post', $area_id ) ) {
822 + wp_send_json_error( array(
823 + 'message' => __( 'Permission denied.', 'content-protector' ),
824 + ) );
825 + }
826 + $area = get_post( $area_id );
827 + if ( !$area || 'protected_areas' !== $area->post_type ) {
828 + wp_send_json_error( array(
829 + 'message' => __( 'Invalid area.', 'content-protector' ),
830 + ) );
831 + }
832 + if ( 'get' === $action ) {
833 + // Return current settings.
834 + $data = array(
835 + 'area_id' => $area_id,
836 + 'title' => $area->post_title,
837 + 'protection_type' => ( get_post_meta( $area_id, 'passster_protection_type', true ) ?: 'password' ),
838 + 'password' => get_post_meta( $area_id, 'passster_password', true ),
839 + 'passwords' => get_post_meta( $area_id, 'passster_passwords', true ),
840 + 'password_list' => get_post_meta( $area_id, 'passster_password_list', true ),
841 + 'source' => get_post_meta( $area_id, '_passster_source', true ),
842 + );
843 + wp_send_json_success( $data );
844 + }
845 + if ( 'save' === $action ) {
846 + // Save settings.
847 + $protection_type = ( isset( $_POST['protection_type'] ) ? sanitize_text_field( wp_unslash( $_POST['protection_type'] ) ) : 'password' );
848 + update_post_meta( $area_id, 'passster_protection_type', $protection_type );
849 + switch ( $protection_type ) {
850 + case 'password':
851 + $password = ( isset( $_POST['password'] ) ? sanitize_text_field( wp_unslash( $_POST['password'] ) ) : '' );
852 + update_post_meta( $area_id, 'passster_password', $password );
853 + break;
854 + case 'passwords':
855 + $passwords = ( isset( $_POST['passwords'] ) ? sanitize_textarea_field( wp_unslash( $_POST['passwords'] ) ) : '' );
856 + update_post_meta( $area_id, 'passster_passwords', $passwords );
857 + break;
858 + case 'password_list':
859 + $password_list = ( isset( $_POST['password_list'] ) ? absint( $_POST['password_list'] ) : 0 );
860 + update_post_meta( $area_id, 'passster_password_list', $password_list );
861 + break;
862 + }
863 + // If block-sourced, sync back to block.
864 + $source = get_post_meta( $area_id, '_passster_source', true );
865 + if ( 'block' === $source ) {
866 + $source_post_id = get_post_meta( $area_id, '_passster_source_post_id', true );
867 + $block_id = get_post_meta( $area_id, '_passster_block_id', true );
868 + if ( $source_post_id && $block_id ) {
869 + $this->sync_settings_to_block( $source_post_id, $block_id, $area_id );
870 + }
871 + }
872 + wp_send_json_success( array(
873 + 'message' => __( 'Settings saved.', 'content-protector' ),
874 + ) );
875 + }
876 + wp_send_json_error( array(
877 + 'message' => __( 'Invalid action.', 'content-protector' ),
878 + ) );
879 + }
880 +
881 + /**
882 + * Sync settings from Protected Area back to the block.
883 + *
884 + * @param int $post_id Source post ID.
885 + * @param string $block_id Block ID.
886 + * @param int $area_id Protected Area ID.
887 + *
888 + * @return bool Success.
889 + */
890 + private function sync_settings_to_block( $post_id, $block_id, $area_id ) {
891 + $post = get_post( $post_id );
892 + if ( !$post ) {
893 + return false;
894 + }
895 + $content = $post->post_content;
896 + $blocks = parse_blocks( $content );
897 + $protection_type = get_post_meta( $area_id, 'passster_protection_type', true );
898 + $password = get_post_meta( $area_id, 'passster_password', true );
899 + $passwords = get_post_meta( $area_id, 'passster_passwords', true );
900 + $password_list = get_post_meta( $area_id, 'passster_password_list', true );
901 + $updated_blocks = $this->update_block_settings( $blocks, $block_id, array(
902 + 'protectionType' => $protection_type,
903 + 'password' => $password,
904 + 'passwords' => $passwords,
905 + 'passwordList' => (int) $password_list,
906 + ) );
907 + $new_content = serialize_blocks( $updated_blocks );
908 + // Update post without triggering our sync hook.
909 + remove_action( 'save_post', array(\passster\PS_Block_Editor::get_instance(), 'sync_protected_content_blocks'), 10 );
910 + wp_update_post( array(
911 + 'ID' => $post_id,
912 + 'post_content' => $new_content,
913 + ) );
914 + add_action(
915 + 'save_post',
916 + array(\passster\PS_Block_Editor::get_instance(), 'sync_protected_content_blocks'),
917 + 10,
918 + 2
919 + );
920 + return true;
921 + }
922 +
923 + /**
924 + * Recursively update block settings.
925 + *
926 + * @param array $blocks Blocks array.
927 + * @param string $block_id Target block ID.
928 + * @param array $settings New settings.
929 + *
930 + * @return array Updated blocks.
931 + */
932 + private function update_block_settings( $blocks, $block_id, $settings ) {
933 + foreach ( $blocks as &$block ) {
934 + if ( 'passster/content-lock' === $block['blockName'] ) {
935 + if ( isset( $block['attrs']['blockId'] ) && $block['attrs']['blockId'] === $block_id ) {
936 + $block['attrs'] = array_merge( $block['attrs'], $settings );
937 + }
938 + }
939 + if ( !empty( $block['innerBlocks'] ) ) {
940 + $block['innerBlocks'] = $this->update_block_settings( $block['innerBlocks'], $block_id, $settings );
941 + }
942 + }
943 + return $blocks;
944 + }
945 +
946 +}