PluginProbe
Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News / 4.0.7
Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News v4.0.7
4.0.8 4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 2.3.5 2.3.6 2.4.0 2.4.1 2.4.10 2.4.11 2.4.12 2.4.13 2.4.14 2.4.15 2.4.16 2.4.17 2.4.18 2.4.19 2.4.2 2.4.20 2.4.21 All 88 releases
post-carousel / admin / blocks-settings / class-blocks-settings-page.php

class-blocks-settings-page.php in Smart Post – Post Grid, Post Carousel, Post Slider Gutenberg Blocks for Blog & News 4.0.7, at admin/blocks-settings/class-blocks-settings-page.php

1,204 lines 38.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The plugin block menu page for admin.
4 *
5 * @link https://shapedplugin.com/
6 *
7 * @package Smart_Post_Show_Pro
8 * @author ShapedPlugin <[email protected]>
9 */
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit; // Exit if accessed directly.
13 }
14
15 if ( ! class_exists( 'Sp_Smart_Post_Block_Admin_Menu_Page' ) ) {
16
17 /**
18 * Sp_Smart_Post_Block_Admin_Menu_Page Class.
19 */
20 class Sp_Smart_Post_Block_Admin_Menu_Page {
21
22 /**
23 * Default Modules variable
24 *
25 * @var array
26 */
27 private $default_modules = array(
28 array(
29 'module_name' => 'post-badges',
30 'options' => array(
31 'post_type' => 'post',
32 ),
33 ),
34 array(
35 'module_name' => 'back-to-top',
36 'options' => array(
37 'display_position' => 'button-right',
38 'horizontal_position' => array(
39 'value' => 30,
40 'unit' => 'px',
41 ),
42 'vertical_position' => array(
43 'value' => 30,
44 'unit' => 'px',
45 ),
46 'back_to_top_icon' => true,
47 'top_icon_source' => 'top-arrow-one',
48 'icon_size' => array(
49 'value' => 24,
50 'unit' => 'px',
51 ),
52 'back_top_label' => '',
53 'display_on' => 'all-pages',
54 'exclude_page' => array(),
55 'include_only' => array(),
56 'smooth_scroll' => true,
57 'transition_delay' => array(
58 'value' => 0.3,
59 'unit' => 's',
60 ),
61 'entrance_animation' => 'fade-in',
62 'go_to_bottom' => false,
63 'back_bottom_label' => '',
64 'typography' => array(
65 'googleFont' => array(
66 'family' => 'Default',
67 'variants' => array( '300', '400', '500', '600', '700', '800', '900' ),
68 ),
69 'typography' => array(
70 'family' => '',
71 'fontWeight' => '',
72 'style' => '',
73 'transform' => '',
74 'decoration' => '',
75 ),
76 ),
77 'font_size' => array(
78 'value' => 16,
79 'unit' => 'px',
80 ),
81 'line_height' => array(
82 'value' => 1,
83 ),
84 'letter_spacing' => array(
85 'value' => '',
86 'unit' => 'px',
87 ),
88 'word_spacing' => array(
89 'value' => '',
90 'unit' => 'px',
91 ),
92 'color' => array(
93 'color' => '#FFF',
94 'hoverColor' => '#FFF',
95 ),
96 'background_color' => array(
97 'color' => array(
98 'style' => 'bgColor',
99 'transparent' => '',
100 'solidColor' => 'var(--smart-post-secondary)',
101 'gradient' => 'linear-gradient(135deg,rgb(181, 51, 97) 0%,rgb(226, 87, 44) 100%)',
102 ),
103 'hover' => array(
104 'style' => 'bgColor',
105 'transparent' => '',
106 'solidColor' => 'var(--smart-post-secondary)',
107 'gradient' => 'linear-gradient(135deg,rgb(181, 51, 97) 0%,rgb(226, 87, 44) 100%)',
108 ),
109 ),
110 'border' => array(
111 'style' => 'none',
112 'color' => '#DDD',
113 'hoverColor' => '#DDD',
114 ),
115 'border_width' => array(
116 'value' => array(
117 'top' => 1,
118 'right' => 1,
119 'bottom' => 1,
120 'left' => 1,
121 ),
122 'unit' => 'px',
123 'allChange' => true,
124 ),
125 'border_radius' => array(
126 'value' => array(
127 'top' => 4,
128 'right' => 4,
129 'bottom' => 4,
130 'left' => 4,
131 ),
132 'unit' => 'px',
133 'allChange' => true,
134 ),
135 'box_shadow_enable' => false,
136 'box_shadow' => array(
137 'value' => array(
138 'top' => 0,
139 'right' => 4,
140 'bottom' => 8,
141 'left' => 0,
142 ),
143 'unit' => 'outset',
144 'color' => '#4E4F521A',
145 'selectDefault' => 'var(--smart-post-shadow-medium-4dp)',
146 ),
147 'box_shadow_enable_hover' => false,
148 'box_shadow_hover' => array(
149 'value' => array(
150 'top' => 0,
151 'right' => 4,
152 'bottom' => 8,
153 'left' => 0,
154 ),
155 'unit' => 'outset',
156 'color' => '#3434361a',
157 'selectDefault' => 'var(--smart-post-shadow-medium-4dp)',
158 ),
159 'padding' => array(
160 'value' => array(
161 'top' => 8,
162 'right' => 8,
163 'bottom' => 8,
164 'left' => 8,
165 ),
166 'unit' => 'px',
167 'allChange' => true,
168 ),
169 'fontListsEditPage' => '',
170 'dynamic_style' => '',
171 ),
172 ),
173 );
174
175 /**
176 * Block constructor.
177 */
178 public function __construct() {
179 $this->default_modules;
180 $this->register_module_settings_option();
181 // update block show/hide option.
182 add_action( 'wp_ajax_sp_smart_post_block_option', array( $this, 'sp_smart_post_block_option' ) );
183
184 // Get plugin changelog data.
185 add_action( 'wp_ajax_sp_smart_post_block_changelog', array( $this, 'sp_smart_post_block_changelog' ) );
186
187 // Script file enqueue for block settings page.
188 add_action( 'admin_enqueue_scripts', array( $this, 'sp_smart_block_settings_enqueue' ) );
189
190 add_action( 'rest_api_init', array( $this, 'sp_smart_post_rest_module_settings' ) );
191
192 // Update setup wizard data.
193 add_action( 'wp_ajax_sp_smart_post_get_user_consent', array( $this, 'sp_smart_post_get_user_consent' ) );
194
195 // Update settings options.
196 add_action( 'wp_ajax_sp_pcp_update_setting_options', array( $this, 'sp_pcp_update_setting_options' ) );
197
198 // Permanently dismiss the "Upgrade to Pro" header notice.
199 add_action( 'wp_ajax_sp_pcp_dismiss_upgrade_notice', array( $this, 'sp_pcp_dismiss_upgrade_notice' ) );
200
201 // Weekly schedules events.
202 add_action( 'smart_post_show_weekly_scheduled_events', array( $this, 'send_usage_data_weekly' ) );
203
204 add_action( 'init', array( $this, 'sp_pcp_submit_user_consent' ) );
205
206 add_action( 'load-post-new.php', array( $this, 'restrict_new_template_creation' ) );
207
208 add_action( 'admin_notices', array( $this, 'maybe_show_user_consent_notice' ) );
209 }
210
211 /**
212 * Send user data weekly.
213 *
214 * @access public
215 * @return void
216 */
217 public function send_usage_data_weekly() {
218 $user_consent = get_option( 'sp_pcp_allow_anonymous_data', false );
219 if ( $user_consent ) {
220 $website_type = get_option( 'sp_ua_site_type', '' );
221 $this->sp_pc_collect_and_send_usage_data( $website_type );
222 }
223 }
224
225 /**
226 * Restrict New Template Creation
227 *
228 * This method checks the total posts (all statuses)
229 * of the `sp_post_template` post type.
230 * If the count is 2 or more, it blocks the
231 * new post creation page.
232 *
233 * @return void
234 */
235 public function restrict_new_template_creation() {
236
237 // Check if the request is for our custom post type.
238 if ( isset( $_GET['post_type'] ) && 'sp_post_template' === $_GET['post_type'] ) {
239
240 // Get all posts count (all statuses).
241 $args = array(
242 'post_type' => 'sp_post_template',
243 'post_status' => 'any',
244 'posts_per_page' => -1,
245 'fields' => 'ids',
246 );
247
248 $posts = get_posts( $args );
249 $count = count( $posts );
250
251 // Prevent creating more than 2 templates.
252 if ( $count >= 2 ) {
253
254 $redirect_url = admin_url( 'edit.php?post_type=sp_post_carousel&page=pcp_help&show_pro_modal=1#savedTemplate' );
255 wp_safe_redirect( $redirect_url );
256 exit;
257 }
258 }
259 }
260
261 /**
262 * Blocks settings option update function
263 *
264 * @return void
265 */
266 public function sp_smart_post_block_option() {
267 // Check user capability.
268 if ( ! current_user_can( apply_filters( 'sp_pc_dashboard_capability', 'manage_options' ) ) ) {
269 wp_send_json_error( __( 'Unauthorized access.', 'post-carousel' ), 403 );
270 }
271
272 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
273
274 if ( ! wp_verify_nonce( $nonce, 'sp-update-block-settings-nonce' ) ) {
275 wp_send_json_error( __( 'Invalid nonce.', 'post-carousel' ), 403 );
276 }
277
278 // Process options data.
279 if ( isset( $_POST['optionData'] ) ) {
280 $options_json = wp_unslash( $_POST['optionData'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
281
282 // Validate and decode JSON.
283 $options = json_decode( $options_json, true );
284
285 if ( json_last_error() !== JSON_ERROR_NONE ) {
286 wp_send_json_error( __( 'Invalid JSON in options data.', 'post-carousel' ), 400 );
287 }
288
289 // Validate it's an array and sanitize.
290 if ( ! is_array( $options ) ) {
291 $options = array();
292 }
293
294 $options = $this->sanitize_array_recursive( $options );
295
296 update_option( 'sp-pcp-blocks-setting-options', $options );
297 }
298
299 // Process modules data.
300 if ( isset( $_POST['modulesData'] ) ) {
301 $modules_json = wp_unslash( $_POST['modulesData'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
302
303 // Validate and decode JSON.
304 $modules = json_decode( $modules_json, true );
305
306 if ( json_last_error() !== JSON_ERROR_NONE ) {
307 wp_send_json_error( __( 'Invalid JSON in modules data.', 'post-carousel' ), 400 );
308 }
309
310 // Validate it's an array and sanitize.
311 if ( ! is_array( $modules ) ) {
312 $modules = array();
313 }
314
315 $modules = $this->sanitize_array_recursive( $modules );
316
317 update_option( 'sp-pcp-blocks-modules-options', $modules );
318 }
319
320 // Process integrations data.
321 if ( isset( $_POST['integrations'] ) ) {
322 $integrations_json = wp_unslash( $_POST['integrations'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
323
324 // Validate and decode JSON.
325 $integrations = json_decode( $integrations_json, true );
326
327 if ( json_last_error() !== JSON_ERROR_NONE ) {
328 wp_send_json_error( __( 'Invalid JSON in integrations data.', 'post-carousel' ), 400 );
329 }
330
331 // Validate it's an array and sanitize.
332 if ( ! is_array( $integrations ) ) {
333 $integrations = array();
334 }
335
336 $integrations = $this->sanitize_array_recursive( $integrations );
337
338 update_option( 'sp-pcp-integration-options', $integrations );
339 }
340
341 // Send success response.
342 wp_send_json_success(
343 array(
344 'message' => __( 'Settings updated successfully.', 'post-carousel' ),
345 )
346 );
347 }
348
349
350 /**
351 * Blocks settings option update function
352 *
353 * @return void
354 */
355 public function sp_smart_post_block_changelog() {
356 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
357
358 if ( ! wp_verify_nonce( $nonce, 'sp-update-block-settings-nonce' ) ) {
359 return;
360 }
361
362 $changelog = get_transient( 'sp_smart_post_changelogs' );
363
364 if ( empty( $changelog ) ) {
365 $changelog = $this->fetch_changelog_from_api();
366 }
367
368 wp_send_json(
369 array(
370 'changelog' => $changelog,
371 )
372 );
373 }
374
375 /**
376 * Fetches changelog from the remote API.
377 *
378 * @return string The changelog content or empty string on failure.
379 */
380 protected function fetch_changelog_from_api() {
381 $api_url = 'https://api.wordpress.org/plugins/info/1.0/post-carousel.json';
382 $response = wp_safe_remote_get(
383 esc_url_raw( $api_url ),
384 array(
385 'timeout' => 15,
386 )
387 );
388
389 if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
390 return '';
391 }
392
393 $api_data = json_decode( wp_remote_retrieve_body( $response ), true );
394 if ( ! isset( $api_data['sections']['changelog'] ) ) {
395 return '';
396 }
397
398 $changelog = wp_kses_post( $api_data['sections']['changelog'] );
399 set_transient( 'sp_smart_post_changelogs', $changelog, 1 * DAY_IN_SECONDS );
400
401 return $changelog;
402 }
403
404 /**
405 * Sanitize array recursively.
406 *
407 * @since 4.0.0
408 * @param mixed $data The data to sanitize.
409 * @return mixed Sanitized data.
410 */
411 private function sanitize_array_recursive( $data ) {
412 if ( ! is_array( $data ) ) {
413 return $data;
414 }
415
416 $sanitized = array();
417 foreach ( $data as $key => $value ) {
418 if ( is_array( $value ) ) {
419 $sanitized[ $key ] = $this->sanitize_array_recursive( $value );
420 } elseif ( is_string( $value ) ) {
421 $sanitized[ $key ] = sanitize_text_field( $value );
422 } elseif ( is_int( $value ) ) {
423 $sanitized[ $key ] = intval( $value );
424 } elseif ( is_float( $value ) ) {
425 $sanitized[ $key ] = floatval( $value );
426 } elseif ( is_bool( $value ) ) {
427 $sanitized[ $key ] = (bool) $value;
428 } else {
429 $sanitized[ $key ] = $value;
430 }
431 }
432
433 return $sanitized;
434 }
435
436 /**
437 * Duplicates a post & its meta and returns the new duplicated Post ID.
438 *
439 * @param int $post_id The Post ID you want to clone.
440 * @return int The duplicated Post ID.
441 */
442 public function duplicate_post( int $post_id ): int {
443 $old_post = get_post( $post_id );
444 if ( ! $old_post ) {
445 // Invalid post ID, return early.
446 return 0;
447 }
448
449 $title = $old_post->post_title;
450 $content = $old_post->post_content;
451
452 // Create new post array.
453 $new_post = array(
454 'post_title' => $title,
455 'post_content' => $content,
456 'post_status' => 'draft',
457 'post_type' => $old_post->post_type,
458 );
459
460 // Insert new post.
461 $new_post_id = wp_insert_post( $new_post );
462
463 // Copy post meta.
464 $post_meta = get_post_custom( $post_id );
465 foreach ( $post_meta as $key => $values ) {
466 foreach ( $values as $value ) {
467 add_post_meta( $new_post_id, $key, maybe_unserialize( $value ) );
468 }
469 }
470
471 // Copy post taxonomies.
472 $taxonomies = get_post_taxonomies( $post_id );
473 foreach ( $taxonomies as $taxonomy ) {
474 $term_ids = wp_get_object_terms( $post_id, $taxonomy, array( 'fields' => 'ids' ) );
475 wp_set_object_terms( $new_post_id, $term_ids, $taxonomy );
476 }
477
478 // Return new post ID.
479 return $new_post_id;
480 }
481
482 /**
483 * Css and Js file enqueue for admin block settings page.
484 *
485 * @return void
486 */
487 public function sp_smart_block_settings_enqueue() {
488 $global_style = get_option( 'sp_smart_post_global_settings', array() );
489 $shadow_css = $global_style['shadow']['shadowRootCSS'] ?? ':root { --smart-post-shadow-subtle-1dp: 0px 1px 2px 0px rgba(0, 0, 0, 0.12); --smart-post-shadow-light-2dp: 0px 2px 4px 0px rgba(0, 0, 0, 0.14); --smart-post-shadow-medium-4dp: 0px 4px 6px 0px rgba(0, 0, 0, 0.16); --smart-post-shadow-strong-8dp: 0px 8px 18px 0px rgba(0, 0, 0, 0.18); --smart-post-shadow-deep-12dp: 0px 12px 17px 0px rgba(0, 0, 0, 0.20); --smart-post-shadow-sharp-4dp: 4px 4px 0px 0px rgba(0, 0, 0, 0.25);}';
490 $root_css = ! empty( $global_style['rootcss'] ) ? $global_style['rootcss'] : ':root{ --sp-smart-breakpoint-tablet: 1023px; --sp-smart-breakpoint-mobile: 767px; --smart-post-light-text: #FAFAFA; --smart-post-background: #FFFFFF; --smart-post-primary-light: #EBEBEB; --smart-post-primary: #999999; --smart-post-primary-dark: #1D1D1D; --smart-post-secondary: #0054FB; --smart-post-dark-2-text: #3E3E3E; --smart-post-dark-text: #0A0A0A; --smart-post-black: #000000;} :root { --smart-post-shadow-subtle-1dp: 0px 1px 2px 0px rgba(0, 0, 0, 0.12); --smart-post-shadow-light-2dp: 0px 2px 4px 0px rgba(0, 0, 0, 0.14); --smart-post-shadow-medium-4dp: 0px 4px 6px 0px rgba(0, 0, 0, 0.16); --smart-post-shadow-strong-8dp: 0px 8px 18px 0px rgba(0, 0, 0, 0.18); --smart-post-shadow-deep-12dp: 0px 12px 17px 0px rgba(0, 0, 0, 0.20); --smart-post-shadow-sharp-4dp: 4px 4px 0px 0px rgba(0, 0, 0, 0.25);}';
491 $inline_css = wp_strip_all_tags( $shadow_css . $root_css );
492
493 wp_register_style( 'sp-smart-post-global-root', false, array(), SMART_POST_SHOW_VERSION );
494 wp_add_inline_style( 'sp-smart-post-global-root', $inline_css );
495 wp_enqueue_style( 'sp-smart-post-global-root' );
496
497 $current_screen = get_current_screen();
498
499 $the_current_page_id = $current_screen->id;
500
501 if ( 'sp_post_carousel_page_pcp_help' === $the_current_page_id ) {
502
503 // css file.
504 wp_enqueue_style( 'sp-pcp-block-setting-page', SP_PC_URL . 'blocks/build/dashboard/style-index.css', array(), SP_PC_VERSION, 'all' );
505 wp_enqueue_style( 'sp-pcp-block-setting-page-editor', SP_PC_URL . 'blocks/build/dashboard/index.css', array( 'wp-components' ), SP_PC_VERSION, 'all' );
506
507 wp_enqueue_style( 'sp-post-carousel-help', SP_PC_URL . 'admin/help-page/css/help-page.min.css', array(), SP_PC_VERSION );
508
509 // WordPress version check.
510 global $wp_version;
511 if ( version_compare( $wp_version, '6.6', '<' ) ) {
512 // Enqueue react-jsx-runtime.js for lower versions of WordPress.
513 wp_enqueue_script( 'sp-jsx-runtime', SP_PC_URL . 'public/assets/js/react-jsx-runtime.min.js', array( 'react', 'react-dom' ), SP_PC_VERSION, true );
514 }
515
516 // Js file.
517 wp_enqueue_script( 'sp-pcp-block-setting-page', SP_PC_URL . 'blocks/build/dashboard/index.js', array( 'wp-plugins', 'wp-element', 'wp-components', 'wp-core-data' ), SP_PC_VERSION, true );
518
519 remove_all_actions( 'admin_notices' );
520
521 $current_user = wp_get_current_user();
522
523 wp_localize_script(
524 'sp-pcp-block-setting-page',
525 'sp_pcp_block_settings',
526 array(
527 'homeUrl' => home_url( '/' ),
528 'pluginUrl' => SP_PC_URL,
529 'getOptions' => get_option( 'sp-pcp-blocks-setting-options' ),
530 'userName' => $current_user->display_name,
531 'modulesOptions' => get_option( 'sp-pcp-blocks-modules-options' ),
532 'integrationOptions' => get_option( 'sp-pcp-integration-options' ),
533 'pluginVersion' => SP_PC_VERSION,
534 'nonce' => wp_create_nonce( 'sp-update-block-settings-nonce' ),
535 'adminUrl' => admin_url(),
536 'settings' => get_option( 'sp_post_carousel_settings', array() ),
537 'restNonce' => wp_create_nonce( 'wp_rest' ),
538 'restUrl' => get_rest_url(),
539 'wizardImages' => SP_PC_URL . 'admin/img/wizard/',
540 'sp_pcp_user_consent' => get_option( 'sp_pcp_allow_anonymous_data', 'undefined' ),
541 'sp_ua_site_type' => get_option( 'sp_ua_site_type' ) ?? '',
542 'pcp_editor_preference' => get_option( 'spsp_blocks_promo_modal_choice', '' ),
543 'savedTemplatesUrl' => admin_url( 'edit.php?post_type=sp_post_carousel&page=pcp_help#savedTemplate' ),
544 'upgradeNoticeVisible' => $this->is_upgrade_notice_visible(),
545 )
546 );
547 }
548 }
549
550 /**
551 * Register block Modules Settings Option.
552 *
553 * @return void
554 */
555 public function register_module_settings_option() {
556 foreach ( $this->default_modules as $module ) {
557 // Build the data you want to save.
558 $data = array(
559 'module_name' => $module['module_name'],
560 'options' => $module['options'],
561 );
562
563 // Save it in wp_options.
564 add_option(
565 'sp_smart_post_module_' . $module['module_name'],
566 $data
567 );
568 }
569 }
570
571 /**
572 * Modules Settings for Your Plugin
573 * Add this to your main plugin file or create a separate
574 * REST API Endpoints - Super Easy Setup.
575 *
576 * @return void
577 */
578 public function sp_smart_post_rest_module_settings() {
579 // Register GET endpoint.
580 register_rest_route(
581 'sp-smart-post/v2',
582 '/get-module-settings',
583 array(
584 'methods' => 'GET',
585 'callback' => array( $this, 'get_module_settings' ),
586 'permission_callback' => function () {
587 return current_user_can( 'manage_options' );
588 },
589 )
590 );
591 // Register POST endpoint.
592 register_rest_route(
593 'sp-smart-post/v2',
594 '/set-module-settings',
595 array(
596 'methods' => 'POST',
597 'callback' => array( $this, 'smart_post_save_module_settings' ),
598 'permission_callback' => function () {
599 return current_user_can( 'manage_options' );
600 },
601 )
602 );
603 // Get total save template.
604 register_rest_route(
605 'sp-smart-post/v2',
606 '/get-total-save-template',
607 array(
608 'methods' => 'get',
609 'callback' => array( $this, 'sp_get_save_total_template' ),
610 'permission_callback' => function () {
611 return current_user_can( 'manage_options' );
612 },
613 )
614 );
615 }
616
617
618 /**
619 * Get all save template function
620 *
621 * @return int
622 */
623 public function sp_get_save_total_template() {
624 $args = array(
625 'post_type' => 'sp_post_template',
626 'post_status' => 'any',
627 'posts_per_page' => -1,
628 'fields' => 'ids',
629 );
630
631 $ids = get_posts( $args );
632
633 return count( $ids );
634 }
635
636 /**
637 * Get Modules Settings function
638 *
639 * @return rest_response
640 */
641 public function get_module_settings() {
642 $settings['post-badges'] = get_option( 'sp_smart_post_module_post-badges', array() );
643 $settings['back-to-top'] = get_option( 'sp_smart_post_module_back-to-top', array() );
644 return rest_ensure_response( $settings );
645 }
646
647 /**
648 * Update Modules Settings function
649 *
650 * @param rest_response $request update module options.
651 *
652 * @return rest_response
653 */
654 public function smart_post_save_module_settings( $request ) {
655 $settings = $request->get_json_params();
656 // Basic validation( optional but recommended ) .
657 if ( ! is_array( $settings ) ) {
658 return new WP_Error( 'invalid_data', 'Settings must be an array', array( 'status' => 400 ) );
659 }
660
661 update_option(
662 'sp_smart_post_module_' . sanitize_key( $settings['module_name'] ),
663 $settings
664 );
665
666 return rest_ensure_response( $settings );
667 }
668
669 /**
670 * Handle AJAX request to update block settings.
671 */
672 public function sp_pcp_update_setting_options() {
673 // Check user capability.
674 if ( ! current_user_can( apply_filters( 'sp_pc_dashboard_capability', 'manage_options' ) ) ) {
675 wp_send_json_error( __( 'Unauthorized access.', 'post-carousel' ), 403 );
676 }
677
678 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
679 if ( ! wp_verify_nonce( $nonce, 'sp-update-block-settings-nonce' ) ) {
680 wp_send_json_error( __( 'Invalid nonce.', 'post-carousel' ) );
681 }
682
683 $options_json = isset( $_POST['optionData'] ) ? wp_unslash( $_POST['optionData'] ) : '';// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON validated via json_decode() and json_last_error().
684 $options = (array) json_decode( $options_json, true );
685
686 // Validate JSON was properly decoded.
687 if ( json_last_error() !== JSON_ERROR_NONE ) {
688 wp_send_json_error( __( 'Invalid JSON data.', 'post-carousel' ) );
689 }
690
691 if ( isset( $_POST['shareData'] ) ) {
692 $share_data = wp_unslash( $_POST['shareData'] );// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON validated via json_decode() and json_last_error().
693 $consent = filter_var( $share_data, FILTER_VALIDATE_BOOLEAN );
694 update_option( 'sp_pcp_allow_anonymous_data', $consent );
695 }
696
697 if ( is_array( $options ) ) {
698
699 // Get existing settings.
700 $existing_options = get_option( 'sp_post_carousel_settings', array() );
701
702 // Safety check.
703 if ( ! is_array( $existing_options ) ) {
704 $existing_options = array();
705 }
706
707 // Merge new values over old ones.
708 $merged_options = array_merge( $existing_options, $options );
709
710 // Save merged result.
711 update_option( 'sp_post_carousel_settings', $merged_options );
712 }
713
714 // Handle editor preference for promo modal.
715 if ( isset( $_POST['editorPreference'] ) ) {
716 $editor_preference = sanitize_key( wp_unslash( $_POST['editorPreference'] ) );
717 $allowed_editors = array( 'block_editor', 'classic_shortcode' );
718 if ( in_array( $editor_preference, $allowed_editors, true ) ) {
719 update_option( 'spsp_blocks_promo_modal_choice', $editor_preference, false );
720 } else {
721 delete_option( 'spsp_blocks_promo_modal_choice' );
722 }
723 }
724 wp_send_json_success(
725 array(
726 'options' => get_option( 'sp_post_carousel_settings' ),
727 )
728 );
729 }
730
731 /**
732 * Determine whether the "Upgrade to Pro" header notice should be shown.
733 *
734 * The notice appears only after 3 days from plugin activation and only
735 * until the user dismisses it. The activation timestamp is read from the
736 * existing `smart_post_show_activation_date` option (set once on install
737 * in class-smart-post-show-updates.php); the dismissal flag from the
738 * `sp_pcp_upgrade_notice_dismissed` option (default false).
739 *
740 * @return bool True when the notice should be displayed.
741 */
742 private function is_upgrade_notice_visible() {
743 $activation_date = (int) get_option( 'smart_post_show_activation_date' );
744
745 // Hide until 3 days have passed since activation (or if unknown).
746 if ( $activation_date <= 0 || ( $activation_date + ( DAY_IN_SECONDS * 3 ) ) > current_time( 'timestamp' ) ) {
747 return false;
748 }
749
750 // Hide once the user has dismissed it.
751 if ( (bool) get_option( 'sp_pcp_upgrade_notice_dismissed', false ) ) {
752 return false;
753 }
754
755 return true;
756 }
757
758 /**
759 * Permanently dismiss the "Upgrade to Pro" header notice.
760 *
761 * Stores the dismissal in the `sp_pcp_upgrade_notice_dismissed` option
762 * (default false) so the notice is never shown again once closed.
763 *
764 * @return void
765 */
766 public function sp_pcp_dismiss_upgrade_notice() {
767 // Check user capability.
768 if ( ! current_user_can( apply_filters( 'sp_pc_dashboard_capability', 'manage_options' ) ) ) {
769 wp_send_json_error( __( 'Unauthorized access.', 'post-carousel' ), 403 );
770 }
771
772 // Verify nonce.
773 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
774 if ( ! wp_verify_nonce( $nonce, 'sp-update-block-settings-nonce' ) ) {
775 wp_send_json_error( __( 'Invalid nonce.', 'post-carousel' ) );
776 }
777
778 // Sanitize the flag and persist the dismissal.
779 $dismissed = isset( $_POST['dismissed'] ) ? filter_var( wp_unslash( $_POST['dismissed'] ), FILTER_VALIDATE_BOOLEAN ) : true;
780 update_option( 'sp_pcp_upgrade_notice_dismissed', $dismissed );
781
782 wp_send_json_success(
783 array(
784 'dismissed' => (bool) get_option( 'sp_pcp_upgrade_notice_dismissed', false ),
785 )
786 );
787 }
788
789 /**
790 * Find blocks by name recursively in a parsed block structure.
791 *
792 * @param array $blocks The array of blocks to search.
793 * @param array $used_blocks The array to store the found block names.
794 * @return void
795 */
796 private function sp_pcp_find_blocks_recursive( $blocks, &$used_blocks ) {
797 foreach ( $blocks as $block ) {
798 if ( ! empty( $block['blockName'] ) && strpos( $block['blockName'], 'sp-smart-post-show/' ) === 0 ) {
799 $used_blocks[] = $block['blockName'];
800 }
801 if ( ! empty( $block['innerBlocks'] ) ) {
802 $this->sp_pcp_find_blocks_recursive( $block['innerBlocks'], $used_blocks );
803 }
804 }
805 }
806
807 /**
808 * Collect and send site usage data to the ShapedPlugin SDR endpoint.
809 *
810 * This function gathers non-sensitive site information such as
811 * WordPress version, PHP version, active plugins, used blocks,
812 * and basic site metadata, then sends it to a remote API endpoint
813 * for analytics purposes.
814 *
815 * Data collection is skipped for local and development environments.
816 *
817 * @param string $website_type Type of the website (e.g. blog, business, portfolio).
818 *
819 * @return void
820 */
821 public function sp_pc_collect_and_send_usage_data( $website_type ) {
822 // ---- Prevent sending data from local/dev sites ----
823 $site_url = home_url();
824 $host = wp_parse_url( $site_url, PHP_URL_HOST );
825
826 // Check for local sites.
827 if (
828 'localhost' === $host ||
829 '127.0.0.1' === $host ||
830 '::1' === $host ||
831 str_ends_with( $host, '.local' ) ||
832 str_ends_with( $host, '.test' ) ||
833 str_ends_with( $host, '.dev' ) ||
834 1 === preg_match( '/^192\.168\./', $host ) ||
835 1 === preg_match( '/^10\./', $host ) ||
836 1 === preg_match( '/^172\.(1[6-9]|2[0-9]|3[0-1])\./', $host )
837 ) {
838 return; // Stop here, do not collect or send data.
839 }
840
841 $theme = wp_get_theme();
842 // PHP version.
843 $php_version = phpversion();
844
845 // Database version.
846 $db_version = get_option( 'smart_post_show_db_version' );
847
848 $site_language = get_locale();
849
850 // Active plugins list.
851 $active_plugins = array();
852 $plugins = get_plugins();
853
854 foreach ( (array) get_option( 'active_plugins', array() ) as $plugin_path ) {
855 if ( isset( $plugins[ $plugin_path ] ) ) {
856 $active_plugins[] = array(
857 'name' => $plugins[ $plugin_path ]['Name'],
858 'version' => $plugins[ $plugin_path ]['Version'],
859 );
860 }
861 }
862
863 // Get used blocks.
864 $used_blocks = array();
865 global $wpdb;
866
867 // Search in posts, pages, custom post types, templates, etc.
868 $post_contents = $wpdb->get_col( $wpdb->prepare( "SELECT post_content FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_content LIKE %s", '%<!-- wp:sp-smart-post-show/%' ) );
869 foreach ( $post_contents as $post_content ) {
870 if ( has_blocks( $post_content ) ) {
871 $blocks = parse_blocks( $post_content );
872 $this->sp_pcp_find_blocks_recursive( $blocks, $used_blocks );
873 }
874 }
875
876 // Search in widgets.
877 $widget_blocks = get_option( 'widget_block' );
878 if ( ! empty( $widget_blocks ) && is_array( $widget_blocks ) ) {
879 foreach ( $widget_blocks as $widget_block ) {
880 if ( is_array( $widget_block ) && isset( $widget_block['content'] ) ) {
881 if ( has_blocks( $widget_block['content'] ) ) {
882 $blocks = parse_blocks( $widget_block['content'] );
883 $this->sp_pcp_find_blocks_recursive( $blocks, $used_blocks );
884 }
885 }
886 }
887 }
888
889 $used_blocks = array_values( $used_blocks );
890
891 // Collect data.
892 $data = array(
893 'user_email' => get_option( 'admin_email' ),
894 'site_url' => get_option( 'siteurl' ),
895 'site_type' => $website_type,
896 'site_language' => $site_language,
897 'theme_name' => $theme->get( 'Name' ),
898 'plugin_version' => SP_PC_VERSION,
899 'wp_version' => get_bloginfo( 'version' ),
900 'php_version' => $php_version,
901 'db_version' => $db_version,
902 'active_plugins' => $active_plugins,
903 'used_blocks' => $used_blocks,
904 );
905
906 wp_remote_post(
907 'https://api.shapedplugin.com/wp-json/spda/v1/smart-post-collect',
908 array(
909 'headers' => array(
910 'Content-Type' => 'application/json',
911 'x-api-key' => '2g7f3a67fd72fasdf645h45adsf3bidsec9309sfd',
912 'User-Agent' => 'sp-smart-post-data-collector/' . home_url(),
913 ),
914 'body' => wp_json_encode( $data ),
915 )
916 );
917 }
918
919 /**
920 * Handle user consent submission for anonymous data collection.
921 *
922 * Processes the admin notice form submission, verifies the nonce,
923 * stores the user’s consent choice, and prevents form resubmission
924 * on page refresh.
925 *
926 * @return void
927 */
928 public function sp_pcp_submit_user_consent() {
929 // Handle POST action for the notice buttons.
930 if (
931 isset( $_POST['sp_pcp_anonymous_data_action'] ) && check_admin_referer( 'sp_pcp_anonymous_data_action', 'sp_pcp_anonymous_data_nonce' )
932 ) {
933 if ( 'allow' === $_POST['sp_pcp_anonymous_data_action'] ) {
934 update_option( 'sp_pcp_allow_anonymous_data', true );
935 } elseif ( 'deny' === $_POST['sp_pcp_anonymous_data_action'] ) {
936 update_option( 'sp_pcp_ignored_consent_notice', true );
937 }
938
939 // Avoid resubmission on refresh.
940 echo '<script>window.location = window.location.href;</script>';
941 exit;
942 }
943 }
944
945
946 /**
947 * Handle AJAX request to get and save user consent.
948 *
949 * This function processes user consent preferences for anonymous data sharing.
950 * It requires user authentication and nonce verification for security.
951 * Updates options for setup wizard visit status, anonymous data sharing preference,
952 * and website type. If consent is granted, it sends collected site data to the
953 * Smart Post SDR service.
954 *
955 * @return void Sends JSON response with success or error message.
956 */
957 public function sp_smart_post_get_user_consent() {
958 // Check user capability.
959 if ( ! current_user_can( apply_filters( 'sp_pc_dashboard_capability', 'manage_options' ) ) ) {
960 wp_send_json_error( __( 'Unauthorized access.', 'post-carousel' ), 403 );
961 }
962
963 // Verify nonce.
964 $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
965 if ( ! wp_verify_nonce( $nonce, 'sp-update-block-settings-nonce' ) ) {
966 wp_send_json_error( __( 'Invalid nonce.', 'post-carousel' ) );
967 }
968
969 update_option( 'sp_smart_post_setup_wizard_visited', true );
970 // Get consent.
971 $consent = isset( $_POST['shareData'] ) ? json_decode( wp_unslash( $_POST['shareData'] ), true ) : false;// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON validated via json_decode() and json_last_error().
972 $website_type = isset( $_POST['website_type'] ) ? sanitize_text_field( wp_unslash( $_POST['website_type'] ) ) : '';
973 update_option( 'sp_ua_site_type', $website_type );
974
975 if ( $consent ) {
976 update_option( 'sp_pcp_allow_anonymous_data', true );
977 $this->sp_pc_collect_and_send_usage_data( $website_type );
978 } else {
979 update_option( 'sp_pcp_allow_anonymous_data', false );
980 }
981
982 wp_send_json_success(
983 array(
984 'message' => 'Successfully done',
985 )
986 );
987 }
988
989 /**
990 * Check whether the consent notice delay period has passed.
991 *
992 * Compares the stored notice start time with the current time
993 * to determine if the specified delay duration has elapsed.
994 *
995 * @param int $days Number of days to wait before showing the notice.
996 * @return bool True if the delay has passed, false otherwise.
997 */
998 private function has_notice_delay_passed( $days = 7 ) {
999 $start_time = get_option( 'sp_pcp_consent_notice_start_time' );
1000
1001 if ( ! $start_time ) {
1002 return false;
1003 }
1004
1005 return ( time() - $start_time ) >= ( DAY_IN_SECONDS * $days );
1006 }
1007
1008 /**
1009 * Set the consent notice start time if it does not already exist.
1010 *
1011 * Stores the current timestamp to track when the consent notice
1012 * delay period begins.
1013 *
1014 * @return void
1015 */
1016 private function maybe_set_notice_start_time() {
1017 if ( ! get_option( 'sp_pcp_consent_notice_start_time' ) ) {
1018 update_option( 'sp_pcp_consent_notice_start_time', time() );
1019 }
1020 }
1021
1022 /**
1023 * Conditionally display the anonymous data consent notice.
1024 *
1025 * Ensures the current user has sufficient permissions, the notice
1026 * has not been ignored, there is at least one saved template or
1027 * classic shortcode, and the delay period has passed before
1028 * showing the notice.
1029 *
1030 * @return void
1031 */
1032 public function maybe_show_user_consent_notice() {
1033
1034 if ( ! current_user_can( 'manage_options' ) ) {
1035 return;
1036 }
1037
1038 $ignored_consent_notice = get_option( 'sp_pcp_ignored_consent_notice', false );
1039 $allow_anonymous_data = get_option( 'sp_pcp_allow_anonymous_data', false );
1040
1041 // Do not show if already allowed or ignored.
1042 if ( $allow_anonymous_data || $ignored_consent_notice ) {
1043 return;
1044 }
1045
1046 // Check if there are any saved templates or classic shortcodes.
1047 $saved_template_count = wp_count_posts( 'sp_post_template' );
1048 $classic_shortcode_count = wp_count_posts( 'sp_post_carousel' );
1049
1050 // Total published count from both post types.
1051 $total_saved_templates = isset( $saved_template_count->publish ) ? $saved_template_count->publish : 0;
1052 $total_classic_shortcodes = isset( $classic_shortcode_count->publish ) ? $classic_shortcode_count->publish : 0;
1053
1054 // Do not show notice if both lists are empty.
1055 if ( $total_saved_templates < 1 && $total_classic_shortcodes < 1 ) {
1056 return;
1057 }
1058
1059 // delay logic (3 days).
1060 $this->maybe_set_notice_start_time();
1061
1062 if ( ! $this->has_notice_delay_passed( 3 ) ) {
1063 return;
1064 }
1065
1066 // Finally show notice.
1067 $this->sp_pcp_notice_for_user_consent();
1068 }
1069
1070 /**
1071 * Render the anonymous data consent admin notice.
1072 *
1073 * Displays an admin notice prompting the user to allow or deny
1074 * anonymous data collection if consent has not yet been given.
1075 *
1076 * @return void
1077 */
1078 public function sp_pcp_notice_for_user_consent() {
1079 ?>
1080 <style>
1081 .sp_pcp-anonymous-data-notice {
1082 background-color: #ffffff;
1083 border: none;
1084 border: 1px solid rgba(204, 204, 204, 1);
1085 border-left: 4px solid #5715c3;
1086 margin-bottom: 20px;
1087 display: flex;
1088 padding: 14px;
1089 align-items: flex-start;
1090 gap: 16px;
1091 box-shadow: 0 16px 32px -4px rgba(12, 12, 13, 0.05), 0 4px 4px -4px rgba(12, 12, 13, 0.02);
1092 position: relative;
1093 border-radius: 4px;
1094 }
1095
1096 button.sp_pcp_anonymous_data_cross {
1097 border: none;
1098 background: transparent;
1099 position: absolute;
1100 top: 0;
1101 right: 7px;
1102 cursor: pointer;
1103 color: #b6b6b6;
1104 font-size: 16px;
1105 }
1106
1107 .sp_pcp-anonymous-data-notice-wrapper {
1108 display: flex;
1109 gap: 26px;
1110 }
1111
1112 .sp_pcp-anonymous-data-notice img {
1113 width: 52px;
1114 border-radius: 4px;
1115 }
1116 .sp_pcp-anonymous-data-notice h3 {
1117 font-weight: 600;
1118 font-size: 18px;
1119 color: #2C2D2F;
1120 margin: 0 0 8px 0;
1121 }
1122 .sp_pcp-anonymous-data-notice p, .sp_pcp-anonymous-data-notice a {
1123 color: #6E6F72;
1124 font-size: 14px;
1125 margin: 0 0 2px 0;
1126 }
1127 .sp_pcp-anonymous-data-notice a {
1128 text-decoration: underline;
1129 }
1130 .sp_pcp-anonymous-data-notice .button {
1131 font-size: 14px;
1132 font-weight: 500;
1133 }
1134 .sp_pcp-anonymous-data-notice .button {
1135 font-size: 14px;
1136 font-weight: 500;
1137 background-color: #ffffff;
1138 color: #6E6F72;
1139 border: 1px solid #ECEDF0;
1140 transition: all 0.2s ease;
1141 margin-top: 8px;
1142 }
1143 .sp_pcp-anonymous-data-notice .button:hover {
1144 border: 1px solid #b7b8bb;
1145 color: #6E6F72;
1146 background-color: #ffffff;
1147 }
1148 .sp_pcp-anonymous-data-notice .sp_pcp_anonymous_data_connect {
1149 background-color: rgba(30, 30, 30, 1);
1150 color: #ffffff;
1151 line-height: 14px;
1152 border-radius: 4px;
1153 font-size: 13px;
1154 }
1155 .sp_pcp-anonymous-data-notice .sp_pcp_anonymous_data_connect:hover {
1156 background-color: rgb(46, 46, 46);
1157 color: #ffffff;
1158 }
1159 .sp_pcp-anonymous-data-notice .sp_pcp_anonymous_data_connect:focus {
1160 border: none;
1161 box-shadow: none;
1162 out-line: none;
1163 }
1164 </style>
1165
1166 <div class="notice notice-info sp_pcp-anonymous-data-notice">
1167 <div class="sp_pcp-anonymous-data-notice-wrapper">
1168 <div>
1169 <h3>
1170 <?php esc_html_e( 'Help us make Smart Post even more awesome?', 'post-carousel' ); ?>
1171 </h3>
1172 <p>
1173 <?php
1174 esc_html_e(
1175 'Allow us to collect non-sensitive diagnostic data to resolve problems faster and improve performance.',
1176 'post-carousel'
1177 );
1178 ?>
1179 <a href="https://wpsmartpost.com/information-we-collect/" target="_blank"><?php esc_html_e( 'Learn More', 'post-carousel' ); ?></a>
1180 </p>
1181 </div>
1182 <div style="display:flex; gap:10px;">
1183 <form method="post" style="display:inline;">
1184 <?php wp_nonce_field( 'sp_pcp_anonymous_data_action', 'sp_pcp_anonymous_data_nonce' ); ?>
1185 <input type="hidden" name="sp_pcp_anonymous_data_action" value="allow" />
1186 <button type="submit" class="sp_pcp_anonymous_data_connect button">
1187 <?php esc_html_e( 'Accept & Close', 'post-carousel' ); ?>
1188 </button>
1189 </form>
1190
1191 <form method="post" style="display:inline;">
1192 <?php wp_nonce_field( 'sp_pcp_anonymous_data_action', 'sp_pcp_anonymous_data_nonce' ); ?>
1193 <input type="hidden" name="sp_pcp_anonymous_data_action" value="deny" />
1194 <button type="submit" class="sp_pcp_anonymous_data_cross dashicons dashicons-dismiss">
1195 </button>
1196 </form>
1197 </div>
1198 </div>
1199 </div>
1200 <?php
1201 }
1202 }
1203 }
1204