register(); ( new Setup_Page() )->register(); ( new Ai_View() )->register(); ( new Dashboard_Widget() )->register(); ( new Site_Icons() )->register(); ( new Forms_Page() )->register(); ( new Report_Page() )->register(); add_filter( 'plugin_action_links_' . plugin_basename( DEALROOT_FILE ), array( $this, 'action_links' ) ); add_filter( 'set-screen-option', array( $this, 'save_screen_option' ), 10, 3 ); add_filter( 'admin_body_class', array( $this, 'body_class' ) ); add_filter( 'submenu_file', array( $this, 'submenu_file' ) ); } /** * Admin menu. */ public function menu() { $counts = Leads::status_counts(); $badge = $counts['new'] > 0 ? ' ' . number_format_i18n( $counts['new'] ) . '' : ''; $this->overview_hook = (string) add_menu_page( __( 'Leads', 'dealroot' ), __( 'Leads', 'dealroot' ) . $badge, self::capability(), self::PAGE, array( $this, 'render_overview' ), 'dashicons-groups', 26 ); add_submenu_page( self::PAGE, __( 'Overview', 'dealroot' ), __( 'Overview', 'dealroot' ), self::capability(), self::PAGE, array( $this, 'render_overview' ) ); $this->leads_hook = (string) add_submenu_page( self::PAGE, __( 'Leads', 'dealroot' ), __( 'All leads', 'dealroot' ), self::capability(), self::LEADS_PAGE, array( $this, 'render_leads' ) ); $this->reports_hook = (string) add_submenu_page( self::PAGE, __( 'Reports', 'dealroot' ), __( 'Reports', 'dealroot' ), self::capability(), Reports_Page::PAGE, array( new Reports_Page(), 'render' ) ); $this->live_hook = (string) add_submenu_page( self::PAGE, __( 'Live', 'dealroot' ), __( 'Live', 'dealroot' ), self::capability(), Live_Page::PAGE, array( new Live_Page(), 'render' ) ); $this->ask_hook = (string) add_submenu_page( self::PAGE, __( 'Ask about your leads', 'dealroot' ), __( 'Ask', 'dealroot' ), self::capability(), Ask_Page::PAGE, array( new Ask_Page(), 'render' ) ); $this->forms_hook = (string) add_submenu_page( self::PAGE, __( 'Forms', 'dealroot' ), __( 'Forms', 'dealroot' ), 'manage_options', Forms_Page::PAGE, array( new Forms_Page(), 'render' ) ); $this->settings_hook = (string) add_submenu_page( self::PAGE, __( 'Dealroot settings', 'dealroot' ), __( 'Settings', 'dealroot' ), 'manage_options', self::SETTINGS_PAGE, array( new Settings_Page(), 'render' ) ); // The assistant is opened from the overview, the settings and the plugins screen, not the menu. $this->setup_hook = (string) add_submenu_page( 'options.php', __( 'Set up Dealroot', 'dealroot' ), __( 'Set up Dealroot', 'dealroot' ), 'manage_options', Setup_Page::PAGE, array( new Setup_Page(), 'render' ) ); add_action( 'load-' . $this->overview_hook, array( $this, 'load_overview' ) ); add_action( 'load-' . $this->leads_hook, array( $this, 'load_leads_page' ) ); } /** * Sends links of the former combined screen, such as a status or a drill-down, to the list. */ public function load_overview() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Only forwards a read-only view. if ( isset( $_GET['lead'] ) ) { return; } /** * Filters the query arguments of the list that links of the overview carry along, so that * add-ons keep their own filters, such as the person a lead belongs to. * * @param string[] $list Argument names. */ $list = array_map( 'sanitize_key', (array) apply_filters( 'dealroot_list_query_keys', array( 'status', 's', 'channel', 'group_value', 'paged', 'orderby', 'order', 'view', 'value', 'owner' ) ) ); $keep = array_merge( $list, array( 'period', 'dim', 'model', 'from', 'to' ) ); $args = array(); foreach ( $keep as $key ) { if ( isset( $_GET[ $key ] ) && is_string( $_GET[ $key ] ) ) { $args[ $key ] = rawurlencode( sanitize_text_field( wp_unslash( $_GET[ $key ] ) ) ); } } // phpcs:enable if ( ! array_intersect( $list, array_keys( $args ) ) ) { return; } $args['page'] = self::LEADS_PAGE; wp_safe_redirect( add_query_arg( $args, admin_url( 'admin.php' ) ) ); exit; } /** * Prepares the list table and processes bulk actions. */ public function load_leads_page() { if ( isset( $_GET['lead'] ) || isset( $_GET['inbox'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } add_screen_option( 'per_page', array( 'default' => 20, 'option' => 'dealroot_per_page', ) ); Leads_Page::remember_view(); $this->table = new Leads_Table(); $this->table->process_bulk_action(); $this->table->clean_address(); } /** * Saves the per-page screen option. * * @param mixed $status Status. * @param string $option Option name. * @param int $value Value. * @return mixed */ public function save_screen_option( $status, $option, $value ) { return 'dealroot_per_page' === $option ? min( 200, max( 1, (int) $value ) ) : $status; } /** * Overview, or a single lead. */ public function render_overview() { if ( ! $this->render_lead() ) { ( new Overview_Page() )->render(); } } /** * List or board of leads, or a single lead. */ public function render_leads() { if ( isset( $_GET['inbox'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended \Dealroot\Inbox::render(); return; } if ( ! $this->render_lead() ) { ( new Leads_Page() )->render( $this->table ? $this->table : new Leads_Table() ); } } /** * Shows the lead from the address, or the form for a new one. * * @return bool Whether a lead screen was shown. */ private function render_lead() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $lead = isset( $_GET['lead'] ) ? sanitize_key( wp_unslash( $_GET['lead'] ) ) : ''; if ( 'new' === $lead ) { ( new Lead_View() )->render_new(); return true; } if ( absint( $lead ) ) { ( new Lead_View() )->render( absint( $lead ) ); return true; } return false; } /** * Marks the screens that are drawn with the Dealroot frame. * * @param string $classes Classes of the body. * @return string */ public function body_class( $classes ) { $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( '' !== $page && ( Setup_Page::PAGE === $page || in_array( $page, wp_list_pluck( Shell::screens(), 'page' ), true ) ) ) { $classes .= ' dealroot-screen'; } return $classes; } /** * Highlights All leads in the menu while a single lead is open. * * @param string|null $file Slug of the highlighted submenu item. * @return string|null */ public function submenu_file( $file ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['page'], $_GET['lead'] ) && self::PAGE === $_GET['page'] ) { return self::LEADS_PAGE; } return $file; } /** * Admin styles. * * @param string $hook Current screen hook. */ public function assets( $hook ) { if ( false === strpos( (string) $hook, 'dealroot' ) && 'index.php' !== $hook ) { return; } wp_enqueue_style( 'dealroot-admin', DEALROOT_URL . 'assets/css/admin.css', array(), Plugin::asset_version( 'assets/css/admin.css' ) ); if ( $hook === $this->forms_hook ) { wp_enqueue_script( 'dealroot-forms', DEALROOT_URL . 'assets/js/forms.js', array(), Plugin::asset_version( 'assets/js/forms.js' ), true ); wp_localize_script( 'dealroot-forms', 'dealrootForms', Forms_Page::script_data() ); } if ( $hook === $this->setup_hook ) { $forms = Forms_Page::script_data(); $forms['done'] = Setup_Page::url( 'forms' ); wp_enqueue_script( 'dealroot-forms', DEALROOT_URL . 'assets/js/forms.js', array(), Plugin::asset_version( 'assets/js/forms.js' ), true ); wp_localize_script( 'dealroot-forms', 'dealrootForms', $forms ); wp_enqueue_script( 'dealroot-setup', DEALROOT_URL . 'assets/js/setup.js', array(), Plugin::asset_version( 'assets/js/setup.js' ), true ); wp_localize_script( 'dealroot-setup', 'dealrootSetup', Setup_Page::script_data() ); } if ( $hook === $this->live_hook ) { wp_enqueue_script( 'dealroot-live', DEALROOT_URL . 'assets/js/live.js', array(), Plugin::asset_version( 'assets/js/live.js' ), true ); wp_localize_script( 'dealroot-live', 'dealrootLive', Live_Page::script_data() ); } if ( $hook === $this->overview_hook || $hook === $this->live_hook ) { wp_enqueue_script( 'dealroot-map', DEALROOT_URL . 'assets/js/map.js', array(), Plugin::asset_version( 'assets/js/map.js' ), true ); wp_localize_script( 'dealroot-map', 'dealrootMap', array( 'outlines' => DEALROOT_URL . 'assets/data/countries.json?ver=' . rawurlencode( Plugin::asset_version( 'assets/data/countries.json' ) ), 'towns' => DEALROOT_URL . 'assets/data/places.json?ver=' . rawurlencode( Plugin::asset_version( 'assets/data/places.json' ) ), 'lang' => substr( determine_locale(), 0, 2 ), 'visits' => __( 'Visits', 'dealroot' ), 'leads' => __( 'Leads', 'dealroot' ), 'won' => __( 'Won', 'dealroot' ), 'failed' => __( 'The map could not be loaded.', 'dealroot' ), /* translators: %s: countries with their visits, e.g. "Germany (3)" */ 'unplaced' => __( 'Visits without a known city, not on the map: %s', 'dealroot' ), 'zoomIn' => __( 'Zoom in', 'dealroot' ), 'zoomOut' => __( 'Zoom out', 'dealroot' ), /* translators: %s: name of a city or country */ 'open' => __( 'Show the leads from %s', 'dealroot' ), ) ); } if ( $hook === $this->reports_hook ) { wp_enqueue_style( 'dealroot-charts', DEALROOT_URL . 'assets/css/charts.css', array( 'dealroot-admin' ), Plugin::asset_version( 'assets/css/charts.css' ) ); wp_enqueue_script( 'dealroot-charts', DEALROOT_URL . 'assets/js/charts.js', array(), Plugin::asset_version( 'assets/js/charts.js' ), true ); } if ( in_array( $hook, array( $this->overview_hook, $this->leads_hook, $this->settings_hook, $this->reports_hook, $this->ask_hook ), true ) ) { wp_enqueue_script( 'dealroot-admin', DEALROOT_URL . 'assets/js/admin.js', array(), Plugin::asset_version( 'assets/js/admin.js' ), true ); wp_localize_script( 'dealroot-admin', 'dealrootAdmin', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'dealroot_quick_status' ), 'saved' => __( 'Saved', 'dealroot' ), 'failed' => __( 'Could not save. Please reload the page.', 'dealroot' ), 'unsaved' => __( 'You have changes that are not saved yet.', 'dealroot' ), 'icons' => Site_Icons::enabled(), 'iconsNonce' => wp_create_nonce( 'dealroot_site_icons' ), /* translators: 1: first row shown, 2: last row shown, 3: number of rows */ 'range' => __( '%1$s–%2$s of %3$s', 'dealroot' ), /* translators: %s: page number */ 'page' => __( 'Page %s', 'dealroot' ), 'previous' => __( 'Previous page', 'dealroot' ), 'next' => __( 'Next page', 'dealroot' ), 'contacted' => __( 'Marked as contacted.', 'dealroot' ), 'aiNonce' => wp_create_nonce( 'dealroot_ai_draft' ), 'askNonce' => wp_create_nonce( 'dealroot_ai_ask' ), 'askFill' => __( 'Change the name and the details, then press Ask.', 'dealroot' ), 'asking' => __( 'One moment…', 'dealroot' ), /* translators: %s: number or amount */ 'askTotal' => __( 'In all: %s', 'dealroot' ), 'aiWriting' => __( 'The AI assistant is writing. This can take up to a minute…', 'dealroot' ), 'aiDone' => __( 'Draft written. Read it and change what does not fit before you send it.', 'dealroot' ), 'aiFailed' => __( 'Something went wrong. Please reload the page and try again.', 'dealroot' ), 'aiErrors' => array( 'ai_quota' => Ai::message( new \WP_Error( 'ai_quota' ) ), 'ai_busy' => Ai::message( new \WP_Error( 'ai_busy' ) ), ), 'copied' => __( 'Copied', 'dealroot' ), 'translating' => __( 'The AI assistant is translating…', 'dealroot' ), 'privacyTools' => __( 'Open the privacy tools', 'dealroot' ), 'aiReading' => __( 'The AI assistant is reading…', 'dealroot' ), 'aiFilled' => __( 'Filled in. Check the fields before you save.', 'dealroot' ), 'recording' => __( 'Recording. Ask your question, then click Stop.', 'dealroot' ), 'stop' => __( 'Stop', 'dealroot' ), 'listening' => __( 'Writing down what you said…', 'dealroot' ), 'noMicrophone' => __( 'The microphone could not be used. Allow it for this site in your browser.', 'dealroot' ), 'ownChecking' => __( 'Checking with the provider…', 'dealroot' ), 'sending' => __( 'Sending…', 'dealroot' ), 'attachTitle' => __( 'Files for the email', 'dealroot' ), 'attachButton' => __( 'Attach', 'dealroot' ), /* translators: %s: name of a file */ 'removeFile' => __( 'Remove %s', 'dealroot' ), 'sentWhatsapp' => __( 'Did you send the message on WhatsApp? Only then it goes into the conversation.', 'dealroot' ), 'sentMail' => __( 'Did you send the email from your email program? Only then it goes into the conversation.', 'dealroot' ), ) ); // Photos and documents for an answer come from the media library or the computer. if ( ! empty( $_GET['lead'] ) && current_user_can( 'upload_files' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended wp_enqueue_media(); } } } /** * Deletes a lead for good, from the lead screen. */ public function handle_delete() { $lead_id = isset( $_POST['lead_id'] ) ? absint( $_POST['lead_id'] ) : 0; check_admin_referer( 'dealroot_delete_lead_' . $lead_id ); if ( ! current_user_can( self::capability() ) ) { wp_die( esc_html__( 'You are not allowed to edit leads.', 'dealroot' ), 403 ); } $deleted = Leads::delete( array( $lead_id ) ); wp_safe_redirect( add_query_arg( array( 'page' => self::LEADS_PAGE, 'deleted' => $deleted, ), admin_url( 'admin.php' ) ) ); exit; } /** * Saves status, value and note from the lead screen. */ public function handle_update() { $lead_id = isset( $_POST['lead_id'] ) ? absint( $_POST['lead_id'] ) : 0; check_admin_referer( 'dealroot_update_lead_' . $lead_id ); if ( ! current_user_can( self::capability() ) ) { wp_die( esc_html__( 'You are not allowed to edit leads.', 'dealroot' ), 403 ); } $changes = array_merge( self::posted_outcome(), self::posted_details() ); $saved = Leads::update( $lead_id, $changes ); wp_safe_redirect( add_query_arg( array( 'page' => self::PAGE, 'lead' => $lead_id, 'updated' => $saved ? 1 : 0, ), admin_url( 'admin.php' ) ) ); exit; } /** * Changes a lead's status from the list. */ public function ajax_quick_status() { check_ajax_referer( 'dealroot_quick_status', 'nonce' ); if ( ! current_user_can( self::capability() ) ) { wp_send_json_error( null, 403 ); } $lead_id = isset( $_POST['lead'] ) ? absint( $_POST['lead'] ) : 0; $changes = array( 'status' => isset( $_POST['status'] ) ? sanitize_key( wp_unslash( $_POST['status'] ) ) : '' ); if ( isset( $_POST['value'] ) && '' !== $_POST['value'] ) { $changes['value'] = self::parse_amount( sanitize_text_field( wp_unslash( $_POST['value'] ) ) ); } if ( ! in_array( $changes['status'], Leads::STATUSES, true ) || ! Leads::update( $lead_id, $changes ) ) { wp_send_json_error( null, 400 ); } // Answered from the reply panel, with or without the AI assistant. if ( isset( $_POST['via'] ) && in_array( $_POST['via'], array( 'reply', 'ai' ), true ) ) { \Dealroot\Impact::answered( $lead_id, sanitize_key( wp_unslash( $_POST['via'] ) ) ); } $lead = Leads::get( $lead_id ); wp_send_json_success( array( 'status' => $lead->status, 'value' => null === $lead->value ? '—' : Format::money( (float) $lead->value, $lead->currency ), ) ); } /** * Creates a lead entered by hand, e.g. after a phone call. */ public function handle_create() { check_admin_referer( 'dealroot_create_lead' ); if ( ! current_user_can( self::capability() ) ) { wp_die( esc_html__( 'You are not allowed to edit leads.', 'dealroot' ), 403 ); } $details = array_merge( array_fill_keys( array( 'name', 'email', 'phone', 'source', 'campaign', 'channel' ), '' ), self::posted_details() ); $outcome = self::posted_outcome(); $message = isset( $_POST['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['message'] ) ) : ''; $date = isset( $_POST['date'] ) ? sanitize_text_field( wp_unslash( $_POST['date'] ) ) : ''; $created = current_time( 'mysql', true ); if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) { $local = new \DateTimeImmutable( $date . ' ' . wp_date( 'H:i:s' ), wp_timezone() ); $created = $local->setTimezone( new \DateTimeZone( 'UTC' ) )->format( 'Y-m-d H:i:s' ); } $fields = array(); foreach ( array( 'name' => __( 'Name', 'dealroot' ), 'email' => __( 'Email', 'dealroot' ), 'phone' => __( 'Phone', 'dealroot' ), ) as $key => $label ) { if ( '' !== $details[ $key ] ) { $fields[] = array( 'label' => $label, 'name' => $key, 'type' => 'phone' === $key ? 'tel' : ( 'email' === $key ? 'email' : 'text' ), 'value' => $details[ $key ], ); } } if ( '' !== $message ) { $fields[] = array( 'label' => __( 'Message', 'dealroot' ), 'name' => 'message', 'type' => 'textarea', 'value' => $message, ); } $data = array( 'created_at' => $created, 'updated_at' => current_time( 'mysql', true ), 'form_plugin' => 'manual', 'form_name' => __( 'Added manually', 'dealroot' ), 'name' => $details['name'], 'email' => $details['email'], 'phone' => $details['phone'], 'fields' => wp_json_encode( $fields ), 'channel' => $details['channel'], 'last_channel' => $details['channel'], 'source' => $details['source'], 'last_source' => $details['source'], 'campaign' => $details['campaign'], 'currency' => Settings::sanitize_currency( $outcome['currency'] ?? Settings::get( 'currency' ) ), 'note' => $outcome['note'] ?? '', ); $lead_id = Leads::insert( $data ); if ( $lead_id ) { Leads::update( $lead_id, array_intersect_key( $outcome, array_flip( array( 'status', 'value' ) ) ) ); /** This action is documented in includes/class-capture.php */ do_action( 'dealroot_lead_created', $lead_id, $data ); } wp_safe_redirect( add_query_arg( array( 'page' => self::PAGE, 'lead' => $lead_id, 'updated' => $lead_id ? 1 : 0, ), admin_url( 'admin.php' ) ) ); exit; } /** * Status, value, currency and note from the posted form. * * @return array */ private static function posted_outcome() { // phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified by the callers. $outcome = array(); if ( isset( $_POST['status'] ) ) { $outcome['status'] = sanitize_key( wp_unslash( $_POST['status'] ) ); } if ( isset( $_POST['value'] ) ) { $outcome['value'] = self::parse_amount( sanitize_text_field( wp_unslash( $_POST['value'] ) ) ); } if ( isset( $_POST['currency'] ) ) { $outcome['currency'] = sanitize_text_field( wp_unslash( $_POST['currency'] ) ); } if ( isset( $_POST['note'] ) ) { $outcome['note'] = sanitize_textarea_field( wp_unslash( $_POST['note'] ) ); } // phpcs:enable return $outcome; } /** * Contact and source details from the posted form, only when that part of the form was sent. * * @return array */ private static function posted_details() { // phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified by the callers. if ( empty( $_POST['details'] ) ) { return array(); } $details = array(); foreach ( array( 'name', 'email', 'phone', 'source', 'campaign' ) as $key ) { $details[ $key ] = isset( $_POST[ $key ] ) ? trim( sanitize_text_field( wp_unslash( $_POST[ $key ] ) ) ) : ''; } $details['channel'] = isset( $_POST['channel'] ) ? sanitize_key( wp_unslash( $_POST['channel'] ) ) : ''; // phpcs:enable $e164 = Phone::to_e164( $details['phone'] ); if ( '' !== $e164 ) { $details['phone'] = $e164; } return $details; } /** * Sends the weekly summary now. */ public function handle_test_digest() { check_admin_referer( 'dealroot_test_digest' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to change these settings.', 'dealroot' ), 403 ); } $sent = Digest::send(); wp_safe_redirect( add_query_arg( array( 'page' => self::SETTINGS_PAGE, 'digest' => $sent ? 'ok' : 'error', ), admin_url( 'admin.php' ) ) ); exit; } /** * Sends a test message with the saved Telegram settings. */ public function handle_test_telegram() { check_admin_referer( 'dealroot_test_telegram' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to change these settings.', 'dealroot' ), 403 ); } $result = isset( $_GET['week'] ) ? Digest::send_telegram() : Notifier::send_telegram( Settings::get( 'telegram_token' ), Settings::get( 'telegram_chat_id' ), /* translators: %s: site name. */ sprintf( __( 'Dealroot is connected to %s. New leads will appear here.', 'dealroot' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ) ); $args = array( 'page' => self::SETTINGS_PAGE ); if ( is_wp_error( $result ) ) { set_transient( 'dealroot_telegram_error_' . get_current_user_id(), $result->get_error_message(), MINUTE_IN_SECONDS ); $args['telegram'] = 'error'; } else { $args['telegram'] = 'ok'; } wp_safe_redirect( add_query_arg( $args, admin_url( 'admin.php' ) ) ); exit; } /** * Sends the most recent lead, or a sample, to the webhook. */ public function handle_test_webhook() { check_admin_referer( 'dealroot_test_webhook' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to change these settings.', 'dealroot' ), 403 ); } $recent = Leads::query( array( 'per_page' => 1 ) ); $lead = $recent['items'] ? Webhook::payload( $recent['items'][0] ) : array( 'id' => 0, 'name' => 'Jane Doe', 'email' => 'jane@example.com', 'channel' => 'paid_search', 'source' => 'google', ); $result = Webhook::send( 'lead.test', $lead ); $args = array( 'page' => self::SETTINGS_PAGE, 'webhook' => is_wp_error( $result ) ? 'error' : 'ok', ); if ( is_wp_error( $result ) ) { set_transient( 'dealroot_webhook_error_' . get_current_user_id(), $result->get_error_message(), MINUTE_IN_SECONDS ); } wp_safe_redirect( add_query_arg( $args, admin_url( 'admin.php' ) ) ); exit; } /** * Settings link on the plugins screen. * * @param array $links Links. * @return array */ public function action_links( $links ) { array_unshift( $links, '' . esc_html__( 'Settings', 'dealroot' ) . '' ); if ( current_user_can( 'manage_options' ) && ! Setup_Page::finished() ) { array_unshift( $links, '' . esc_html__( 'Set up', 'dealroot' ) . '' ); } return $links; } /** * Parses "1.234,50", "1,234.50", "12,400", "12.400" or "1234.5" into a number. * * @param string $raw User input. * @return float|null */ public static function parse_amount( $raw ) { $raw = preg_replace( '/[^\d.,-]/', '', (string) $raw ); if ( '' === $raw ) { return null; } $last_comma = strrpos( $raw, ',' ); $last_dot = strrpos( $raw, '.' ); if ( false !== $last_comma && false !== $last_dot ) { // Both appear: the last one separates the cents. $raw = $last_comma > $last_dot ? str_replace( array( '.', ',' ), array( '', '.' ), $raw ) : str_replace( ',', '', $raw ); } elseif ( false !== $last_comma || false !== $last_dot ) { $mark = false !== $last_comma ? ',' : '.'; // One mark followed by three digits, or several marks, group thousands: 12,400 or 1.250.000. if ( substr_count( $raw, $mark ) > 1 || preg_match( '/^-?[1-9]\d{0,2}' . preg_quote( $mark, '/' ) . '\d{3}$/', $raw ) ) { $raw = str_replace( $mark, '', $raw ); } else { $raw = str_replace( ',', '.', $raw ); } } return is_numeric( $raw ) ? max( 0, (float) $raw ) : null; } }