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 +158 -32 6.1.3 → 6.2.1 View file →
@@ -88,8 +88,10 @@
88 88 add_filter( 'mainwp_init_primary_menu_items', array( $this, 'hook_init_primary_menu_items' ), 10, 2 );
89 89 add_filter( 'mainwp_module_cost_tracker_get_default_cost_fields', array( $this, 'hook_get_default_cost_fields' ), 10, 2 );
90 90 add_filter( 'mainwp_module_cost_tracker_get_next_renewal', array( $this, 'hook_get_next_renewal' ), 10, 3 );
91 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 );
92 94 add_filter( 'mainwp_module_cost_tracker_get_total_cost', array( $this, 'hook_get_total_cost' ), 10, 2 );
93 95 }
94 96
95 97
@@ -131,8 +133,57 @@
131 133 );
132 134 }
133 135
134 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 + /**
135 186 * Admin Init
136 187 *
137 188 * Initiates admin hooks.
138 189 */
@@ -146,9 +197,9 @@
146 197
147 198 $allow_pages = array( 'ManageCostTracker', 'CostTrackerAdd', 'CostTrackerSettings', 'CostSummary', 'CostTrackerImport' );
148 199 if ( isset( $_GET['page'] ) && in_array( $_GET['page'], $allow_pages, true ) ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
149 200 $base_url = Cost_Tracker_Manager::get_location_path( 'url' );
150 - 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 );
151 202 if ( 'CostSummary' === $_GET['page'] ) { //phpcs:ignore WordPress.Security.NonceVerification.Recommended
152 203 add_filter(
153 204 'mainwp_admin_enqueue_scripts',
154 205 function ( $scripts ) {
@@ -759,16 +810,80 @@
759 810 return $valid_arr;
760 811 }
761 812
762 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 + /**
763 878 * AJAX handler for importing cost tracker entries.
764 879 *
765 - * @return void
880 + * @return never wp_send_json_*() exits.
766 881 */
767 882 public static function ajax_import_cost() { //phpcs:ignore -- NOSONAR - complex method.
768 883 MainWP_Post_Handler::instance()->secure_request( 'mainwp_cost_tracker_import_cost' );
769 884
770 - if ( ! isset( $_POST['encoded_data'] ) ) {
885 + if ( ! isset( $_POST['encoded_data'] ) || ! is_string( $_POST['encoded_data'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- secure_request() verifies the nonce above.
771 886 return wp_send_json_error(
772 887 array(
773 888 'message' => esc_html__( 'No cost data provided', 'mainwp' ),
774 889 )
@@ -774,12 +889,13 @@
774 889 )
775 890 );
776 891 }
777 892
778 - $encoded_data = wp_unslash( $_POST['encoded_data'] );
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.
779 894 $cost_data_raw = json_decode( $encoded_data, true );
780 895
781 - if ( null === $cost_data_raw || ! is_array( $cost_data_raw ) ) {
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'] ) ) {
782 898 return wp_send_json_error(
783 899 array(
784 900 'message' => esc_html__( 'Invalid cost data format', 'mainwp' ),
785 901 )
@@ -785,9 +901,9 @@
785 901 )
786 902 );
787 903 }
788 904
789 - $cost_name = isset( $cost_data_raw['cost']['name'] ) ? sanitize_text_field( $cost_data_raw['cost']['name'] ) : '';
905 + $cost_name = isset( $cost_data_raw['cost']['name'] ) && is_string( $cost_data_raw['cost']['name'] ) ? sanitize_text_field( $cost_data_raw['cost']['name'] ) : '';
790 906
791 907 if ( empty( $cost_name ) ) {
792 908 return wp_send_json_error(
793 909 array(
@@ -795,40 +911,50 @@
795 911 )
796 912 );
797 913 }
798 914
799 - $cost_data = array(
800 - 'name' => $cost_name,
801 - 'url' => isset( $cost_data_raw['cost']['url'] ) ? esc_url( $cost_data_raw['cost']['url'] ) : '',
802 - 'type' => isset( $cost_data_raw['cost']['type'] ) ? sanitize_text_field( $cost_data_raw['cost']['type'] ) : '',
803 - 'product_type' => isset( $cost_data_raw['cost']['product_type'] ) ? sanitize_text_field( $cost_data_raw['cost']['product_type'] ) : '',
804 - 'license_type' => isset( $cost_data_raw['cost']['license_type'] ) ? sanitize_text_field( $cost_data_raw['cost']['license_type'] ) : '',
805 - 'price' => isset( $cost_data_raw['cost']['price'] ) ? floatval( $cost_data_raw['cost']['price'] ) : 0,
806 - 'payment_method' => isset( $cost_data_raw['cost']['payment_method'] ) ? sanitize_text_field( $cost_data_raw['cost']['payment_method'] ) : '',
807 - 'renewal_type' => isset( $cost_data_raw['cost']['renewal_type'] ) ? sanitize_text_field( $cost_data_raw['cost']['renewal_type'] ) : '',
808 - 'last_renewal' => isset( $cost_data_raw['cost']['last_renewal'] ) ? intval( $cost_data_raw['cost']['last_renewal'] ) : 0,
809 - 'cost_status' => isset( $cost_data_raw['cost']['cost_status'] ) ? sanitize_text_field( $cost_data_raw['cost']['cost_status'] ) : '',
810 - );
915 + $cost_fields = static::import_cost_fields( $cost_data_raw['cost'] );
811 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 +
812 937 $selected_sites = array();
813 938
814 - if ( isset( $cost_data_raw['cost']['select_sites'] ) && is_array( $cost_data_raw['cost']['select_sites'] ) ) {
815 - foreach ( $cost_data_raw['cost']['select_sites'] as $url ) {
816 - $url = esc_url( trim( $url ) );
817 - if ( empty( $url ) ) {
818 - continue;
819 - }
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 + }
820 947
821 - if ( '/' !== substr( $url, -1 ) ) {
822 - $url .= '/';
823 - }
948 + if ( '/' !== substr( $url, -1 ) ) {
949 + $url .= '/';
950 + }
824 951
825 - $website = MainWP_DB::instance()->get_websites_by_url( $url );
826 - $site_id = ! empty( $website ) ? current( $website )->id : null;
952 + $website = MainWP_DB::instance()->get_websites_by_url( $url );
953 + $site_id = ! empty( $website ) ? current( $website )->id : null;
827 954
828 - if ( ! empty( $site_id ) ) {
829 - $selected_sites[] = $site_id;
830 - }
955 + if ( ! empty( $site_id ) ) {
956 + $selected_sites[] = $site_id;
831 957 }
832 958 }
833 959
834 960 $cost_data['sites'] = ! empty( $selected_sites ) ? wp_json_encode( array_values( array_unique( array_map( 'intval', $selected_sites ) ) ) ) : '';