' . esc_html($msgs[$state][1]) . '
'; } /** The form. With a product, it watches that product's model; with a query (search * pages, the shortcode), whatever the query names. */ function usedkit_form_html($product, $query = '', $head = '') { if ($head !== '') { // The caller's own question (a model page with some for sale). } elseif ($product) { $head = !$product->is_in_stock() ? __('Sold. Want to hear when the next one comes in?', 'usedkit') : __('Want to hear when another one comes in?', 'usedkit'); } else { $head = $query /* translators: %s: the model the customer searched for */ ? sprintf(__('Not in stock right now. Want to hear when a %s comes in?', 'usedkit'), $query) : __('Looking for something? We\'ll email you when it comes in.', 'usedkit'); } ob_start(); ?> is_in_stock()) { echo wp_kses(usedkit_form_html($product), usedkit_allowed_html()); } }, 31); /** * The public forms (notify me, trade-in, offers, holds) carry a nonce for their action. * Shops cache their pages, and a nonce cached for over a day no longer passes: the visitor * is sent back with ?usedkit=error, an address caches pass by, so the page they land on has * a fresh nonce and sending again works. A honeypot field that people never see, and bots * fill in, turns most automated posts away besides. */ function usedkit_nonce_field($action) { wp_nonce_field('usedkit_form_' . $action, 'usedkit_nonce', false); } function usedkit_check_form() { if (!empty($_POST['website'])) { // phpcs:ignore WordPress.Security.NonceVerification -- the honeypot, before the nonce wp_safe_redirect(usedkit_result_url(usedkit_back(), 'check-email')); exit; } $action = sanitize_key(wp_unslash($_POST['action'] ?? '')); $nonce = sanitize_text_field(wp_unslash($_POST['usedkit_nonce'] ?? '')); if (!wp_verify_nonce($nonce, 'usedkit_form_' . $action)) { wp_safe_redirect(usedkit_result_url(usedkit_back(), 'error')); exit; } } /** At most `$max` of a kind of request a day from one address: the public forms that * email the shop (holds, offers) cannot be used to flood it. */ function usedkit_rate_ok($what, $max) { $ip = sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'] ?? '')); $key = 'usedkit_rate_' . $what . '_' . md5($ip . wp_salt()); $n = (int) get_transient($key); if ($n >= $max) { return false; } set_transient($key, $n + 1, DAY_IN_SECONDS); return true; } /** The address a public form came from, its state message cleared. */ function usedkit_back_clean($fallback) { return remove_query_arg(array('usedkit', 'usedkit_q', 'usedkit_n'), wp_validate_redirect(esc_url_raw(wp_unslash($_POST['back'] ?? '')), $fallback)); // phpcs:ignore WordPress.Security.NonceVerification } /** The current page's address, for a form to come back to: the site's own scheme and host * with the path asked for (which already holds a subfolder the site is installed in). */ function usedkit_here() { $path = isset($_SERVER['REQUEST_URI']) ? sanitize_url(wp_unslash($_SERVER['REQUEST_URI'])) : '/'; if ($path === '' || $path[0] !== '/') { $path = '/'; } return preg_replace('#^(https?://[^/]+).*$#', '$1', home_url('/')) . $path; } /** A result message for a form, from ?usedkit=..., out of the messages it knows. */ function usedkit_form_state($msgs) { $state = usedkit_result(); if (!isset($msgs[$state])) { return ''; } return '' . esc_html($msgs[$state][1]) . '
'; } function usedkit_handle_watch() { usedkit_check_form(); // phpcs:disable WordPress.Security.NonceVerification -- verified in usedkit_check_form() $email = sanitize_email(wp_unslash($_POST['email'] ?? '')); $product = absint($_POST['product'] ?? 0); $query = sanitize_text_field(wp_unslash($_POST['query'] ?? '')); $back = wp_validate_redirect(esc_url_raw(wp_unslash($_POST['back'] ?? '')), $product ? get_permalink($product) : home_url('/')); $back = remove_query_arg(array('usedkit', 'usedkit_q', 'usedkit_n'), $back); if (!is_email($email) || (!$product && strlen($query) < 2)) { wp_safe_redirect(usedkit_result_url($back, 'invalid')); exit; } $body = array('email' => $email, 'lang' => substr(get_locale(), 0, 2)); $max = absint($_POST['max_price'] ?? 0); if ($max > 0) { $body['max_price'] = $max; } if ($product) { $body['external_id'] = (string) $product; } else { $body['query'] = mb_substr($query, 0, 120); } $res = usedkit_call('/v1/watches', $body); $code = is_wp_error($res) ? 0 : wp_remote_retrieve_response_code($res); $state = $code === 200 ? 'check-email' : ($code === 429 ? 'busy' : 'error'); wp_safe_redirect(usedkit_result_url($back, $state)); exit; } // phpcs:enable add_action('admin_post_usedkit_watch', 'usedkit_handle_watch'); add_action('admin_post_nopriv_usedkit_watch', 'usedkit_handle_watch'); /* The used market price, from Kamarvo, under the price: a buyer comparing sees the * shop's price against the market without leaving. Cached a day per product. */ add_action('woocommerce_single_product_summary', function () { global $product; if (!$product || usedkit_settings()['market'] !== 'yes') { return; } $cache = 'usedkit_market2_' . $product->get_id() . '_' . substr(get_locale(), 0, 2); $m = get_transient($cache); if ($m === false) { $s = usedkit_settings(); $res = wp_remote_get(add_query_arg(array('external_id' => $product->get_id(), 'lang' => substr(get_locale(), 0, 2)), rtrim($s['service_url'], '/') . '/v1/market'), array('timeout' => 4, 'headers' => array('Authorization' => 'Bearer ' . $s['api_key']))); $m = (!is_wp_error($res) && wp_remote_retrieve_response_code($res) === 200) ? (json_decode(wp_remote_retrieve_body($res), true)['market'] ?? null) : null; set_transient($cache, $m ? $m : 'none', DAY_IN_SECONDS); } if (is_array($m)) { printf('%s %s · Kamarvo
', esc_html__('Used market price:', 'usedkit'), esc_url($m['url']), esc_html($m['price_text'])); } }, 11); /* ---- trade-in quotes ------------------------------------------------------------------- */ function usedkit_conditions() { return array( 'excellent' => __('Excellent: like new, no visible wear', 'usedkit'), 'good' => __('Good: light signs of use, fully working', 'usedkit'), 'fair' => __('Fair: clear wear, fully working', 'usedkit'), ); } /** The quote: the used market price from Kamarvo times the shop's share for the * condition, rounded down to a 5. None when Kamarvo has no confident figure. */ function usedkit_quote($query, $condition) { $s = usedkit_settings(); $res = wp_remote_get(add_query_arg(array('query' => $query, 'lang' => substr(get_locale(), 0, 2)), rtrim($s['service_url'], '/') . '/v1/market'), array('timeout' => 6, 'headers' => array('Authorization' => 'Bearer ' . $s['api_key']))); $m = (!is_wp_error($res) && wp_remote_retrieve_response_code($res) === 200) ? (json_decode(wp_remote_retrieve_body($res), true)['market'] ?? null) : null; $pct = (int) ($s['trade_pct'][$condition] ?? 0); $q = array('query' => $query, 'condition' => $condition, 'model' => $m['title'] ?? $query, 'market' => $m['price_text'] ?? null, 'url' => $m['url'] ?? null, 'offer' => null, 'currency' => $m['currency'] ?? get_woocommerce_currency()); if ($m && !empty($m['cents']) && empty($m['thin']) && $pct > 0) { $q['offer'] = (int) (floor($m['cents'] * $pct / 100 / 500) * 5); } elseif ($pct > 0 && function_exists('usedkit_reference_on') && usedkit_reference_on()) { // No market price: what the shop has sold the model for, if it has sold a few. $r = usedkit_reference(usedkit_query_key($query)); if ($r && $r['count'] >= 2) { $q['offer'] = (int) (floor($r['median'] * $pct / 100 / 5) * 5); $q['own'] = $r['count']; $q['currency'] = get_woocommerce_currency(); } } return $q; } add_shortcode('usedkit_tradein', function () { $id = usedkit_result('usedkit_q'); $q = $id ? get_transient('usedkit_q_' . $id) : false; $sent = usedkit_result() === 'sent'; $back = esc_url_raw(remove_query_arg(array('usedkit_q', 'usedkit', 'usedkit_n'), usedkit_here())); $action = admin_url('admin-post.php'); ob_start(); echo '' . esc_html__('Thanks! We\'ll get back to you shortly.', 'usedkit') . '
%s %s
', esc_html__('Our offer:', 'usedkit'), esc_html(usedkit_money($q['offer'], array('currency' => $q['currency'], 'decimals' => 0)))); if ($q['market']) { printf('%s %s · Kamarvo. %s
', esc_html__('Based on the used market price', 'usedkit'), esc_url($q['url']), esc_html($q['market']), esc_html__('Final offer after we have seen the item.', 'usedkit')); } elseif (!empty($q['own'])) { printf('%s %s
', esc_html__('Based on what we have sold this model for.', 'usedkit'), esc_html__('Final offer after we have seen the item.', 'usedkit')); } } else { echo '' . esc_html__('We\'ll check this one and email you an offer.', 'usedkit') . '
'; } echo '%s %s