USEDKIT_HOME, 'api_key' => '', 'show_on' => 'sold', // sold | all 'hide_sold' => 'no', // yes: sold items out of listings, pages kept 'market' => 'no', // yes: Kamarvo's market price on product pages 'trade_pct' => array('excellent' => 60, 'good' => 50, 'fair' => 40), 'trade_email' => '', // where trade-in requests go; the site admin's if empty ))); } /** A price as plain text, "1 234,00 €": for text that is escaped as a whole. */ function usedkit_money($amount, $args = array()) { return html_entity_decode(wp_strip_all_tags(wc_price($amount, $args)), ENT_QUOTES, 'UTF-8'); } function usedkit_call($path, $body, $blocking = true) { $s = usedkit_settings(); if (empty($s['api_key'])) { return new WP_Error('usedkit_no_key', 'Usedkit API key not set'); } return wp_remote_post(rtrim($s['service_url'], '/') . $path, array( 'timeout' => $blocking ? 8 : 3, 'blocking' => $blocking, 'headers' => array( 'Authorization' => 'Bearer ' . $s['api_key'], 'Content-Type' => 'application/json', ), 'body' => wp_json_encode($body), )); } /** The fields the service needs from a product. */ function usedkit_product_payload($product) { $image = wp_get_attachment_image_url($product->get_image_id(), 'medium'); $brand = ''; $terms = get_the_terms($product->get_id(), 'product_brand'); if (is_array($terms) && $terms) { $brand = $terms[0]->name; } $cats = wp_get_post_terms($product->get_id(), 'product_cat', array('fields' => 'names')); return array( 'external_id' => (string) $product->get_id(), 'title' => $product->get_name(), 'url' => get_permalink($product->get_id()), 'image' => $image ? $image : null, 'price' => $product->get_price(), 'currency' => get_woocommerce_currency(), 'in_stock' => $product->is_in_stock() && $product->get_status() === 'publish', 'brand' => $brand, 'category' => is_array($cats) && $cats ? $cats[0] : null, 'match_key' => $product->get_meta('_usedkit_match_key') ?: null, ); } /** Sync a product after any save or stock change. Non-blocking for the admin. */ function usedkit_sync($product_id) { $product = wc_get_product($product_id); if (!$product || $product->is_type('variation')) { return; } // Not waited for: saving a product in the admin must not hang on the service. A // change lost while the service is down is caught by the nightly full sync. usedkit_call('/v1/products', usedkit_product_payload($product), false); } /* A full sync every night. */ add_action('usedkit_nightly_sync', 'usedkit_sync_all'); add_action('init', function () { if (!wp_next_scheduled('usedkit_nightly_sync')) { wp_schedule_event(strtotime('tomorrow 03:15'), 'daily', 'usedkit_nightly_sync'); } // Markdowns, expired holds and offers: early, before the shop's day starts. if (!wp_next_scheduled('usedkit_daily')) { wp_schedule_event(strtotime('tomorrow 04:30'), 'daily', 'usedkit_daily'); } }); register_deactivation_hook(__FILE__, function () { wp_clear_scheduled_hook('usedkit_nightly_sync'); wp_clear_scheduled_hook('usedkit_daily'); }); /* How many customers each product would notify: a column in the product list, so the * demand behind a model is in view while pricing and buying stock. */ add_filter('manage_edit-product_columns', function ($cols) { $cols['usedkit_waiting'] = __('Waiting', 'usedkit'); return $cols; }); add_action('manage_product_posts_custom_column', function ($col, $id) { static $counts = null; if ($col !== 'usedkit_waiting') { return; } if ($counts === null) { global $wp_query; $ids = array_map(function ($p) { return (string) (is_object($p) ? $p->ID : $p); }, $wp_query->posts ?? array()); $key = 'usedkit_waiting_' . md5(implode(',', $ids)); $counts = get_transient($key); if ($counts === false) { $res = usedkit_call('/v1/waiting', array('external_ids' => $ids)); $counts = (!is_wp_error($res) && wp_remote_retrieve_response_code($res) === 200) ? (json_decode(wp_remote_retrieve_body($res), true)['waiting'] ?? array()) : array(); set_transient($key, $counts, 10 * MINUTE_IN_SECONDS); } } $n = (int) ($counts[(string) $id] ?? 0); echo $n ? '' . esc_html($n) . '' : ''; }, 10, 2); add_action('woocommerce_update_product', 'usedkit_sync', 20); add_action('woocommerce_new_product', 'usedkit_sync', 20); add_action('woocommerce_product_set_stock_status', 'usedkit_sync', 20); /** A full sync, from the settings page: every product, in batches. */ function usedkit_sync_all() { $page = 1; $sent = 0; do { $ids = wc_get_products(array('limit' => 200, 'page' => $page, 'return' => 'ids', 'status' => array('publish', 'private'))); $batch = array(); foreach ($ids as $id) { $p = wc_get_product($id); if ($p) { $batch[] = usedkit_product_payload($p); } } if ($batch) { usedkit_call('/v1/products', array('products' => $batch)); $sent += count($batch); } $page++; } while (count($ids) === 200); return $sent; } /* ---- the customer's form ---------------------------------------------------------- */ /** * A public form's result travels to the page it returns to as ?usedkit= (and the * trade-in quote as ?usedkit_q=), with a nonce: the page shows a result only when this * site produced it. */ function usedkit_result_url($url, $state = '', $quote = '') { $url = remove_query_arg(array('usedkit', 'usedkit_q', 'usedkit_n'), $url); $args = array('usedkit_n' => wp_create_nonce('usedkit_result')); if ($state !== '') { $args['usedkit'] = $state; } if ($quote !== '') { $args['usedkit_q'] = $quote; } return add_query_arg(array_map('rawurlencode', $args), $url); } /** The result a form's redirect carries, or '' when there is none or its nonce fails. */ function usedkit_result($key = 'usedkit') { $nonce = isset($_GET['usedkit_n']) ? sanitize_text_field(wp_unslash($_GET['usedkit_n'])) : ''; if ($nonce === '' || !wp_verify_nonce($nonce, 'usedkit_result')) { return ''; } return isset($_GET[$key]) ? sanitize_key(wp_unslash($_GET[$key])) : ''; } /** * A filter of an admin view (a date range, a month), from the view's own GET form, which * carries a nonce; or from its CSV link, which carries the CSV action's nonce. Null when * absent or neither nonce passes: the view then shows its default. */ function usedkit_view_arg($view, $key, $csv_action = '') { $nonce = isset($_GET['usedkit_view']) ? sanitize_text_field(wp_unslash($_GET['usedkit_view'])) : ''; $ok = $nonce !== '' && wp_verify_nonce($nonce, 'usedkit_view_' . $view); if (!$ok && $csv_action !== '') { $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field(wp_unslash($_GET['_wpnonce'])) : ''; $ok = $nonce !== '' && wp_verify_nonce($nonce, $csv_action); } if (!$ok || !isset($_GET[$key])) { return null; } return sanitize_text_field(wp_unslash($_GET[$key])); } /** The nonce for a view's GET filter form. */ function usedkit_view_nonce_field($view) { wp_nonce_field('usedkit_view_' . $view, 'usedkit_view', false); } /** The result of a sign-up, shown inside the form: works on classic and block themes. */ function usedkit_state_html() { $state = usedkit_result(); $msgs = array( 'check-email' => array('ok', __('Almost done: check your email and click the link to confirm.', 'usedkit')), 'invalid' => array('err', __('Please enter a valid email address.', 'usedkit')), 'error' => array('err', __('Something went wrong. Please try again later.', 'usedkit')), 'busy' => array('err', __('Too many requests. Please try again later.', 'usedkit')), ); if (!isset($msgs[$state])) { return ''; } return '

