PluginProbe
Passster – Password Protect Pages and Content / 4.3.16
Passster – Password Protect Pages and Content v4.3.16
4.3.16 4.3.15 4.3.14 4.3.12 4.3.13 4.3.11 4.3.10 4.3.9 4.3.8 4.3.7 4.3.6 4.3.5 trunk 3.5.4 3.5.5.2 3.5.5.8 3.5.5.9 4.0 4.1.4 4.2.10 4.2.11 4.2.12 4.2.13 4.2.14 4.2.15 All 48 releases
content-protector / inc / admin / inc / class-ps-admin.php

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

947 lines 36.7 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 class PS_Admin {
6 /**
7 * Contains instance or null
8 *
9 * @var object|null
10 */
11 private static $instance = null;
12
13 /**
14 * Returns instance of PS_Admin.
15 *
16 * @return object
17 */
18 public static function get_instance() {
19 if ( null === self::$instance ) {
20 self::$instance = new self();
21 }
22 return self::$instance;
23 }
24
25 public function __construct() {
26 // Check permissions before include settings.
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 );
32 add_action(
33 'manage_protected_areas_posts_custom_column',
34 array($this, 'set_area_columns_content'),
35 10,
36 2
37 );
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'),
42 10,
43 2
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.
56 add_action(
57 'save_post_protected_areas',
58 array($this, 'save_area_shortcode'),
59 20,
60 1
61 );
62 }
63
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 /**
148 * Register post type "protected areas"
149 *
150 * @return void
151 */
152 public function register_password_areas() {
153 $labels = array(
154 'name' => _x( 'Protected Areas', 'post type general name', 'content-protector' ),
155 'singular_name' => _x( 'Protected Area', 'post type singular name', 'content-protector' ),
156 'menu_name' => _x( 'Protected Areas', 'admin menu', 'content-protector' ),
157 'name_admin_bar' => _x( 'Protected Area', 'add new on admin bar', 'content-protector' ),
158 'add_new' => _x( 'Add New', 'content-protector' ),
159 'add_new_item' => __( 'Add New Protected Area', 'content-protector' ),
160 'new_item' => __( 'New Protected Area', 'content-protector' ),
161 'edit_item' => __( 'Edit Protected Area', 'content-protector' ),
162 'view_item' => __( 'View Protected Area', 'content-protector' ),
163 'all_items' => __( 'Protected Areas', 'content-protector' ),
164 'search_items' => __( 'Search Protected Areas', 'content-protector' ),
165 'parent_item_colon' => __( 'Parent Protected Area', 'content-protector' ),
166 'not_found' => __( 'No Protected Areas found.', 'content-protector' ),
167 'not_found_in_trash' => __( 'No Protected Areas found in Trash.', 'content-protector' ),
168 );
169 $args = array(
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,
185 );
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' )) ) {
187 $args['public'] = true;
188 $args['publicly_queryable'] = true;
189 }
190 $theme = wp_get_theme();
191 if ( current_user_can( 'administrator' ) && ('Divi' == $theme->name || 'Divi' == $theme->parent_theme) ) {
192 $args['public'] = true;
193 $args['publicly_queryable'] = true;
194 }
195 register_post_type( 'protected_areas', $args );
196 }
197
198 /**
199 * Set column headers for pages/post
200 *
201 * @param array $columns array of columns.
202 *
203 * @return array
204 */
205 public function set_post_columns( array $columns ) : array {
206 $columns['protected'] = __( 'Password', 'content-protector' );
207 return $columns;
208 }
209
210 /**
211 * Add content to registered columns for posts/pages
212 *
213 * @param string $column name of the column.
214 * @param int $post_id current id.
215 *
216 * @return void
217 */
218 public function set_post_columns_content( string $column, int $post_id ) {
219 switch ( $column ) {
220 case 'protected':
221 $protected = get_post_meta( $post_id, 'passster_activate_protection', true );
222 if ( $protected ) {
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 }
239 }
240 break;
241 }
242 }
243
244 /**
245 * Set column headers password lists post type
246 *
247 * @param array $columns array of columns.
248 *
249 * @return array
250 */
251 public function set_area_columns( array $columns ) : array {
252 $columns['source'] = __( 'Source', 'content-protector' );
253 $columns['shortcode'] = __( 'Shortcode', 'content-protector' );
254 $columns['protection-type'] = __( 'Protection Type', 'content-protector' );
255 $columns['password'] = __( 'Password', 'content-protector' );
256 $columns['redirect'] = __( 'Redirect', 'content-protector' );
257 unset($columns['date']);
258 return $columns;
259 }
260
261 /**
262 * Add content to registered columns for password list post type.
263 *
264 * @param string $column name of the column.
265 * @param int $post_id current id.
266 *
267 * @return void
268 */
269 public function set_area_columns_content( string $column, int $post_id ) {
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;
302 case 'shortcode':
303 $source = get_post_meta( $post_id, '_passster_source', true );
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 }
310 ?>
311 <?php
312 if ( !empty( $shortcode ) ) {
313 ?>
314 <button type="button"
315 class="components-button components-clipboard-button is-primary area-<?php
316 echo esc_html( $post_id );
317 ?>"
318 data-shortcode="<?php
319 echo esc_html( $shortcode );
320 ?>"
321 onclick="copyShortcode(this)"
322 >
323 <?php
324 esc_html_e( 'Copy Shortcode', 'content-protector' );
325 ?>
326 </button>
327 <script>
328 function copyShortcode(element) {
329 // Copy to clipboard.
330 navigator.clipboard.writeText(element.getAttribute('data-shortcode')).then(function () {
331 element.innerHTML = "<?php
332 esc_html_e( 'Copied!', 'content-protector' );
333 ?>";
334
335 setTimeout(function () {
336 element.innerHTML = "<?php
337 esc_html_e( 'Copy Shortcode', 'content-protector' );
338 ?>"
339 }, 1500);
340
341 }, function (err) {
342 console.error('Could not copy shortcode: ', err);
343 });
344 }
345 </script>
346 <style>
347 button {
348 white-space: nowrap;
349 text-shadow: none;
350 outline: 1px solid transparent;
351 width: auto;
352 display: inline-flex;
353 text-decoration: none;
354 font-family: inherit;
355 font-weight: normal;
356 font-size: 13px;
357 margin: 0;
358 border: 0;
359 cursor: pointer;
360 -webkit-appearance: none;
361 background: #007cba;
362 transition: box-shadow 0.1s linear;
363 height: 36px;
364 align-items: center;
365 box-sizing: border-box;
366 padding: 6px 12px;
367 border-radius: 2px;
368 color: #fff;
369 }
370
371 button:hover {
372 background: #006ba1;
373 }
374 </style>
375 <?php
376 }
377 ?>
378 <?php
379 break;
380 case 'protection-type':
381 $type = get_post_meta( $post_id, 'passster_protection_type', true );
382 $protection_types = array(
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' ),
389 );
390 echo esc_html( $protection_types[$type] );
391 break;
392 case 'password':
393 $type = get_post_meta( $post_id, 'passster_protection_type', true );
394 $protection_types = array(
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' => '-',
401 );
402 // Build the edit link if it's a password list.
403 if ( 'password_list' === $type ) {
404 $edit_link = get_edit_post_link( $protection_types[$type] );
405 ?>
406 <a target="_blank"
407 href="<?php
408 echo esc_url( $edit_link );
409 ?>"><?php
410 echo esc_html( $protection_types[$type] );
411 ?></a>
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, " |" );
421 } else {
422 echo esc_html( $protection_types[$type] );
423 }
424 break;
425 case 'user-restriction':
426 $restriction = get_post_meta( $post_id, 'passster_activate_user_restriction', true );
427 $type = get_post_meta( $post_id, 'passster_user_restriction_type', true );
428 $value = get_post_meta( $post_id, 'passster_user_restriction', true );
429 // Get available user roles.
430 global $wp_roles;
431 $roles = $wp_roles->roles;
432 if ( $restriction ) {
433 if ( 'user-role' === $type ) {
434 echo esc_html( $roles[$value]['name'] );
435 } elseif ( 'username' === $type ) {
436 echo esc_html( $value );
437 } else {
438 echo '-';
439 }
440 } else {
441 echo '-';
442 }
443 break;
444 case 'redirect':
445 $redirect = get_post_meta( $post_id, 'passster_redirect_url', true );
446 if ( !empty( $redirect ) ) {
447 echo '<a href="' . esc_url( $redirect ) . '">' . esc_url( $redirect ) . '</a>';
448 } else {
449 echo '-';
450 }
451 break;
452 }
453 }
454
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 }
947