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;
}
}