' . 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 '
'; if ($sent) { echo '

' . esc_html__('Thanks! We\'ll get back to you shortly.', 'usedkit') . '

'; } elseif ($q) { echo '
'; echo '

' . esc_html($q['model']) . '

'; if ($q['offer']) { printf('

%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 '
'; ?>

$name) : list($title, $desc) = array_pad(array_map('trim', explode(':', $name, 2)), 2, ''); ?>
'; return wp_kses(ob_get_clean(), usedkit_allowed_html()); }); /** Where a public form came from: read before its nonce is checked, so a failed check can * send the visitor back; wp_validate_redirect() keeps it to this site. */ function usedkit_back() { return wp_validate_redirect(esc_url_raw(wp_unslash($_POST['back'] ?? '')), home_url('/')); // phpcs:ignore WordPress.Security.NonceVerification.Missing } function usedkit_handle_quote() { usedkit_check_form(); // phpcs:disable WordPress.Security.NonceVerification -- verified in usedkit_check_form() $query = sanitize_text_field(wp_unslash($_POST['query'] ?? '')); $cond = sanitize_key($_POST['condition'] ?? 'good'); if (strlen($query) < 2 || !isset(usedkit_conditions()[$cond])) { wp_safe_redirect(usedkit_back()); exit; } $id = wp_generate_password(12, false); set_transient('usedkit_q_' . strtolower($id), usedkit_quote(mb_substr($query, 0, 120), $cond), HOUR_IN_SECONDS); wp_safe_redirect(usedkit_result_url(usedkit_back(), '', strtolower($id))); exit; } // phpcs:enable add_action('admin_post_usedkit_tradein_quote', 'usedkit_handle_quote'); add_action('admin_post_nopriv_usedkit_tradein_quote', 'usedkit_handle_quote'); function usedkit_handle_tradein_send() { usedkit_check_form(); // phpcs:disable WordPress.Security.NonceVerification -- verified in usedkit_check_form() $q = get_transient('usedkit_q_' . sanitize_key($_POST['q'] ?? '')); $email = sanitize_email(wp_unslash($_POST['email'] ?? '')); $name = sanitize_text_field(wp_unslash($_POST['name'] ?? '')); $notes = sanitize_textarea_field(wp_unslash($_POST['notes'] ?? '')); if (!$q || !is_email($email) || !$name) { wp_safe_redirect(usedkit_back()); exit; } do_action('usedkit_tradein_request', $q, $name, $email, $notes); $s = usedkit_settings(); $to = $s['trade_email'] ?: get_option('admin_email'); $conds = usedkit_conditions(); $body = sprintf("%s %s: %s %s: %s %s: %s %s: %s %s: %s <%s> %s", __('A trade-in request from the website.', 'usedkit'), __('Model', 'usedkit'), $q['model'] . ($q['model'] !== $q['query'] ? ' (' . $q['query'] . ')' : ''), __('Condition', 'usedkit'), $conds[$q['condition']], __('Offer shown', 'usedkit'), $q['offer'] ? $q['offer'] . ' ' . $q['currency'] : '—', __('Market price', 'usedkit'), $q['market'] ? $q['market'] . ' (' . $q['url'] . ')' : '—', __('From', 'usedkit'), $name, $email, $notes); /* translators: %s: the item's make and model */ wp_mail($to, sprintf(__('Trade-in request: %s', 'usedkit'), $q['model']), $body, array('Reply-To: ' . $name . ' <' . $email . '>')); wp_safe_redirect(usedkit_result_url(usedkit_back(), 'sent')); exit; } // phpcs:enable add_action('admin_post_usedkit_tradein_send', 'usedkit_handle_tradein_send'); add_action('admin_post_nopriv_usedkit_tradein_send', 'usedkit_handle_tradein_send'); /* Search with nothing found: offer to watch for what was searched. */ add_action('woocommerce_no_products_found', function () { if (is_search() && get_search_query()) { echo wp_kses(usedkit_form_html(null, get_search_query()), usedkit_allowed_html()); } }, 20); /* The same on block themes, where search results are blocks and their "nothing * found" message is a block of its own. Once per page. */ add_filter('render_block', function ($html, $block) { static $done = false; $names = array('core/query-no-results', 'woocommerce/product-collection-no-results'); if ($done || !in_array($block['blockName'] ?? '', $names, true) || !is_search() || !get_search_query()) { return $html; } $done = true; return $html . wp_kses(usedkit_form_html(null, get_search_query()), usedkit_allowed_html()); }, 10, 2); /* [usedkit_watch] anywhere; [usedkit_watch query="Leica M6"] for a fixed model. */ add_shortcode('usedkit_watch', function ($atts) { $atts = shortcode_atts(array('query' => ''), $atts); return wp_kses(usedkit_form_html(null, sanitize_text_field($atts['query'])), usedkit_allowed_html()); }); /* Sold items: out of the shop's listings, their pages kept -- with the form on them, * a sold page is where the next buyer signs up. */ add_action('woocommerce_product_query', function ($q) { if (usedkit_settings()['hide_sold'] !== 'yes' || is_admin()) { return; } $meta = (array) $q->get('meta_query'); $meta[] = array('key' => '_stock_status', 'value' => 'outofstock', 'compare' => '!='); $q->set('meta_query', $meta); }); add_action('before_woocommerce_init', function () { if (class_exists('\\Automattic\\WooCommerce\\Utilities\\FeaturesUtil')) { \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('custom_order_tables', __FILE__, true); } }); add_action('init', function () { load_plugin_textdomain('usedkit', false, dirname(plugin_basename(__FILE__)) . '/languages'); }); /* One translation per language (usedkit-de.l10n.php) serves every locale of it: de_DE, * de_AT, de_CH_informal. WordPress asks for the full locale; this falls back to the * language when there is no file for the locale itself. */ add_filter('load_textdomain_mofile', function ($mofile, $domain) { if ($domain !== 'usedkit' || file_exists($mofile) || file_exists(substr($mofile, 0, -3) . '.l10n.php')) { return $mofile; } return preg_replace('/usedkit-([a-z]{2,3})_[A-Za-z_]+\.mo$/', 'usedkit-$1.mo', $mofile); }, 10, 2); add_action('wp_enqueue_scripts', function () { wp_enqueue_style('usedkit', plugins_url('assets/usedkit.css', __FILE__), array(), USEDKIT_VERSION); }); /** A small line icon for the shop-facing boxes, in the text colour. */ function usedkit_icon($name) { $paths = array( 'bell' => '', 'tag' => '', 'clock' => '', 'swap' => '', ); return ''; } /** * The HTML Usedkit's own markup may contain, for wp_kses() wherever it is echoed or * returned: what a post may hold, plus the customer forms, the product fields, the icons' * SVG and responsive images (as WooCommerce's product lists print them). */ function usedkit_allowed_html() { static $allowed = null; if ($allowed !== null) { return $allowed; } $common = array('id' => true, 'class' => true, 'style' => true, 'name' => true, 'value' => true, 'placeholder' => true, 'required' => true, 'disabled' => true, 'readonly' => true, 'autocomplete' => true, 'aria-label' => true, 'aria-hidden' => true, 'tabindex' => true); $allowed = wp_kses_allowed_html('post'); $allowed['form'] = array('action' => true, 'method' => true, 'class' => true, 'id' => true, 'novalidate' => true); $allowed['input'] = $common + array('type' => true, 'checked' => true, 'min' => true, 'max' => true, 'step' => true, 'inputmode' => true, 'pattern' => true, 'size' => true, 'maxlength' => true, 'list' => true); $allowed['select'] = $common + array('multiple' => true); $allowed['option'] = array('value' => true, 'selected' => true, 'disabled' => true); $allowed['optgroup'] = array('label' => true); $allowed['textarea'] = $common + array('rows' => true, 'cols' => true, 'maxlength' => true); $allowed['button'] = $common + array('type' => true); $allowed['label'] = array('for' => true, 'class' => true, 'id' => true, 'style' => true); $allowed['fieldset'] = array('class' => true, 'id' => true); $allowed['legend'] = array('class' => true); $allowed['datalist'] = array('id' => true); $allowed['details'] = array('class' => true, 'open' => true, 'id' => true); $allowed['summary'] = array('class' => true); $allowed['bdi'] = array(); $allowed['mark'] = array('class' => true); $allowed['img'] = ($allowed['img'] ?? array()) + array('srcset' => true, 'sizes' => true, 'decoding' => true, 'loading' => true, 'fetchpriority' => true, 'class' => true); $allowed['svg'] = array('viewbox' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'aria-hidden' => true, 'class' => true, 'width' => true, 'height' => true, 'xmlns' => true); $allowed['path'] = array('d' => true); $allowed['circle'] = array('cx' => true, 'cy' => true, 'r' => true); return $allowed; } /* ---- settings ------------------------------------------------------------------------ */ add_action('admin_menu', function () { add_submenu_page('woocommerce', 'Usedkit', 'Usedkit', 'manage_woocommerce', 'usedkit', 'usedkit_settings_page'); }); function usedkit_settings_page() { if (!current_user_can('manage_woocommerce')) { return; } $notice = ''; if (isset($_POST['usedkit_save']) && check_admin_referer('usedkit_settings')) { update_option(USEDKIT_OPTION, array( 'service_url' => esc_url_raw(wp_unslash($_POST['service_url'] ?? '')), 'api_key' => sanitize_text_field(wp_unslash($_POST['api_key'] ?? '')), 'show_on' => sanitize_key(wp_unslash($_POST['show_on'] ?? '')) === 'all' ? 'all' : 'sold', 'hide_sold' => !empty($_POST['hide_sold']) ? 'yes' : 'no', 'market' => !empty($_POST['market']) ? 'yes' : 'no', 'trade_pct' => array_map(function ($v) { return max(0, min(100, absint($v))); }, array_intersect_key(array_map('absint', (array) wp_unslash($_POST['trade_pct'] ?? array())), array('excellent' => 1, 'good' => 1, 'fair' => 1))), 'trade_email' => sanitize_email(wp_unslash($_POST['trade_email'] ?? '')), ) + apply_filters('usedkit_save_settings', array(), map_deep(wp_unslash($_POST), 'sanitize_textarea_field'))); $notice = __('Saved.', 'usedkit'); } if (isset($_POST['usedkit_sync']) && check_admin_referer('usedkit_settings')) { /* translators: %d: number of products */ $notice = sprintf(__('Sent %d products.', 'usedkit'), usedkit_sync_all()); } $s = usedkit_settings(); $stats = null; $status = ''; if ($s['api_key']) { $res = wp_remote_get(rtrim($s['service_url'], '/') . '/v1/stats', array('timeout' => 5, 'headers' => array('Authorization' => 'Bearer ' . $s['api_key']))); $code = is_wp_error($res) ? 0 : wp_remote_retrieve_response_code($res); if ($code === 200) { $stats = json_decode(wp_remote_retrieve_body($res), true); $status = '✓ ' . esc_html__('Connected', 'usedkit') . ''; } elseif ($code === 401) { $status = '' . esc_html__('The key was not accepted. Check it, or get a new one.', 'usedkit') . ''; } else { $status = '' . esc_html__('The service could not be reached just now.', 'usedkit') . ''; } } ?>

