PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.2
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.2
6.2.2 6.2.1 6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 All 155 releases
← All changes | modules/cost-tracker/classes/class-cost-tracker-admin.php +434 -58 5.1 → 6.2.2 View file →
@@ -14,11 +14,17 @@
14 14 use MainWP\Dashboard\MainWP_Utility;
15 15 use MainWP\Dashboard\MainWP_System_Utility;
16 16 use MainWP\Dashboard\MainWP_Logger;
17 17 use MainWP\Dashboard\MainWP_Post_Handler;
18 +use MainWP\Dashboard\MainWP_Settings;
18 19 use MainWP\Dashboard\MainWP_Settings_Indicator;
20 +use MainWP\Dashboard\MainWP_Exception;
21 +use MainWP\Dashboard\Module\CostTracker\Cost_Tracker_Import;
19 22
20 -use function MainWP\Dashboard\mainwp_current_user_have_right;
23 +// Exit if accessed directly.
24 +if ( ! defined( 'ABSPATH' ) ) {
25 + exit;
26 +}
21 27
22 28 /**
23 29 * Class Cost_Tracker_Admin
24 30 */
@@ -82,8 +88,10 @@
82 88 add_filter( 'mainwp_init_primary_menu_items', array( $this, 'hook_init_primary_menu_items' ), 10, 2 );
83 89 add_filter( 'mainwp_module_cost_tracker_get_default_cost_fields', array( $this, 'hook_get_default_cost_fields' ), 10, 2 );
84 90 add_filter( 'mainwp_module_cost_tracker_get_next_renewal', array( $this, 'hook_get_next_renewal' ), 10, 3 );
85 91 add_action( 'mainwp_delete_site', array( $this, 'hook_delete_site' ), 10, 3 );
92 + add_action( 'mainwp_client_deleted', array( $this, 'hook_delete_client' ) );
93 + add_action( 'mainwp_site_tag_action', array( $this, 'hook_site_tag_action' ), 10, 2 );
86 94 add_filter( 'mainwp_module_cost_tracker_get_total_cost', array( $this, 'hook_get_total_cost' ), 10, 2 );
87 95 }
88 96
89 97
@@ -89,9 +97,9 @@
89 97
90 98 /**
91 99 * Initiate Hooks
92 100 *
93 - * Initiates hooks for the Subscription extension.
101 + * Initiates hooks for the Cost Tracker module.
94 102 */
95 103 public function init() {
96 104 add_action( 'admin_init', array( &$this, 'admin_init' ) );
97 105 add_action( 'mainwp_help_sidebar_content', array( $this, 'mainwp_help_content' ) );
@@ -125,8 +133,57 @@
125 133 );
126 134 }
127 135
128 136 /**
137 + * Method hook_delete_client()
138 + *
139 + * Prunes the cost lookup rows of a deleted client. Left behind, they keep matching the
140 + * client filter on the Costs page long after the client is gone.
141 + *
142 + * @param mixed $client client object.
143 + *
144 + * @return bool result.
145 + */
146 + public function hook_delete_client( $client ) {
147 + if ( empty( $client ) || empty( $client->client_id ) ) {
148 + return false;
149 + }
150 + return MainWP_DB::instance()->delete_lookup_items(
151 + 'object_id',
152 + array(
153 + 'item_name' => 'cost',
154 + 'object_id' => $client->client_id,
155 + 'object_names' => array( 'client' ),
156 + )
157 + );
158 + }
159 +
160 + /**
161 + * Method hook_site_tag_action()
162 + *
163 + * Prunes the cost lookup rows of a deleted tag. The hook also fires on create and update,
164 + * which leave the rows alone.
165 + *
166 + * @param mixed $group tag object.
167 + * @param string $action tag action.
168 + *
169 + * @return bool result.
170 + */
171 + public function hook_site_tag_action( $group, $action ) {
172 + if ( 'deleted' !== $action || empty( $group ) || empty( $group->id ) ) {
173 + return false;
174 + }
175 + return MainWP_DB::instance()->delete_lookup_items(
176 + 'object_id',
177 + array(
178 + 'item_name' => 'cost',
179 + 'object_id' => $group->id,
180 + 'object_names' => array( 'tag' ),
181 + )
182 + );
183 + }
184 +
185 + /**
129 186 * Admin Init
130 187 *
131 188 * Initiates admin hooks.
132 189 */
@@ -132,16 +189,17 @@
132 189 */
133 190 public function admin_init() {
134 191
135 192 MainWP_Post_Handler::instance()->add_action( 'mainwp_module_cost_tracker_upload_product_icon', array( Cost_Tracker_Add_Edit::get_instance(), 'ajax_upload_product_icon' ) );
193 + MainWP_Post_Handler::instance()->add_action( 'mainwp_cost_tracker_import_cost', array( static::class, 'ajax_import_cost' ) );
136 194
137 195 $this->handle_edit_cost_tracker_post();
138 196 $this->handle_settings_post();
139 197
140 - $allow_pages = array( 'ManageCostTracker', 'CostTrackerAdd', 'CostTrackerSettings', 'CostSummary' );
198 + $allow_pages = array( 'ManageCostTracker', 'CostTrackerAdd', 'CostTrackerSettings', 'CostSummary', 'CostTrackerImport' );
141 199 if ( isset( $_GET['page'] ) && in_array( $_GET['page'], $allow_pages, true ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
142 200 $base_url = Cost_Tracker_Manager::get_location_path( 'url' );
143 - wp_enqueue_script( 'mainwp-module-cost-tracker-extension', $base_url . 'ui/js/cost-tracker.js', array( 'jquery' ), $this->version, true );
201 + wp_enqueue_script( 'mainwp-module-cost-tracker-extension', $base_url . 'ui/js/cost-tracker.js', array( 'jquery', 'mainwp' ), $this->version, true );
144 202 if ( 'CostSummary' === $_GET['page'] ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
145 203 add_filter(
146 204 'mainwp_admin_enqueue_scripts',
147 205 function ( $scripts ) {
@@ -151,8 +209,11 @@
151 209 return $scripts;
152 210 }
153 211 );
154 212 }
213 + if ( 'CostTrackerImport' === $_GET['page'] ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
214 + wp_enqueue_script( 'mainwp-module-cost-tracker-import', $base_url . 'ui/js/cost-tracker-import.js', array( 'jquery' ), $this->version, true );
215 + }
155 216 }
156 217 }
157 218
158 219 /**
@@ -223,10 +284,10 @@
223 284 'multi_site' => esc_html__( 'Multiple-Site License', 'mainwp' ),
224 285 );
225 286 $product_types = static::get_product_types();
226 287 $payment_types = array(
227 - 'subscription' => esc_html__( 'Subscription', 'mainwp' ),
228 - 'lifetime' => esc_html__( 'Lifetime', 'mainwp' ),
288 + 'subscription' => esc_html__( 'Recurring (Subscription)', 'mainwp' ),
289 + 'lifetime' => esc_html__( 'One-time (Lifetime)', 'mainwp' ),
229 290 );
230 291 $payment_methods = static::get_payment_methods();
231 292 $renewal_frequency = static::get_renewal_frequency();
232 293 $cost_status = static::get_cost_status();
@@ -291,15 +352,15 @@
291 352 );
292 353
293 354 add_submenu_page(
294 355 'mainwp_tab',
295 - esc_html__( 'Settings', 'mainwp' ),
296 - '<div class="mainwp-hidden">' . esc_html__( 'Settings', 'mainwp' ) . '</div>',
356 + esc_html__( 'Import Costs', 'mainwp' ),
357 + '<div class="mainwp-hidden">' . esc_html__( 'Import Costs', 'mainwp' ) . '</div>',
297 358 'read',
298 - 'CostTrackerSettings',
359 + 'CostTrackerImport',
299 360 array(
300 - Cost_Tracker_Settings::get_instance(),
301 - 'render_settings_page',
361 + Cost_Tracker_Import::get_instance(),
362 + 'render_import_costs',
302 363 )
303 364 );
304 365
305 366 /**
@@ -332,8 +393,12 @@
332 393 *
333 394 * Run on page load.
334 395 */
335 396 public static function on_load_summary_page() {
397 + if ( ! \mainwp_current_user_can( 'dashboard', 'access_cost_summary_dashboard' ) && \mainwp_current_user_can( 'dashboard', 'manage_cost_tracker' ) ) {
398 + wp_safe_redirect( 'admin.php?page=ManageCostTracker' );
399 + exit();
400 + }
336 401 Cost_Tracker_Summary::instance()->on_load_page( static::$page );
337 402 }
338 403
339 404 /**
@@ -343,9 +408,9 @@
343 408 */
344 409 public static function init_left_menu( $subPages ) {
345 410 MainWP_Menu::add_left_menu(
346 411 array(
347 - 'title' => esc_html__( 'Cost Tracker', 'mainwp' ),
412 + 'title' => esc_html__( 'Costs', 'mainwp' ),
348 413 'parent_key' => 'mainwp_tab',
349 414 'slug' => 'CostSummary',
350 415 'href' => 'admin.php?page=CostSummary',
351 416 'icon' => '<i class="dollar sign icon"></i>',
@@ -353,8 +418,15 @@
353 418 'nosubmenu' => true,
354 419 ),
355 420 0
356 421 );
422 +
423 + $sum_item_class = '';
424 +
425 + if ( ! \mainwp_current_user_can( 'dashboard', 'access_cost_summary_dashboard' ) ) {
426 + $sum_item_class = 'hidden-menu-item';
427 + }
428 +
357 429 $init_sub_subleftmenu = array(
358 430 array(
359 431 'title' => esc_html__( 'Cost Summary', 'mainwp' ),
360 432 'parent_key' => 'CostSummary',
@@ -360,8 +432,9 @@
360 432 'parent_key' => 'CostSummary',
361 433 'href' => 'admin.php?page=CostSummary',
362 434 'slug' => 'CostSummary',
363 435 'right' => 'manage_cost_tracker',
436 + 'item_class' => $sum_item_class,
364 437 ),
365 438 array(
366 439 'title' => esc_html__( 'Manage Costs', 'mainwp' ),
367 440 'parent_key' => 'CostSummary',
@@ -376,13 +449,13 @@
376 449 'slug' => 'CostTrackerAdd',
377 450 'right' => '',
378 451 ),
379 452 array(
380 - 'title' => esc_html__( 'Settings', 'mainwp' ),
453 + 'title' => esc_html__( 'Import Costs', 'mainwp' ),
381 454 'parent_key' => 'CostSummary',
382 - 'href' => 'admin.php?page=CostTrackerSettings',
383 - 'slug' => 'CostTrackerSettings',
384 - 'right' => '',
455 + 'href' => 'admin.php?page=CostTrackerImport',
456 + 'slug' => 'CostTrackerImport',
457 + 'right' => 'manage_cost_tracker',
385 458 ),
386 459 );
387 460
388 461 MainWP_Menu::init_subpages_left_menu( $subPages, $init_sub_subleftmenu, 'ManageCostTracker', 'ManageCostTracker' );
@@ -404,10 +477,17 @@
404 477 *
405 478 * @param string $shownPage Current Page.
406 479 */
407 480 public static function render_header( $shownPage = '' ) { //phpcs:ignore -- NOSONAR - complex.
481 + $title = esc_html__( 'Operational Costs', 'mainwp' );
482 + if ( 'edit' === $shownPage ) {
483 + $title = esc_html__( 'Edit Operational Cost', 'mainwp' );
484 + } elseif ( 'add' === $shownPage ) {
485 + $title = esc_html__( 'Add Operational Cost', 'mainwp' );
486 + }
487 +
408 488 $params = array(
409 - 'title' => esc_html__( 'Cost Tracker', 'mainwp' ),
489 + 'title' => $title,
410 490 'which' => 'page_cost_tracker_overview',
411 491 'wrap_class' => 'mainwp-module-cost-tracker-content-wrapper',
412 492 );
413 493
@@ -414,11 +494,11 @@
414 494 MainWP_UI::render_top_header( $params );
415 495
416 496 $renderItems = array();
417 497
418 - if ( mainwp_current_user_have_right( 'dashboard', 'manage_cost_tracker' ) ) {
498 + if ( \mainwp_current_user_can( 'dashboard', 'manage_cost_tracker' ) ) {
419 499 $renderItems[] = array(
420 - 'title' => esc_html__( 'Overview', 'mainwp' ),
500 + 'title' => esc_html__( 'Operations', 'mainwp' ),
421 501 'href' => 'admin.php?page=ManageCostTracker',
422 502 'active' => ( '' === $shownPage ) ? true : false,
423 503 );
424 504 }
@@ -430,8 +510,16 @@
430 510 'active' => ( 'add' === $shownPage ) ? true : false,
431 511 );
432 512 }
433 513
514 + if ( ! MainWP_Menu::is_disable_menu_item( 3, 'CostTrackerImport' ) ) {
515 + $renderItems[] = array(
516 + 'title' => esc_html__( 'Import Costs', 'mainwp' ),
517 + 'href' => 'admin.php?page=CostTrackerImport',
518 + 'active' => ( 'ImportCosts' === $shownPage ) ? true : false,
519 + );
520 + }
521 +
434 522 $cost_id = isset( $_GET['id'] ) ? intval( $_GET['id'] ) : 0; //phpcs:ignore WordPress.Security.NonceVerification.Recommended
435 523 if ( $cost_id ) {
436 524 $renderItems[] = array(
437 525 'title' => esc_html__( 'Edit Cost Tracker', 'mainwp' ),
@@ -439,15 +527,8 @@
439 527 'active' => ( 'edit' === $shownPage ) ? true : false,
440 528 );
441 529 }
442 530
443 - if ( ! MainWP_Menu::is_disable_menu_item( 3, 'CostTrackerSettings' ) ) {
444 - $renderItems[] = array(
445 - 'title' => esc_html__( 'Settings', 'mainwp' ),
446 - 'href' => 'admin.php?page=CostTrackerSettings',
447 - 'active' => ( 'settings' === $shownPage ) ? true : false,
448 - );
449 - }
450 531 if ( isset( static::$subPages ) && is_array( static::$subPages ) ) {
451 532 foreach ( static::$subPages as $subPage ) {
452 533 if ( MainWP_Menu::is_disable_menu_item( 3, 'ManageCostTracker' . $subPage['slug'] ) ) {
453 534 continue;
@@ -473,9 +554,11 @@
473 554 * @return mixed Save output.
474 555 */
475 556 public static function handle_edit_cost_tracker_post() { //phpcs:ignore -- NOSONAR - complex method.
476 557
477 - if ( ! isset( $_POST['mwp_cost_tracker_editing_submit'] ) || ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'module_cost_tracker_edit_nonce' ) ) {
558 + $updating = ! empty( $_POST['mainwp_module_cost_tracker_edit_id'] ) ? intval( $_POST['mainwp_module_cost_tracker_edit_id'] ) : ''; //phpcs:ignore -- -- NOSONAR - ok.
559 +
560 + if ( ! isset( $_POST['mwp_cost_tracker_editing_submit'] ) || ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'module_cost_tracker_edit_nonce' . $updating ) ) {
478 561 return;
479 562 }
480 563 //phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
481 564 $last_renewal = isset( $_POST['mainwp_module_cost_tracker_edit_last_renewal'] ) ? strtotime( wp_unslash( $_POST['mainwp_module_cost_tracker_edit_last_renewal'] ) ) : 0;
@@ -537,19 +620,28 @@
537 620 $current = Cost_Tracker_DB::get_instance()->get_cost_tracker_by( 'id', $update['id'] );
538 621 }
539 622
540 623 //phpcs:enable
541 - $err_msg = '';
542 - $output = false;
624 + $err_msg = '';
625 + $output = false;
626 + $saved_id = 0;
543 627 try {
544 628 $output = Cost_Tracker_DB::get_instance()->update_cost_tracker( $update );
545 629 if ( $output && ! empty( $output->id ) ) {
630 + $saved_id = $output->id;
546 631 Cost_Tracker_DB::get_instance()->update_selected_lookup_cost( $output->id, $selected_sites, $selected_groups, $selected_clients );
547 632 if ( $current && ! empty( $current->cost_icon ) && false === strpos( $current->cost_icon, 'deficon:' ) && $current->cost_icon !== $update['cost_icon'] ) {
548 633 Cost_Tracker_Add_Edit::get_instance()->delete_product_icon_file( $current->cost_icon );
549 634 }
635 + $next_today = static::calc_next_renewal_today( $output, $next_renewal );
636 + Cost_Tracker_DB::get_instance()->update_cost_tracker(
637 + array(
638 + 'id' => $output->id,
639 + 'next_renewal_today' => $next_today,
640 + )
641 + );
550 642 }
551 - } catch ( \Exception $ex ) {
643 + } catch ( MainWP_Exception $ex ) {
552 644 $err_msg = $ex->getMessage();
553 645 }
554 646
555 647 $msg_id = ! empty( $update['id'] ) ? intval( $update['id'] ) : 0;
@@ -557,10 +649,24 @@
557 649 if ( ! empty( $err_msg ) ) {
558 650 set_transient( 'mainwp_cost_tracker_update_error_' . $msg_id, $err_msg, HOUR_IN_SECONDS );
559 651 }
560 652
653 + $snapshot_info = static::get_costs_snapshot_info();
654 +
655 + /**
656 + * Action hook for cost tracker update or insert.
657 + *
658 + * @since 6.0
659 + *
660 + * @param array $update The data used for the update.
661 + * @param int $saved_id The saved cost tracker ID.
662 + * @param string $err_msg The error message, if any.
663 + * @param array $snapshot_info Costs snapshot info.
664 + */
665 + do_action( 'mainwp_cost_tracker_update_cost', $update, $saved_id, $err_msg, $snapshot_info );
666 +
561 667 if ( empty( $err_msg ) && ! empty( $output ) ) { // success.
562 - wp_safe_redirect( admin_url( 'admin.php?page=CostTrackerAdd&message=1&id=' . $output->id ) );
668 + wp_safe_redirect( admin_url( 'admin.php?page=ManageCostTracker&message=1' ) );
563 669 } elseif ( ! empty( $err_msg ) ) { // error.
564 670 wp_safe_redirect( admin_url( 'admin.php?page=CostTrackerAdd&message=2&id=' . $msg_id ) );
565 671 }
566 672 exit();
@@ -565,9 +671,45 @@
565 671 }
566 672 exit();
567 673 }
568 674
675 + /**
676 + * Handle get_costs_snapshot_info
677 + *
678 + * @return array
679 + */
680 + public static function get_costs_snapshot_info() {
569 681
682 + $costs = Cost_Tracker_DB::get_instance()->get_cost_tracker_by( 'all' );
683 +
684 + $total = 0;
685 + $recurring = 0;
686 + $one_time = 0;
687 + $active = 0;
688 +
689 + if ( is_array( $costs ) ) {
690 + $total = count( $costs );
691 + foreach ( $costs as $cost ) {
692 + if ( 'active' === $cost->cost_status ) {
693 + ++$active;
694 + }
695 + if ( 'subscription' === $cost->type ) {
696 + ++$recurring;
697 + } else {
698 + ++$one_time;
699 + }
700 + }
701 + }
702 +
703 + return array(
704 + 'cost_items_total' => $total,
705 + 'cost_items_recurring_count' => $recurring,
706 + 'cost_items_one_time_count' => $one_time,
707 + 'cost_items_active_count' => $active,
708 + );
709 + }
710 +
711 +
570 712 /**
571 713 * Settigns Post
572 714 *
573 715 * Handles the save settings post request.
@@ -578,8 +720,10 @@
578 720 if ( ! isset( $_POST['mwp_cost_tracker_settings_submit'] ) || ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'module_cost_tracker_settings_nonce' ) ) {
579 721 return;
580 722 }
581 723
724 + $old_settings = MainWP_Settings::get_all_settings_values();
725 +
582 726 $all_opts = Cost_Tracker_Utility::get_instance()->get_all_options();
583 727 $currency = isset( $_POST['mainwp_module_cost_tracker_settings_currency'] ) ? sanitize_text_field( wp_unslash( $_POST['mainwp_module_cost_tracker_settings_currency'] ) ) : '';
584 728 $currency_format = isset( $_POST['mainwp_module_cost_tracker_currency_format'] ) ? wp_unslash( $_POST['mainwp_module_cost_tracker_currency_format'] ) : array(); //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
585 729 $currency_format = Cost_Tracker_Utility::validate_currency_settings( $currency_format );
@@ -608,8 +752,22 @@
608 752 $all_opts = apply_filters( 'mainwp_module_cost_tracker_before_save_settings', $all_opts );
609 753
610 754 Cost_Tracker_Utility::get_instance()->save_options( $all_opts );
611 755
756 + $new_settings = MainWP_Settings::get_all_settings_values();
757 +
758 + /**
759 + * Action: mainwp_after_save_settings
760 + *
761 + * Fires after save settings.
762 + *
763 + * @since 6.0
764 + *
765 + * @param array $new_settings The new settings.
766 + * @param array $old_settings The old settings.
767 + */
768 + do_action( 'mainwp_after_save_settings', $new_settings, $old_settings );
769 +
612 770 wp_safe_redirect( admin_url( 'admin.php?page=CostTrackerSettings&message=1' ) );
613 771 exit();
614 772 }
615 773
@@ -652,8 +810,192 @@
652 810 return $valid_arr;
653 811 }
654 812
655 813 /**
814 + * Extract the scalar fields of an imported cost.
815 + *
816 + * @param array $cost Decoded cost object.
817 + *
818 + * @return array|false Fields for the DB write, or false when one of them carries a shape
819 + * that cannot be stored.
820 + */
821 + private static function import_cost_fields( $cost ) {
822 + // Coercing instead of refusing stored a price of 1 for [ 5 ] and a last renewal of
823 + // 2026 for '2026-01-15', neither of which the admin pasted.
824 + $spec = array(
825 + 'url' => 'url',
826 + 'type' => 'text',
827 + 'product_type' => 'text',
828 + 'license_type' => 'text',
829 + 'price' => 'float',
830 + 'payment_method' => 'text',
831 + 'renewal_type' => 'text',
832 + 'last_renewal' => 'int',
833 + 'cost_status' => 'text',
834 + );
835 +
836 + $fields = array();
837 +
838 + foreach ( $spec as $field => $kind ) {
839 + $value = isset( $cost[ $field ] ) ? $cost[ $field ] : null;
840 +
841 + if ( 'float' === $kind || 'int' === $kind ) {
842 + if ( null === $value ) {
843 + $fields[ $field ] = 0;
844 + continue;
845 + }
846 + if ( 'int' === $kind ) {
847 + // is_numeric() lets 1700000000.9 and '1e9' through, and intval() would
848 + // quietly turn them into a renewal timestamp nobody pasted.
849 + if ( ! is_int( $value ) && ! ( is_string( $value ) && preg_match( '/^-?\d+$/', $value ) ) ) {
850 + return false;
851 + }
852 + $fields[ $field ] = (int) $value;
853 + continue;
854 + }
855 + if ( ! is_numeric( $value ) ) {
856 + return false;
857 + }
858 + $fields[ $field ] = floatval( $value );
859 + continue;
860 + }
861 +
862 + if ( null === $value ) {
863 + $fields[ $field ] = '';
864 + continue;
865 + }
866 +
867 + if ( ! is_string( $value ) ) {
868 + return false;
869 + }
870 +
871 + $fields[ $field ] = 'url' === $kind ? esc_url_raw( $value ) : sanitize_text_field( $value );
872 + }
873 +
874 + return $fields;
875 + }
876 +
877 + /**
878 + * AJAX handler for importing cost tracker entries.
879 + *
880 + * @return never wp_send_json_*() exits.
881 + */
882 + public static function ajax_import_cost() { //phpcs:ignore -- NOSONAR - complex method.
883 + MainWP_Post_Handler::instance()->secure_request( 'mainwp_cost_tracker_import_cost' );
884 +
885 + if ( ! isset( $_POST['encoded_data'] ) || ! is_string( $_POST['encoded_data'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- secure_request() verifies the nonce above.
886 + return wp_send_json_error(
887 + array(
888 + 'message' => esc_html__( 'No cost data provided', 'mainwp' ),
889 + )
890 + );
891 + }
892 +
893 + $encoded_data = wp_unslash( $_POST['encoded_data'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce verified by secure_request() above; a JSON string, and each decoded field is type-checked and sanitized below before use.
894 + $cost_data_raw = json_decode( $encoded_data, true );
895 +
896 + // The decoded fields feed string functions, so a non-object cost shape is refused here.
897 + if ( ! is_array( $cost_data_raw ) || ! isset( $cost_data_raw['cost'] ) || ! is_array( $cost_data_raw['cost'] ) ) {
898 + return wp_send_json_error(
899 + array(
900 + 'message' => esc_html__( 'Invalid cost data format', 'mainwp' ),
901 + )
902 + );
903 + }
904 +
905 + $cost_name = isset( $cost_data_raw['cost']['name'] ) && is_string( $cost_data_raw['cost']['name'] ) ? sanitize_text_field( $cost_data_raw['cost']['name'] ) : '';
906 +
907 + if ( empty( $cost_name ) ) {
908 + return wp_send_json_error(
909 + array(
910 + 'message' => esc_html__( 'Cost name is required', 'mainwp' ),
911 + )
912 + );
913 + }
914 +
915 + $cost_fields = static::import_cost_fields( $cost_data_raw['cost'] );
916 +
917 + if ( false === $cost_fields ) {
918 + return wp_send_json_error(
919 + array(
920 + 'message' => esc_html__( 'Invalid cost data format', 'mainwp' ),
921 + )
922 + );
923 + }
924 +
925 + $cost_data = array_merge( array( 'name' => $cost_name ), $cost_fields );
926 +
927 + $select_sites = $cost_data_raw['cost']['select_sites'] ?? array();
928 +
929 + if ( ! is_array( $select_sites ) ) {
930 + return wp_send_json_error(
931 + array(
932 + 'message' => esc_html__( 'Invalid cost data format', 'mainwp' ),
933 + )
934 + );
935 + }
936 +
937 + $selected_sites = array();
938 +
939 + foreach ( $select_sites as $url ) {
940 + if ( ! is_string( $url ) ) {
941 + continue;
942 + }
943 + $url = esc_url_raw( trim( $url ) );
944 + if ( empty( $url ) ) {
945 + continue;
946 + }
947 +
948 + if ( '/' !== substr( $url, -1 ) ) {
949 + $url .= '/';
950 + }
951 +
952 + $website = MainWP_DB::instance()->get_websites_by_url( $url );
953 + $site_id = ! empty( $website ) ? current( $website )->id : null;
954 +
955 + if ( ! empty( $site_id ) ) {
956 + $selected_sites[] = $site_id;
957 + }
958 + }
959 +
960 + $cost_data['sites'] = ! empty( $selected_sites ) ? wp_json_encode( array_values( array_unique( array_map( 'intval', $selected_sites ) ) ) ) : '';
961 +
962 + try {
963 + $inserted = Cost_Tracker_DB::get_instance()->update_cost_tracker( $cost_data );
964 + } catch ( \MainWP_Exception $e ) {
965 + return wp_send_json_error(
966 + array(
967 + 'message' => $e->getMessage(),
968 + )
969 + );
970 + }
971 +
972 + if ( empty( $inserted ) || ! is_object( $inserted ) || ! isset( $inserted->id ) ) {
973 + return wp_send_json_error(
974 + array(
975 + 'message' => esc_html__( 'Failed to create cost tracker entry', 'mainwp' ),
976 + )
977 + );
978 + }
979 +
980 + if ( ! empty( $selected_sites ) ) {
981 + Cost_Tracker_DB::get_instance()->update_selected_lookup_cost(
982 + $inserted->id,
983 + $selected_sites,
984 + false,
985 + false
986 + );
987 + }
988 +
989 + return wp_send_json_success(
990 + array(
991 + 'message' => esc_html__( 'Cost imported successfully.', 'mainwp' ),
992 + 'cost' => $inserted,
993 + )
994 + );
995 + }
996 +
997 + /**
656 998 * Method hook_get_next_renewal().
657 999 *
658 1000 * Get the next renewal.
659 1001 *
@@ -773,11 +1115,14 @@
773 1115 *
774 1116 * @param string $type Product type.
775 1117 */
776 1118 public static function get_product_type_icons( $type = false ) {
1119 + $defaults = static::get_default_product_types_icons();
777 1120 $product_icons = Cost_Tracker_Utility::get_instance()->get_option( 'product_types_icons', array(), true );
778 - if ( ! is_array( $product_icons ) ) {
779 - $product_icons = array();
1121 + if ( is_array( $product_icons ) && ! empty( $product_icons ) ) {
1122 + $product_icons = array_replace( $defaults, $product_icons );
1123 + } else {
1124 + $product_icons = $defaults;
780 1125 }
781 1126 if ( ! empty( $type ) && is_string( $type ) ) {
782 1127 return isset( $product_icons[ $type ] ) ? $product_icons[ $type ] : '';
783 1128 }
@@ -870,32 +1215,39 @@
870 1215 }
871 1216 }
872 1217
873 1218 /**
874 - * Hooks the section help content to the Help Sidebar element.
1219 + * Method mainwp_help_content()
1220 + *
1221 + * Creates the MainWP Help Documentation List for the help component in the sidebar.
875 1222 */
876 1223 public static function mainwp_help_content() {
877 - $allow_pages = array( 'ManageCostTracker', 'CostTrackerAdd', 'CostTrackerSettings' );
1224 + $allow_pages = array( 'ManageCostTracker', 'CostTrackerAdd', 'CostTrackerSettings', 'CostSummary' );
878 1225 if ( isset( $_GET['page'] ) && in_array( $_GET['page'], $allow_pages, true ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
879 1226 ?>
880 - <p><?php esc_html_e( 'If you need help with the Cost Tracker extension, please review following help documents', 'mainwp' ); ?></p>
881 - <div class="ui relaxed bulleted list">
882 - <div class="item"><a href="#" target="_blank" aria-hidden=""></a></div> <?php // NOSONAR -- compatible with help. ?>
883 - <div class="item"><a href="#" target="_blank" aria-hidden=""></a></div> <?php // NOSONAR -- compatible with help. ?>
884 - <?php
885 - /**
886 - * Action: mainwp_module_cost_tracker_help_item
887 - *
888 - * Fires at the bottom of the help articles list in the Help sidebar on the Themes page.
889 - *
890 - * Suggested HTML markup:
891 - *
892 - * <div class="item"><a href="Your custom URL">Your custom text</a></div>
893 - *
894 - * @since 4.0
895 - */
896 - do_action( 'mainwp_module_cost_tracker_help_item' );
897 - ?>
1227 + <p><?php esc_html_e( 'If you need help with the Cost Tracker module, please review following help documents', 'mainwp' ); ?></p>
1228 + <div class="ui list">
1229 + <div class="item"><i class="external alternate icon"></i> <a href="https://docs.mainwp.com/add-ons/client/cost-tracker-extension#cost-tracker" target="_blank">Cost Tracker</a></div> <?php // NOSONAR -- compatible with help. ?>
1230 + <div class="item"><i class="external alternate icon"></i> <a href="https://docs.mainwp.com/add-ons/client/cost-tracker-extension#manage-costs-page" target="_blank">Manage Costs</a></div> <?php // NOSONAR -- compatible with help. ?>
1231 + <div class="item"><i class="external alternate icon"></i> <a href="https://docs.mainwp.com/add-ons/client/cost-tracker-extension#adding-a-new-cost-to-track" target="_blank">Adding a New Cost to track</a></div> <?php // NOSONAR -- compatible with help. ?>
1232 + <div class="item"><i class="external alternate icon"></i> <a href="https://docs.mainwp.com/add-ons/client/cost-tracker-extension#edit-an-item" target="_blank">Edit Costs</a></div> <?php // NOSONAR -- compatible with help. ?>
1233 + <div class="item"><i class="external alternate icon"></i> <a href="https://docs.mainwp.com/add-ons/client/cost-tracker-extension#delete-an-item" target="_blank">Delete Costs</a></div> <?php // NOSONAR -- compatible with help. ?>
1234 + <div class="item"><i class="external alternate icon"></i> <a href="https://docs.mainwp.com/add-ons/client/cost-tracker-extension#settings-page" target="_blank">Cost Tracker Settings</a></div> <?php // NOSONAR -- compatible with help. ?>
1235 + <div class="item"><i class="external alternate icon"></i> <a href="https://docs.mainwp.com/add-ons/client/cost-tracker-extension#cost-tracker-pro-extension" target="_blank">Cost Tracker Pro Extension</a></div> <?php // NOSONAR -- compatible with help. ?>
1236 + <?php
1237 + /**
1238 + * Action: mainwp_module_cost_tracker_help_item
1239 + *
1240 + * Fires at the bottom of the help articles list in the Help sidebar on the Cost Tracker page.
1241 + *
1242 + * Suggested HTML markup:
1243 + *
1244 + * <div class="item"><a href="Your custom URL">Your custom text</a></div>
1245 + *
1246 + * @since 5.0
1247 + */
1248 + do_action( 'mainwp_module_cost_tracker_help_item' );
1249 + ?>
898 1250 </div>
899 1251 <?php
900 1252 }
901 1253 }
@@ -900,8 +1252,32 @@
900 1252 }
901 1253 }
902 1254
903 1255 /**
1256 + * Method calc_next_renewal_today()
1257 + *
1258 + * Calculate next renewal today.
1259 + *
1260 + * @param object $subscription subscription.
1261 + * @param int $next_renewal Next renewal.
1262 + *
1263 + * @return mixed result.
1264 + */
1265 + public static function calc_next_renewal_today( $subscription, $next_renewal = false ) {
1266 + if ( 'lifetime' === $subscription->type ) {
1267 + return 0;
1268 + }
1269 + $next_renewal = ! empty( $next_renewal ) ? intval( $next_renewal ) : (int) $subscription->next_renewal;
1270 + if ( empty( $next_renewal ) ) {
1271 + return 0;
1272 + }
1273 + if ( 'active' !== $subscription->cost_status ) {
1274 + return 0;
1275 + }
1276 + return $next_renewal;
1277 + }
1278 +
1279 + /**
904 1280 * Method generate_next_renewal()
905 1281 *
906 1282 * Generate next renewal info.
907 1283 *
@@ -916,9 +1292,9 @@
916 1292 return;
917 1293 }
918 1294
919 1295 if ( 'lifetime' === $subscription->type ) {
920 - esc_html_e( '', 'mainwp' );
1296 + echo '';
921 1297 return;
922 1298 }
923 1299
924 1300 $next_renewal = ! empty( $next_renewal ) ? intval( $next_renewal ) : (int) $subscription->next_renewal;
@@ -923,13 +1299,13 @@
923 1299
924 1300 $next_renewal = ! empty( $next_renewal ) ? intval( $next_renewal ) : (int) $subscription->next_renewal;
925 1301
926 1302 if ( empty( $next_renewal ) ) {
927 - echo esc_html__( '', 'mainwp' );
1303 + echo '';
928 1304 return;
929 1305 }
930 1306 if ( 'active' !== $subscription->cost_status ) {
931 - echo esc_html__( '', 'mainwp' );
1307 + echo '';
932 1308 return;
933 1309 }
934 1310
935 1311 $current_time = time();
@@ -1010,9 +1386,9 @@
1010 1386 } else {
1011 1387 $upload_icon = $prod_icon;
1012 1388 }
1013 1389
1014 - $style = 'width:32px;height:auto;display:inline-block;';
1390 + $style = 'width:28px;height:auto;display:inline-block;';
1015 1391
1016 1392 if ( empty( $prod_color ) ) {
1017 1393 $prod_color = '#34424D';
1018 1394 }
@@ -1037,9 +1413,9 @@
1037 1413 $scr = $icon_base . $upload_icon;
1038 1414 } else {
1039 1415 $scr = '';
1040 1416 }
1041 - $icon_wrapper_attr = ! empty( $img_id_attr ) ? 'id="' . esc_attr( $img_id_attr ) . '" class="ui mini circular image" ' : 'class="' . esc_attr( $default_cls ) . ' ui mini circular image "';
1417 + $icon_wrapper_attr = ! empty( $img_id_attr ) ? 'id="' . esc_attr( $img_id_attr ) . '" class="ui circular image" ' : 'class="' . esc_attr( $default_cls ) . ' ui circular image "';
1042 1418 $icon = '<div style="display:inline-block;" ' . $icon_wrapper_attr . '><img style="' . $style . '" alt="' . esc_attr__( 'Product icon', 'mainwp' ) . '" src="' . esc_attr( $scr ) . '"/></div>';
1043 1419 }
1044 1420 return $icon;
1045 1421 }