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
← All changes | inc/admin/inc/class-ps-admin.php +946 -945 4.3.94.3.16 View file →
@@ -1,945 +1,946 @@
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 - );
185 - 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' )) ) {
186 - $args['public'] = true;
187 - $args['publicly_queryable'] = true;
188 - }
189 - $theme = wp_get_theme();
190 - if ( current_user_can( 'administrator' ) && 'Divi' == $theme->name || 'Divi' == $theme->parent_theme ) {
191 - $args['public'] = true;
192 - $args['publicly_queryable'] = true;
193 - }
194 - register_post_type( 'protected_areas', $args );
195 - }
196 -
197 - /**
198 - * Set column headers for pages/post
199 - *
200 - * @param array $columns array of columns.
201 - *
202 - * @return array
203 - */
204 - public function set_post_columns( array $columns ) : array {
205 - $columns['protected'] = __( 'Password', 'content-protector' );
206 - return $columns;
207 - }
208 -
209 - /**
210 - * Add content to registered columns for posts/pages
211 - *
212 - * @param string $column name of the column.
213 - * @param int $post_id current id.
214 - *
215 - * @return void
216 - */
217 - public function set_post_columns_content( string $column, int $post_id ) {
218 - switch ( $column ) {
219 - case 'protected':
220 - $protected = get_post_meta( $post_id, 'passster_activate_protection', true );
221 - if ( $protected ) {
222 - $protection_type = get_post_meta( $post_id, 'passster_protection_type', true );
223 - $password = get_post_meta( $post_id, 'passster_password', true );
224 - echo esc_html( $password );
225 - } else {
226 - // Check category-level protection.
227 - $term_data = PS_Category_Lock::get_instance()->get_protected_term_for_post( $post_id );
228 - if ( $term_data ) {
229 - $term = get_term( $term_data['term_id'] );
230 - $edit_url = get_edit_term_link( $term_data['term_id'], $term_data['taxonomy'] );
231 - echo '<span class="dashicons dashicons-category" style="font-size:14px;width:14px;height:14px;color:#007cba;vertical-align:middle;"></span> ';
232 - echo '<a href="' . esc_url( $edit_url ) . '" title="' . esc_attr__( 'Edit category protection', 'content-protector' ) . '">';
233 - echo esc_html( $term->name );
234 - echo '</a><br>';
235 - $protection_type = ( get_term_meta( $term_data['term_id'], 'passster_protection_type', true ) ?: 'password' );
236 - echo esc_html( get_term_meta( $term_data['term_id'], 'passster_password', true ) );
237 - }
238 - }
239 - break;
240 - }
241 - }
242 -
243 - /**
244 - * Set column headers password lists post type
245 - *
246 - * @param array $columns array of columns.
247 - *
248 - * @return array
249 - */
250 - public function set_area_columns( array $columns ) : array {
251 - $columns['source'] = __( 'Source', 'content-protector' );
252 - $columns['shortcode'] = __( 'Shortcode', 'content-protector' );
253 - $columns['protection-type'] = __( 'Protection Type', 'content-protector' );
254 - $columns['password'] = __( 'Password', 'content-protector' );
255 - $columns['redirect'] = __( 'Redirect', 'content-protector' );
256 - unset($columns['date']);
257 - return $columns;
258 - }
259 -
260 - /**
261 - * Add content to registered columns for password list post type.
262 - *
263 - * @param string $column name of the column.
264 - * @param int $post_id current id.
265 - *
266 - * @return void
267 - */
268 - public function set_area_columns_content( string $column, int $post_id ) {
269 - switch ( $column ) {
270 - case 'source':
271 - $source = get_post_meta( $post_id, '_passster_source', true );
272 - if ( 'block' === $source ) {
273 - $source_post_id = get_post_meta( $post_id, '_passster_source_post_id', true );
274 - $source_post = get_post( $source_post_id );
275 - if ( $source_post ) {
276 - $block_id = get_post_meta( $post_id, '_passster_block_id', true );
277 - $params = array(
278 - 'post' => absint( $source_post_id ),
279 - 'action' => 'edit',
280 - );
281 - if ( $block_id ) {
282 - $params['passster_focus_block'] = $block_id;
283 - }
284 - $edit_link = add_query_arg( $params, admin_url( 'post.php' ) );
285 - echo '<span class="dashicons dashicons-block-default" style="color: #007cba;"></span> ';
286 - printf(
287 - '<a href="%s" title="%s">%s</a>',
288 - esc_url( $edit_link ),
289 - esc_attr__( 'Edit source post', 'content-protector' ),
290 - esc_html( $source_post->post_title )
291 - );
292 - } else {
293 - echo '<span class="dashicons dashicons-block-default"></span> ';
294 - esc_html_e( 'Block (orphaned)', 'content-protector' );
295 - }
296 - } else {
297 - echo '<span class="dashicons dashicons-admin-page" style="color: #757575;"></span> ';
298 - esc_html_e( 'Standalone', 'content-protector' );
299 - }
300 - break;
301 - case 'shortcode':
302 - $source = get_post_meta( $post_id, '_passster_source', true );
303 - $shortcode = get_post_meta( $post_id, 'passster_area_shortcode', true );
304 - // Block-sourced areas don't need shortcode (embedded in post).
305 - if ( 'block' === $source ) {
306 - echo '<span style="color: #757575;">' . esc_html__( 'Embedded in block', 'content-protector' ) . '</span>';
307 - break;
308 - }
309 - ?>
310 - <?php
311 - if ( !empty( $shortcode ) ) {
312 - ?>
313 - <button type="button"
314 - class="components-button components-clipboard-button is-primary area-<?php
315 - echo esc_html( $post_id );
316 - ?>"
317 - data-shortcode="<?php
318 - echo esc_html( $shortcode );
319 - ?>"
320 - onclick="copyShortcode(this)"
321 - >
322 - <?php
323 - esc_html_e( 'Copy Shortcode', 'content-protector' );
324 - ?>
325 - </button>
326 - <script>
327 - function copyShortcode(element) {
328 - // Copy to clipboard.
329 - navigator.clipboard.writeText(element.getAttribute('data-shortcode')).then(function () {
330 - element.innerHTML = "<?php
331 - esc_html_e( 'Copied!', 'content-protector' );
332 - ?>";
333 -
334 - setTimeout(function () {
335 - element.innerHTML = "<?php
336 - esc_html_e( 'Copy Shortcode', 'content-protector' );
337 - ?>"
338 - }, 1500);
339 -
340 - }, function (err) {
341 - console.error('Could not copy shortcode: ', err);
342 - });
343 - }
344 - </script>
345 - <style>
346 - button {
347 - white-space: nowrap;
348 - text-shadow: none;
349 - outline: 1px solid transparent;
350 - width: auto;
351 - display: inline-flex;
352 - text-decoration: none;
353 - font-family: inherit;
354 - font-weight: normal;
355 - font-size: 13px;
356 - margin: 0;
357 - border: 0;
358 - cursor: pointer;
359 - -webkit-appearance: none;
360 - background: #007cba;
361 - transition: box-shadow 0.1s linear;
362 - height: 36px;
363 - align-items: center;
364 - box-sizing: border-box;
365 - padding: 6px 12px;
366 - border-radius: 2px;
367 - color: #fff;
368 - }
369 -
370 - button:hover {
371 - background: #006ba1;
372 - }
373 - </style>
374 - <?php
375 - }
376 - ?>
377 - <?php
378 - break;
379 - case 'protection-type':
380 - $type = get_post_meta( $post_id, 'passster_protection_type', true );
381 - $protection_types = array(
382 - 'password' => __( 'Password', 'content-protector' ),
383 - 'password_list' => __( 'Password List', 'content-protector' ),
384 - 'password_lists' => __( 'Password Lists', 'content-protector' ),
385 - 'passwords' => __( 'Passwords', 'content-protector' ),
386 - 'recaptcha' => __( 'reCaptcha', 'content-protector' ),
387 - 'turnstile' => __( 'Turnstile', 'content-protector' ),
388 - );
389 - echo esc_html( $protection_types[$type] );
390 - break;
391 - case 'password':
392 - $type = get_post_meta( $post_id, 'passster_protection_type', true );
393 - $protection_types = array(
394 - 'password' => get_post_meta( $post_id, 'passster_password', true ),
395 - 'password_list' => get_post_meta( $post_id, 'passster_password_list', true ),
396 - 'password_lists' => get_post_meta( $post_id, 'passster_password_lists', true ),
397 - 'passwords' => get_post_meta( $post_id, 'passster_passwords', true ),
398 - 'recaptcha' => '-',
399 - 'turnstile' => '-',
400 - );
401 - // Build the edit link if it's a password list.
402 - if ( 'password_list' === $type ) {
403 - $edit_link = get_edit_post_link( $protection_types[$type] );
404 - ?>
405 - <a target="_blank"
406 - href="<?php
407 - echo esc_url( $edit_link );
408 - ?>"><?php
409 - echo esc_html( $protection_types[$type] );
410 - ?></a>
411 - <?php
412 - } elseif ( 'password_lists' === $type ) {
413 - $password_list_ids = get_post_meta( $post_id, 'passster_password_lists', true );
414 - $lists_string = '';
415 - foreach ( $password_list_ids as $password_list_id ) {
416 - $edit_url = admin_url( 'post.php?post=' . esc_html( $password_list_id ) . '&action=edit', 'https' );
417 - $lists_string .= '<a href="' . esc_url( $edit_url ) . '">' . esc_html( $password_list_id ) . '</a> | ';
418 - }
419 - echo rtrim( $lists_string, " |" );
420 - } else {
421 - echo esc_html( $protection_types[$type] );
422 - }
423 - break;
424 - case 'user-restriction':
425 - $restriction = get_post_meta( $post_id, 'passster_activate_user_restriction', true );
426 - $type = get_post_meta( $post_id, 'passster_user_restriction_type', true );
427 - $value = get_post_meta( $post_id, 'passster_user_restriction', true );
428 - // Get available user roles.
429 - global $wp_roles;
430 - $roles = $wp_roles->roles;
431 - if ( $restriction ) {
432 - if ( 'user-role' === $type ) {
433 - echo esc_html( $roles[$value]['name'] );
434 - } elseif ( 'username' === $type ) {
435 - echo esc_html( $value );
436 - } else {
437 - echo '-';
438 - }
439 - } else {
440 - echo '-';
441 - }
442 - break;
443 - case 'redirect':
444 - $redirect = get_post_meta( $post_id, 'passster_redirect_url', true );
445 - if ( !empty( $redirect ) ) {
446 - echo '<a href="' . esc_url( $redirect ) . '">' . esc_url( $redirect ) . '</a>';
447 - } else {
448 - echo '-';
449 - }
450 - break;
451 - }
452 - }
453 -
454 - /**
455 - * Output quick edit UI HTML and JS.
456 - *
457 - * @return void
458 - */
459 - public function quick_edit_ui() {
460 - $screen = get_current_screen();
461 - if ( !$screen || 'protected_areas' !== $screen->post_type ) {
462 - return;
463 - }
464 - // Get password lists for dropdown.
465 - $password_lists = get_posts( array(
466 - 'post_type' => 'password_lists',
467 - 'posts_per_page' => -1,
468 - 'post_status' => 'publish',
469 - ) );
470 - ?>
471 - <div id="passster-quick-edit-modal" style="display: none;">
472 - <div class="passster-quick-edit-backdrop"></div>
473 - <div class="passster-quick-edit-panel">
474 - <div class="passster-quick-edit-header">
475 - <h3><?php
476 - esc_html_e( 'Quick Settings', 'content-protector' );
477 - ?></h3>
478 - <button type="button" class="passster-quick-edit-close">&times;</button>
479 - </div>
480 - <div class="passster-quick-edit-body">
481 - <input type="hidden" id="passster-qe-area-id" value="">
482 - <input type="hidden" id="passster-qe-nonce" value="">
483 -
484 - <p class="passster-qe-field">
485 - <label for="passster-qe-protection-type"><?php
486 - esc_html_e( 'Protection Type', 'content-protector' );
487 - ?></label>
488 - <select id="passster-qe-protection-type">
489 - <option value="password"><?php
490 - esc_html_e( 'Single Password', 'content-protector' );
491 - ?></option>
492 - <option value="passwords"><?php
493 - esc_html_e( 'Multiple Passwords', 'content-protector' );
494 - ?></option>
495 - <option value="password_list"><?php
496 - esc_html_e( 'Password List', 'content-protector' );
497 - ?></option>
498 - </select>
499 - </p>
500 -
501 - <p class="passster-qe-field passster-qe-password-field">
502 - <label for="passster-qe-password"><?php
503 - esc_html_e( 'Password', 'content-protector' );
504 - ?></label>
505 - <input type="text" id="passster-qe-password" value="">
506 - </p>
507 -
508 - <p class="passster-qe-field passster-qe-passwords-field" style="display: none;">
509 - <label for="passster-qe-passwords"><?php
510 - esc_html_e( 'Passwords (one per line)', 'content-protector' );
511 - ?></label>
512 - <textarea id="passster-qe-passwords" rows="4"></textarea>
513 - </p>
514 -
515 - <p class="passster-qe-field passster-qe-list-field" style="display: none;">
516 - <label for="passster-qe-password-list"><?php
517 - esc_html_e( 'Password List', 'content-protector' );
518 - ?></label>
519 - <select id="passster-qe-password-list">
520 - <option value="0"><?php
521 - esc_html_e( '— Select —', 'content-protector' );
522 - ?></option>
523 - <?php
524 - foreach ( $password_lists as $list ) {
525 - ?>
526 - <option value="<?php
527 - echo esc_attr( $list->ID );
528 - ?>"><?php
529 - echo esc_html( $list->post_title );
530 - ?></option>
531 - <?php
532 - }
533 - ?>
534 - </select>
535 - </p>
536 -
537 - <p class="passster-qe-source-info" style="display: none;">
538 - <em><?php
539 - esc_html_e( 'This area is synced from a block. Changes will update the block.', 'content-protector' );
540 - ?></em>
541 - </p>
542 - </div>
543 - <div class="passster-quick-edit-footer">
544 - <button type="button" class="button passster-quick-edit-cancel"><?php
545 - esc_html_e( 'Cancel', 'content-protector' );
546 - ?></button>
547 - <button type="button" class="button button-primary passster-quick-edit-save"><?php
548 - esc_html_e( 'Save', 'content-protector' );
549 - ?></button>
550 - <span class="spinner"></span>
551 - </div>
552 - </div>
553 - </div>
554 -
555 - <style>
556 - .passster-quick-edit-backdrop {
557 - position: fixed;
558 - top: 0;
559 - left: 0;
560 - right: 0;
561 - bottom: 0;
562 - background: rgba(0, 0, 0, 0.5);
563 - z-index: 100000;
564 - }
565 - .passster-quick-edit-panel {
566 - position: fixed;
567 - top: 50%;
568 - left: 50%;
569 - transform: translate(-50%, -50%);
570 - background: #fff;
571 - border-radius: 8px;
572 - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
573 - width: 400px;
574 - max-width: 90vw;
575 - z-index: 100001;
576 - }
577 - .passster-quick-edit-header {
578 - display: flex;
579 - justify-content: space-between;
580 - align-items: center;
581 - padding: 16px 20px;
582 - border-bottom: 1px solid #ddd;
583 - }
584 - .passster-quick-edit-header h3 {
585 - margin: 0;
586 - font-size: 16px;
587 - }
588 - .passster-quick-edit-close {
589 - background: none;
590 - border: none;
591 - font-size: 24px;
592 - cursor: pointer;
593 - color: #666;
594 - padding: 0;
595 - line-height: 1;
596 - }
597 - .passster-quick-edit-close:hover {
598 - color: #d63638;
599 - }
600 - .passster-quick-edit-body {
601 - padding: 20px;
602 - }
603 - .passster-qe-field {
604 - margin-bottom: 16px;
605 - }
606 - .passster-qe-field label {
607 - display: block;
608 - margin-bottom: 4px;
609 - font-weight: 600;
610 - }
611 - .passster-qe-field input,
612 - .passster-qe-field select,
613 - .passster-qe-field textarea {
614 - width: 100%;
615 - }
616 - .passster-qe-source-info {
617 - background: #f0f6fc;
618 - padding: 10px;
619 - border-radius: 4px;
620 - color: #1e1e1e;
621 - }
622 - .passster-quick-edit-footer {
623 - display: flex;
624 - justify-content: flex-end;
625 - align-items: center;
626 - gap: 8px;
627 - padding: 16px 20px;
628 - border-top: 1px solid #ddd;
629 - background: #f6f7f7;
630 - border-radius: 0 0 8px 8px;
631 - }
632 - .passster-quick-edit-footer .spinner {
633 - float: none;
634 - margin: 0;
635 - }
636 - </style>
637 -
638 - <script>
639 - jQuery(function($) {
640 - var $modal = $('#passster-quick-edit-modal');
641 - var $typeSelect = $('#passster-qe-protection-type');
642 -
643 - // Toggle fields based on protection type
644 - function toggleFields() {
645 - var type = $typeSelect.val();
646 - $('.passster-qe-password-field').toggle(type === 'password');
647 - $('.passster-qe-passwords-field').toggle(type === 'passwords');
648 - $('.passster-qe-list-field').toggle(type === 'password_list');
649 - }
650 -
651 - $typeSelect.on('change', toggleFields);
652 -
653 - // Open modal
654 - $(document).on('click', '.passster-quick-settings', function(e) {
655 - e.preventDefault();
656 - var areaId = $(this).data('area-id');
657 - var nonce = $(this).data('nonce');
658 -
659 - $('#passster-qe-area-id').val(areaId);
660 - $('#passster-qe-nonce').val(nonce);
661 -
662 - // Load current settings
663 - $.post(ajaxurl, {
664 - action: 'passster_quick_edit_area',
665 - area_id: areaId,
666 - nonce: nonce,
667 - edit_action: 'get'
668 - }, function(response) {
669 - if (response.success) {
670 - var data = response.data;
671 - $typeSelect.val(data.protection_type);
672 - $('#passster-qe-password').val(data.password || '');
673 - $('#passster-qe-passwords').val(data.passwords || '');
674 - $('#passster-qe-password-list').val(data.password_list || 0);
675 - $('.passster-qe-source-info').toggle(data.source === 'block');
676 - toggleFields();
677 - $modal.show();
678 - }
679 - });
680 - });
681 -
682 - // Close modal
683 - $modal.on('click', '.passster-quick-edit-close, .passster-quick-edit-cancel, .passster-quick-edit-backdrop', function() {
684 - $modal.hide();
685 - });
686 -
687 - // Save
688 - $modal.on('click', '.passster-quick-edit-save', function() {
689 - var $btn = $(this);
690 - var $spinner = $modal.find('.spinner');
691 -
692 - $btn.prop('disabled', true);
693 - $spinner.addClass('is-active');
694 -
695 - $.post(ajaxurl, {
696 - action: 'passster_quick_edit_area',
697 - area_id: $('#passster-qe-area-id').val(),
698 - nonce: $('#passster-qe-nonce').val(),
699 - edit_action: 'save',
700 - protection_type: $typeSelect.val(),
701 - password: $('#passster-qe-password').val(),
702 - passwords: $('#passster-qe-passwords').val(),
703 - password_list: $('#passster-qe-password-list').val()
704 - }, function(response) {
705 - $btn.prop('disabled', false);
706 - $spinner.removeClass('is-active');
707 -
708 - if (response.success) {
709 - $modal.hide();
710 - location.reload();
711 - } else {
712 - alert(response.data.message || 'Error saving settings.');
713 - }
714 - });
715 - });
716 -
717 - // ESC to close
718 - $(document).on('keydown', function(e) {
719 - if (e.key === 'Escape' && $modal.is(':visible')) {
720 - $modal.hide();
721 - }
722 - });
723 - });
724 - </script>
725 - <?php
726 - }
727 -
728 - /**
729 - * Modify row actions for Protected Areas.
730 - * Block-sourced areas: Edit goes to source post, add Quick Settings.
731 - *
732 - * @param array $actions Row actions.
733 - * @param WP_Post $post Post object.
734 - *
735 - * @return array Modified actions.
736 - */
737 - public function modify_area_row_actions( $actions, $post ) {
738 - if ( 'protected_areas' !== $post->post_type ) {
739 - return $actions;
740 - }
741 - $source = get_post_meta( $post->ID, '_passster_source', true );
742 - if ( 'block' === $source ) {
743 - $source_post_id = get_post_meta( $post->ID, '_passster_source_post_id', true );
744 - $source_post = get_post( $source_post_id );
745 - if ( $source_post ) {
746 - $block_id = get_post_meta( $post->ID, '_passster_block_id', true );
747 - $edit_url = get_edit_post_link( $source_post_id );
748 - if ( $block_id && $edit_url ) {
749 - $edit_url = add_query_arg( 'passster_focus_block', rawurlencode( $block_id ), $edit_url );
750 - }
751 - // Change Edit to go to source post with the specific block focused.
752 - $actions['edit'] = sprintf(
753 - '<a href="%s" aria-label="%s">%s</a>',
754 - esc_url( $edit_url ),
755 - /* translators: %s: Post title */
756 - esc_attr( sprintf( __( 'Edit block in &#8220;%s&#8221;', 'content-protector' ), $source_post->post_title ) ),
757 - __( 'Edit Block', 'content-protector' )
758 - );
759 - }
760 - // Remove trash for block-sourced (they're synced automatically).
761 - unset($actions['trash']);
762 - }
763 - // Add Quick Settings for all areas.
764 - $actions['quick_settings'] = sprintf(
765 - '<a href="#" class="passster-quick-settings" data-area-id="%d" data-nonce="%s">%s</a>',
766 - $post->ID,
767 - wp_create_nonce( 'passster_quick_edit_' . $post->ID ),
768 - __( 'Quick Settings', 'content-protector' )
769 - );
770 - return $actions;
771 - }
772 -
773 - /**
774 - * Modify edit post link for block-sourced areas.
775 - *
776 - * @param string $link Edit link.
777 - * @param int $post_id Post ID.
778 - * @param string $context Context.
779 - *
780 - * @return string Modified link.
781 - */
782 - public function modify_area_edit_link( $link, $post_id, $context ) {
783 - $post = get_post( $post_id );
784 - if ( !$post || 'protected_areas' !== $post->post_type ) {
785 - return $link;
786 - }
787 - $source = get_post_meta( $post_id, '_passster_source', true );
788 - if ( 'block' === $source ) {
789 - $source_post_id = get_post_meta( $post_id, '_passster_source_post_id', true );
790 - if ( $source_post_id ) {
791 - $block_id = get_post_meta( $post_id, '_passster_block_id', true );
792 - $params = array(
793 - 'post' => absint( $source_post_id ),
794 - 'action' => 'edit',
795 - );
796 - if ( $block_id ) {
797 - $params['passster_focus_block'] = $block_id;
798 - }
799 - $edit_link = add_query_arg( $params, admin_url( 'post.php' ) );
800 - return ( 'display' === $context ? esc_url( $edit_link ) : $edit_link );
801 - }
802 - }
803 - return $link;
804 - }
805 -
806 - /**
807 - * AJAX handler for quick edit area settings.
808 - *
809 - * @return void
810 - */
811 - public function ajax_quick_edit_area() {
812 - $area_id = ( isset( $_POST['area_id'] ) ? absint( $_POST['area_id'] ) : 0 );
813 - $nonce = ( isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '' );
814 - $action = ( isset( $_POST['edit_action'] ) ? sanitize_text_field( wp_unslash( $_POST['edit_action'] ) ) : 'get' );
815 - if ( !wp_verify_nonce( $nonce, 'passster_quick_edit_' . $area_id ) ) {
816 - wp_send_json_error( array(
817 - 'message' => __( 'Security check failed.', 'content-protector' ),
818 - ) );
819 - }
820 - if ( !current_user_can( 'edit_post', $area_id ) ) {
821 - wp_send_json_error( array(
822 - 'message' => __( 'Permission denied.', 'content-protector' ),
823 - ) );
824 - }
825 - $area = get_post( $area_id );
826 - if ( !$area || 'protected_areas' !== $area->post_type ) {
827 - wp_send_json_error( array(
828 - 'message' => __( 'Invalid area.', 'content-protector' ),
829 - ) );
830 - }
831 - if ( 'get' === $action ) {
832 - // Return current settings.
833 - $data = array(
834 - 'area_id' => $area_id,
835 - 'title' => $area->post_title,
836 - 'protection_type' => ( get_post_meta( $area_id, 'passster_protection_type', true ) ?: 'password' ),
837 - 'password' => get_post_meta( $area_id, 'passster_password', true ),
838 - 'passwords' => get_post_meta( $area_id, 'passster_passwords', true ),
839 - 'password_list' => get_post_meta( $area_id, 'passster_password_list', true ),
840 - 'source' => get_post_meta( $area_id, '_passster_source', true ),
841 - );
842 - wp_send_json_success( $data );
843 - }
844 - if ( 'save' === $action ) {
845 - // Save settings.
846 - $protection_type = ( isset( $_POST['protection_type'] ) ? sanitize_text_field( wp_unslash( $_POST['protection_type'] ) ) : 'password' );
847 - update_post_meta( $area_id, 'passster_protection_type', $protection_type );
848 - switch ( $protection_type ) {
849 - case 'password':
850 - $password = ( isset( $_POST['password'] ) ? sanitize_text_field( wp_unslash( $_POST['password'] ) ) : '' );
851 - update_post_meta( $area_id, 'passster_password', $password );
852 - break;
853 - case 'passwords':
854 - $passwords = ( isset( $_POST['passwords'] ) ? sanitize_textarea_field( wp_unslash( $_POST['passwords'] ) ) : '' );
855 - update_post_meta( $area_id, 'passster_passwords', $passwords );
856 - break;
857 - case 'password_list':
858 - $password_list = ( isset( $_POST['password_list'] ) ? absint( $_POST['password_list'] ) : 0 );
859 - update_post_meta( $area_id, 'passster_password_list', $password_list );
860 - break;
861 - }
862 - // If block-sourced, sync back to block.
863 - $source = get_post_meta( $area_id, '_passster_source', true );
864 - if ( 'block' === $source ) {
865 - $source_post_id = get_post_meta( $area_id, '_passster_source_post_id', true );
866 - $block_id = get_post_meta( $area_id, '_passster_block_id', true );
867 - if ( $source_post_id && $block_id ) {
868 - $this->sync_settings_to_block( $source_post_id, $block_id, $area_id );
869 - }
870 - }
871 - wp_send_json_success( array(
872 - 'message' => __( 'Settings saved.', 'content-protector' ),
873 - ) );
874 - }
875 - wp_send_json_error( array(
876 - 'message' => __( 'Invalid action.', 'content-protector' ),
877 - ) );
878 - }
879 -
880 - /**
881 - * Sync settings from Protected Area back to the block.
882 - *
883 - * @param int $post_id Source post ID.
884 - * @param string $block_id Block ID.
885 - * @param int $area_id Protected Area ID.
886 - *
887 - * @return bool Success.
888 - */
889 - private function sync_settings_to_block( $post_id, $block_id, $area_id ) {
890 - $post = get_post( $post_id );
891 - if ( !$post ) {
892 - return false;
893 - }
894 - $content = $post->post_content;
895 - $blocks = parse_blocks( $content );
896 - $protection_type = get_post_meta( $area_id, 'passster_protection_type', true );
897 - $password = get_post_meta( $area_id, 'passster_password', true );
898 - $passwords = get_post_meta( $area_id, 'passster_passwords', true );
899 - $password_list = get_post_meta( $area_id, 'passster_password_list', true );
900 - $updated_blocks = $this->update_block_settings( $blocks, $block_id, array(
901 - 'protectionType' => $protection_type,
902 - 'password' => $password,
903 - 'passwords' => $passwords,
904 - 'passwordList' => (int) $password_list,
905 - ) );
906 - $new_content = serialize_blocks( $updated_blocks );
907 - // Update post without triggering our sync hook.
908 - remove_action( 'save_post', array(\passster\PS_Block_Editor::get_instance(), 'sync_protected_content_blocks'), 10 );
909 - wp_update_post( array(
910 - 'ID' => $post_id,
911 - 'post_content' => $new_content,
912 - ) );
913 - add_action(
914 - 'save_post',
915 - array(\passster\PS_Block_Editor::get_instance(), 'sync_protected_content_blocks'),
916 - 10,
917 - 2
918 - );
919 - return true;
920 - }
921 -
922 - /**
923 - * Recursively update block settings.
924 - *
925 - * @param array $blocks Blocks array.
926 - * @param string $block_id Target block ID.
927 - * @param array $settings New settings.
928 - *
929 - * @return array Updated blocks.
930 - */
931 - private function update_block_settings( $blocks, $block_id, $settings ) {
932 - foreach ( $blocks as &$block ) {
933 - if ( 'passster/content-lock' === $block['blockName'] ) {
934 - if ( isset( $block['attrs']['blockId'] ) && $block['attrs']['blockId'] === $block_id ) {
935 - $block['attrs'] = array_merge( $block['attrs'], $settings );
936 - }
937 - }
938 - if ( !empty( $block['innerBlocks'] ) ) {
939 - $block['innerBlocks'] = $this->update_block_settings( $block['innerBlocks'], $block_id, $settings );
940 - }
941 - }
942 - return $blocks;
943 - }
944 -
945 -}
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 +}