Usedkit

·


$name) : ?>

__('Usedkit notify-me sign-ups', 'usedkit'), 'callback' => function ($email) { $res = usedkit_call('/v1/forget', array('email' => $email)); $ok = !is_wp_error($res) && wp_remote_retrieve_response_code($res) === 200; $n = $ok ? (int) (json_decode(wp_remote_retrieve_body($res), true)['deleted'] ?? 0) : 0; return array('items_removed' => $n > 0, 'items_retained' => false, 'messages' => $ok ? array() : array(__('The Usedkit service could not be reached; try again later.', 'usedkit')), 'done' => true); }, ); return $erasers; }); /* Suggested text for the shop's own privacy policy (Settings → Privacy → Policy Guide). */ add_action('admin_init', function () { if (function_exists('wp_add_privacy_policy_content')) { wp_add_privacy_policy_content('Usedkit', wp_kses_post(sprintf( /* translators: %s: link to Usedkit's data processing agreement */ __('When you ask to be told when a model comes back in stock, your email address, the model, an optional maximum price and your language are sent to Usedkit, which stores them and sends the emails on our behalf as our data processor (%s). Nothing is sent before you confirm by email, every email has an unsubscribe link, and a sign-up ends after a year.', 'usedkit'), '' . esc_html(USEDKIT_HOME . '/dpa') . ''))); } }); /* A Settings link on the plugins page, and a nudge until the key is in. */ add_filter('plugin_action_links_' . plugin_basename(__FILE__), function ($links) { array_unshift($links, '' . esc_html__('Settings', 'usedkit') . ''); return $links; }); add_action('admin_notices', function () { $screen = function_exists('get_current_screen') ? get_current_screen() : null; if (!current_user_can('manage_woocommerce') || usedkit_settings()['api_key'] !== '' || ($screen && strpos((string) $screen->id, 'usedkit') !== false)) { return; } printf('

%s %s

', esc_html__('Usedkit is installed. Add your key to start telling customers when sold models come back.', 'usedkit'), esc_url(admin_url('admin.php?page=usedkit')), esc_html__('Set it up', 'usedkit')); });