PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.1
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.1
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 +432 -57 5.1.1 → 6.2.1 View file →
@@ -14,12 +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;
19 20 use MainWP\Dashboard\MainWP_Exception;
21 +use MainWP\Dashboard\Module\CostTracker\Cost_Tracker_Import;
20 22
21 -use function MainWP\Dashboard\mainwp_current_user_have_right;
23 +// Exit if accessed directly.
24 +if ( ! defined( 'ABSPATH' ) ) {
25 + exit;
26 +}
22 27
23 28 /**
24 29 * Class Cost_Tracker_Admin
25 30 */
@@ -83,8 +88,10 @@
83 88 add_filter( 'mainwp_init_primary_menu_items', array( $this, 'hook_init_primary_menu_items' ), 10, 2 );
84 89 add_filter( 'mainwp_module_cost_tracker_get_default_cost_fields', array( $this, 'hook_get_default_cost_fields' ), 10, 2 );
85 90 add_filter( 'mainwp_module_cost_tracker_get_next_renewal', array( $this, 'hook_get_next_renewal' ), 10, 3 );
86 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 );
87 94 add_filter( 'mainwp_module_cost_tracker_get_total_cost', array( $this, 'hook_get_total_cost' ), 10, 2 );
88 95 }
89 96
90 97
@@ -90,9 +97,9 @@
90 97
91 98 /**
92 99 * Initiate Hooks
93 100 *
94 - * Initiates hooks for the Subscription extension.
101 + * Initiates hooks for the Cost Tracker module.
95 102 */
96 103 public function init() {
97 104 add_action( 'admin_init', array( &$this, 'admin_init' ) );
98 105 add_action( 'mainwp_help_sidebar_content', array( $this, 'mainwp_help_content' ) );
@@ -126,8 +133,57 @@
126 133 );
127 134 }
128 135
129 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 + /**
130 186 * Admin Init
131 187 *
132 188 * Initiates admin hooks.
133 189 */
@@ -133,16 +189,17 @@
133 189 */
134 190 public function admin_init() {
135 191
136 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' ) );
137 194
138 195 $this->handle_edit_cost_tracker_post();
139 196 $this->handle_settings_post();
140 197
141 - $allow_pages = array( 'ManageCostTracker', 'CostTrackerAdd', 'CostTrackerSettings', 'CostSummary' );
198 + $allow_pages = array( 'ManageCostTracker', 'CostTrackerAdd', 'CostTrackerSettings', 'CostSummary', 'CostTrackerImport' );
142 199 if ( isset( $_GET['page'] ) && in_array( $_GET['page'], $allow_pages, true ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
143 200 $base_url = Cost_Tracker_Manager::get_location_path( 'url' );
144 - 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 );
145 202 if ( 'CostSummary' === $_GET['page'] ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
146 203 add_filter(
147 204 'mainwp_admin_enqueue_scripts',
148 205 function ( $scripts ) {
@@ -152,8 +209,11 @@
152 209 return $scripts;
153 210 }
154 211 );
155 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 + }
156 216 }
157 217 }
158 218
159 219 /**
@@ -224,10 +284,10 @@
224 284 'multi_site' => esc_html__( 'Multiple-Site License', 'mainwp' ),
225 285 );
226 286 $product_types = static::get_product_types();
227 287 $payment_types = array(
228 - 'subscription' => esc_html__( 'Subscription', 'mainwp' ),
229 - 'lifetime' => esc_html__( 'Lifetime', 'mainwp' ),
288 + 'subscription' => esc_html__( 'Recurring (Subscription)', 'mainwp' ),
289 + 'lifetime' => esc_html__( 'One-time (Lifetime)', 'mainwp' ),
230 290 );
231 291 $payment_methods = static::get_payment_methods();
232 292 $renewal_frequency = static::get_renewal_frequency();
233 293 $cost_status = static::get_cost_status();
@@ -292,15 +352,15 @@
292 352 );
293 353
294 354 add_submenu_page(
295 355 'mainwp_tab',
296 - esc_html__( 'Settings', 'mainwp' ),
297 - '<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>',
298 358 'read',
299 - 'CostTrackerSettings',
359 + 'CostTrackerImport',
300 360 array(
301 - Cost_Tracker_Settings::get_instance(),
302 - 'render_settings_page',
361 + Cost_Tracker_Import::get_instance(),
362 + 'render_import_costs',
303 363 )
304 364 );
305 365
306 366 /**
@@ -333,8 +393,12 @@
333 393 *
334 394 * Run on page load.
335 395 */
336 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 + }
337 401 Cost_Tracker_Summary::instance()->on_load_page( static::$page );
338 402 }
339 403
340 404 /**
@@ -344,9 +408,9 @@
344 408 */
345 409 public static function init_left_menu( $subPages ) {
346 410 MainWP_Menu::add_left_menu(
347 411 array(
348 - 'title' => esc_html__( 'Cost Tracker', 'mainwp' ),
412 + 'title' => esc_html__( 'Costs', 'mainwp' ),
349 413 'parent_key' => 'mainwp_tab',
350 414 'slug' => 'CostSummary',
351 415 'href' => 'admin.php?page=CostSummary',
352 416 'icon' => '<i class="dollar sign icon"></i>',
@@ -354,8 +418,15 @@
354 418 'nosubmenu' => true,
355 419 ),
356 420 0
357 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 +
358 429 $init_sub_subleftmenu = array(
359 430 array(
360 431 'title' => esc_html__( 'Cost Summary', 'mainwp' ),
361 432 'parent_key' => 'CostSummary',
@@ -361,8 +432,9 @@
361 432 'parent_key' => 'CostSummary',
362 433 'href' => 'admin.php?page=CostSummary',
363 434 'slug' => 'CostSummary',
364 435 'right' => 'manage_cost_tracker',
436 + 'item_class' => $sum_item_class,
365 437 ),
366 438 array(
367 439 'title' => esc_html__( 'Manage Costs', 'mainwp' ),
368 440 'parent_key' => 'CostSummary',
@@ -377,13 +449,13 @@
377 449 'slug' => 'CostTrackerAdd',
378 450 'right' => '',
379 451 ),
380 452 array(
381 - 'title' => esc_html__( 'Settings', 'mainwp' ),
453 + 'title' => esc_html__( 'Import Costs', 'mainwp' ),
382 454 'parent_key' => 'CostSummary',
383 - 'href' => 'admin.php?page=CostTrackerSettings',
384 - 'slug' => 'CostTrackerSettings',
385 - 'right' => '',
455 + 'href' => 'admin.php?page=CostTrackerImport',
456 + 'slug' => 'CostTrackerImport',
457 + 'right' => 'manage_cost_tracker',
386 458 ),
387 459 );
388 460
389 461 MainWP_Menu::init_subpages_left_menu( $subPages, $init_sub_subleftmenu, 'ManageCostTracker', 'ManageCostTracker' );
@@ -405,10 +477,17 @@
405 477 *
406 478 * @param string $shownPage Current Page.
407 479 */
408 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 +
409 488 $params = array(
410 - 'title' => esc_html__( 'Cost Tracker', 'mainwp' ),
489 + 'title' => $title,
411 490 'which' => 'page_cost_tracker_overview',
412 491 'wrap_class' => 'mainwp-module-cost-tracker-content-wrapper',
413 492 );
414 493
@@ -415,11 +494,11 @@
415 494 MainWP_UI::render_top_header( $params );
416 495
417 496 $renderItems = array();
418 497
419 - if ( mainwp_current_user_have_right( 'dashboard', 'manage_cost_tracker' ) ) {
498 + if ( \mainwp_current_user_can( 'dashboard', 'manage_cost_tracker' ) ) {
420 499 $renderItems[] = array(
421 - 'title' => esc_html__( 'Overview', 'mainwp' ),
500 + 'title' => esc_html__( 'Operations', 'mainwp' ),
422 501 'href' => 'admin.php?page=ManageCostTracker',
423 502 'active' => ( '' === $shownPage ) ? true : false,
424 503 );
425 504 }
@@ -431,8 +510,16 @@
431 510 'active' => ( 'add' === $shownPage ) ? true : false,
432 511 );
433 512 }
434 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 +
435 522 $cost_id = isset( $_GET['id'] ) ? intval( $_GET['id'] ) : 0; //phpcs:ignore WordPress.Security.NonceVerification.Recommended
436 523 if ( $cost_id ) {
437 524 $renderItems[] = array(
438 525 'title' => esc_html__( 'Edit Cost Tracker', 'mainwp' ),
@@ -440,15 +527,8 @@
440 527 'active' => ( 'edit' === $shownPage ) ? true : false,
441 528 );
442 529 }
443 530
444 - if ( ! MainWP_Menu::is_disable_menu_item( 3, 'CostTrackerSettings' ) ) {
445 - $renderItems[] = array(
446 - 'title' => esc_html__( 'Settings', 'mainwp' ),
447 - 'href' => 'admin.php?page=CostTrackerSettings',
448 - 'active' => ( 'settings' === $shownPage ) ? true : false,
449 - );
450 - }
451 531 if ( isset( static::$subPages ) && is_array( static::$subPages ) ) {
452 532 foreach ( static::$subPages as $subPage ) {
453 533 if ( MainWP_Menu::is_disable_menu_item( 3, 'ManageCostTracker' . $subPage['slug'] ) ) {
454 534 continue;
@@ -474,9 +554,11 @@
474 554 * @return mixed Save output.
475 555 */
476 556 public static function handle_edit_cost_tracker_post() { //phpcs:ignore -- NOSONAR - complex method.
477 557
478 - 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 ) ) {
479 561 return;
480 562 }
481 563 //phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
482 564 $last_renewal = isset( $_POST['mainwp_module_cost_tracker_edit_last_renewal'] ) ? strtotime( wp_unslash( $_POST['mainwp_module_cost_tracker_edit_last_renewal'] ) ) : 0;
@@ -538,17 +620,26 @@
538 620 $current = Cost_Tracker_DB::get_instance()->get_cost_tracker_by( 'id', $update['id'] );
539 621 }
540 622
541 623 //phpcs:enable
542 - $err_msg = '';
543 - $output = false;
624 + $err_msg = '';
625 + $output = false;
626 + $saved_id = 0;
544 627 try {
545 628 $output = Cost_Tracker_DB::get_instance()->update_cost_tracker( $update );
546 629 if ( $output && ! empty( $output->id ) ) {
630 + $saved_id = $output->id;
547 631 Cost_Tracker_DB::get_instance()->update_selected_lookup_cost( $output->id, $selected_sites, $selected_groups, $selected_clients );
548 632 if ( $current && ! empty( $current->cost_icon ) && false === strpos( $current->cost_icon, 'deficon:' ) && $current->cost_icon !== $update['cost_icon'] ) {
549 633 Cost_Tracker_Add_Edit::get_instance()->delete_product_icon_file( $current->cost_icon );
550 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 + );
551 642 }
552 643 } catch ( MainWP_Exception $ex ) {
553 644 $err_msg = $ex->getMessage();
554 645 }
@@ -558,10 +649,24 @@
558 649 if ( ! empty( $err_msg ) ) {
559 650 set_transient( 'mainwp_cost_tracker_update_error_' . $msg_id, $err_msg, HOUR_IN_SECONDS );
560 651 }
561 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 +
562 667 if ( empty( $err_msg ) && ! empty( $output ) ) { // success.
563 - 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' ) );
564 669 } elseif ( ! empty( $err_msg ) ) { // error.
565 670 wp_safe_redirect( admin_url( 'admin.php?page=CostTrackerAdd&message=2&id=' . $msg_id ) );
566 671 }
567 672 exit();
@@ -566,9 +671,45 @@
566 671 }
567 672 exit();
568 673 }
569 674
675 + /**
676 + * Handle get_costs_snapshot_info
677 + *
678 + * @return array
679 + */
680 + public static function get_costs_snapshot_info() {
570 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 +
571 712 /**
572 713 * Settigns Post
573 714 *
574 715 * Handles the save settings post request.
@@ -579,8 +720,10 @@
579 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' ) ) {
580 721 return;
581 722 }
582 723
724 + $old_settings = MainWP_Settings::get_all_settings_values();
725 +
583 726 $all_opts = Cost_Tracker_Utility::get_instance()->get_all_options();
584 727 $currency = isset( $_POST['mainwp_module_cost_tracker_settings_currency'] ) ? sanitize_text_field( wp_unslash( $_POST['mainwp_module_cost_tracker_settings_currency'] ) ) : '';
585 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
586 729 $currency_format = Cost_Tracker_Utility::validate_currency_settings( $currency_format );
@@ -609,8 +752,22 @@
609 752 $all_opts = apply_filters( 'mainwp_module_cost_tracker_before_save_settings', $all_opts );
610 753
611 754 Cost_Tracker_Utility::get_instance()->save_options( $all_opts );
612 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 +
613 770 wp_safe_redirect( admin_url( 'admin.php?page=CostTrackerSettings&message=1' ) );
614 771 exit();
615 772 }
616 773
@@ -653,8 +810,192 @@
653 810 return $valid_arr;
654 811 }
655 812
656 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 + /**
657 998 * Method hook_get_next_renewal().
658 999 *
659 1000 * Get the next renewal.
660 1001 *
@@ -774,11 +1115,14 @@
774 1115 *
775 1116 * @param string $type Product type.
776 1117 */
777 1118 public static function get_product_type_icons( $type = false ) {
1119 + $defaults = static::get_default_product_types_icons();
778 1120 $product_icons = Cost_Tracker_Utility::get_instance()->get_option( 'product_types_icons', array(), true );
779 - if ( ! is_array( $product_icons ) ) {
780 - $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;
781 1125 }
782 1126 if ( ! empty( $type ) && is_string( $type ) ) {
783 1127 return isset( $product_icons[ $type ] ) ? $product_icons[ $type ] : '';
784 1128 }
@@ -871,32 +1215,39 @@
871 1215 }
872 1216 }
873 1217
874 1218 /**
875 - * 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.
876 1222 */
877 1223 public static function mainwp_help_content() {
878 - $allow_pages = array( 'ManageCostTracker', 'CostTrackerAdd', 'CostTrackerSettings' );
1224 + $allow_pages = array( 'ManageCostTracker', 'CostTrackerAdd', 'CostTrackerSettings', 'CostSummary' );
879 1225 if ( isset( $_GET['page'] ) && in_array( $_GET['page'], $allow_pages, true ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
880 1226 ?>
881 - <p><?php esc_html_e( 'If you need help with the Cost Tracker extension, please review following help documents', 'mainwp' ); ?></p>
882 - <div class="ui relaxed bulleted list">
883 - <div class="item"><a href="#" target="_blank" aria-hidden=""></a></div> <?php // NOSONAR -- compatible with help. ?>
884 - <div class="item"><a href="#" target="_blank" aria-hidden=""></a></div> <?php // NOSONAR -- compatible with help. ?>
885 - <?php
886 - /**
887 - * Action: mainwp_module_cost_tracker_help_item
888 - *
889 - * Fires at the bottom of the help articles list in the Help sidebar on the Themes page.
890 - *
891 - * Suggested HTML markup:
892 - *
893 - * <div class="item"><a href="Your custom URL">Your custom text</a></div>
894 - *
895 - * @since 4.0
896 - */
897 - do_action( 'mainwp_module_cost_tracker_help_item' );
898 - ?>
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 + ?>
899 1250 </div>
900 1251 <?php
901 1252 }
902 1253 }
@@ -901,8 +1252,32 @@
901 1252 }
902 1253 }
903 1254
904 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 + /**
905 1280 * Method generate_next_renewal()
906 1281 *
907 1282 * Generate next renewal info.
908 1283 *
@@ -917,9 +1292,9 @@
917 1292 return;
918 1293 }
919 1294
920 1295 if ( 'lifetime' === $subscription->type ) {
921 - esc_html_e( '', 'mainwp' );
1296 + echo '';
922 1297 return;
923 1298 }
924 1299
925 1300 $next_renewal = ! empty( $next_renewal ) ? intval( $next_renewal ) : (int) $subscription->next_renewal;
@@ -924,13 +1299,13 @@
924 1299
925 1300 $next_renewal = ! empty( $next_renewal ) ? intval( $next_renewal ) : (int) $subscription->next_renewal;
926 1301
927 1302 if ( empty( $next_renewal ) ) {
928 - echo esc_html__( '', 'mainwp' );
1303 + echo '';
929 1304 return;
930 1305 }
931 1306 if ( 'active' !== $subscription->cost_status ) {
932 - echo esc_html__( '', 'mainwp' );
1307 + echo '';
933 1308 return;
934 1309 }
935 1310
936 1311 $current_time = time();
@@ -1011,9 +1386,9 @@
1011 1386 } else {
1012 1387 $upload_icon = $prod_icon;
1013 1388 }
1014 1389
1015 - $style = 'width:32px;height:auto;display:inline-block;';
1390 + $style = 'width:28px;height:auto;display:inline-block;';
1016 1391
1017 1392 if ( empty( $prod_color ) ) {
1018 1393 $prod_color = '#34424D';
1019 1394 }
@@ -1038,9 +1413,9 @@
1038 1413 $scr = $icon_base . $upload_icon;
1039 1414 } else {
1040 1415 $scr = '';
1041 1416 }
1042 - $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 "';
1043 1418 $icon = '<div style="display:inline-block;" ' . $icon_wrapper_attr . '><img style="' . $style . '" alt="' . esc_attr__( 'Product icon', 'mainwp' ) . '" src="' . esc_attr( $scr ) . '"/></div>';
1044 1419 }
1045 1420 return $icon;
1046 1421 }