/* Options / slugs */ const PAGE_SLUG = 't14sync-opt'; const OPT_ALIASES = 't14sync_brand_aliases'; // [brand_id => "Short"] const OPT_TITLE_FMT = 't14sync_title_format'; // pattern string const OPT_SETTINGS = 't14sync_settings'; // base_url, client_id, client_secret const OPT_TOKEN = 't14sync_token'; // access_token, expires_at /* Admin-post actions */ const ACTION_RUN = 't14optimizer_run'; const ACT_SAVE_ALIASES = 't14optimizer_save_aliases'; public static function boot(){ $self = new self(); add_action('admin_post_' . self::ACTION_RUN, [ $self, 'handle_run' ]); add_action('admin_post_' . self::ACT_SAVE_ALIASES, [ $self, 'handle_save_aliases' ]); } /* ---------- Page ---------- */ public static function render_page(){ if ( ! current_user_can('manage_options') ) return; $version = defined('T14SYNC_VERSION') ? T14SYNC_VERSION : 'dev'; $aliases = get_option(self::OPT_ALIASES, []); $fmt = get_option(self::OPT_TITLE_FMT, '{brand_short} {make} {model} {year} - {name}'); $active = isset($_GET['tab']) && $_GET['tab']==='brands' ? 'brands' : 'slugs'; // token status (compact) $tok = get_option(self::OPT_TOKEN, []); $tok_msg = ''; if ( ! empty($tok['access_token']) && ! empty($tok['expires_at']) ){ $mins = max(0, floor(($tok['expires_at'] - time())/60)); $tok_msg = 'Token OK — ~'.intval($mins).' min left (cached).'; } else { $tok_msg = 'No token cached (only needed when fixing descriptions/images).'; } echo '
Build: '.esc_html($version).'
'.esc_html($tok_msg).'
'; // tabs echo 'You can set short names under the “Brand Short Names” tab.
'; if ($aliases){ echo '%d
→ %sNo aliases yet.
'; } echo 'No matching products found for this scope.
'; return; } echo 'ID | ' . 'SKU | ' . 'Old Title | New Title | ' . 'Old Slug | New Slug | ' . '
---|---|---|---|---|---|
%d | %s | %s | %s | %s | %s |
Switch to Apply mode and click “Run Optimizer” to save the new titles/slugs.'; echo ' Cancel is available next to Apply to return without resubmitting.
'; // Inject Apply/Cancel helper (reposts with mode=apply) + cancel link that avoids resubmit echo ''; } /* ---------- Helpers ---------- */ private static function parse_id_list(string $raw): array { $raw = trim($raw); if ($raw === '') return []; $out = []; foreach (preg_split('~\s*,\s*~', $raw) as $tok){ if (preg_match('~^\d+$~', $tok)){ $out[] = (int)$tok; } elseif (preg_match('~^(\d+)\s*-\s*(\d+)$~', $tok, $m)){ $a = (int)$m[1]; $b = (int)$m[2]; if ($a <= $b){ for ($i=$a; $i<=$b; $i++) $out[] = $i; } } } return array_values(array_unique(array_filter($out))); } private static function gather_bits(int $post_id): array { $sku = get_post_meta($post_id, '_sku', true); $bid = (int) get_post_meta($post_id, '_t14_brand_id', true); $bname = get_post_meta($post_id, '_t14_brand_name', true); $item = (int) get_post_meta($post_id, '_t14_item_id', true); // Year/Make/Model (common storage guesses) $year = get_post_meta($post_id, '_t14_year', true); $make = get_post_meta($post_id, '_t14_make', true); $model = get_post_meta($post_id, '_t14_model', true); // Also try attributes if present if ( ! $year ) $year = get_post_meta($post_id, 'pa_year', true); if ( ! $make ) $make = get_post_meta($post_id, 'pa_make', true); if ( ! $model ) $model = get_post_meta($post_id, 'pa_model', true); return [ 'sku' => trim((string)$sku), 'brand_id' => $bid, 'brand_name' => (string)$bname, 'year' => (string)$year, 'make' => (string)$make, 'model' => (string)$model, 'item_id' => $item, ]; } private static function build_title(string $fmt, array $d, array $aliases, string $fallback): string { $brand = $d['brand_name'] ?: ''; $short = ($d['brand_id'] && isset($aliases[$d['brand_id']])) ? $aliases[$d['brand_id']] : ($brand ?: ''); $map = [ '{brand_short}' => trim($short), '{brand}' => trim($brand), '{make}' => trim((string)$d['make']), '{model}' => trim((string)$d['model']), '{year}' => trim((string)$d['year']), '{sku}' => trim((string)$d['sku']), '{name}' => $fallback ?: '', ]; $title = $fmt; foreach ($map as $k=>$v) $title = str_replace($k, $v, $title); // collapse spaces/delims $title = trim(preg_replace('~\s+~', ' ', $title)); $title = trim(trim($title, '-–—')); if ($title === '') $title = $fallback ?: 'Product'; return $title; } private static function build_slug(string $title, string $sku, bool $includeSku, string $pos, string $case, string $delim): string { $core = sanitize_title($title); if ($case === 'lower') $core = strtolower($core); if ( $includeSku && $sku !== '' ){ $skus = sanitize_title($sku); $glue = ($delim !== '') ? $delim : ''; $core = ($pos === 'prefix') ? ($skus.$glue.$core) : ($core.$glue.$skus); } return $core; } private static function unique_slug_for(int $post_id, string $slug): string { if ($slug === '') return ''; $post = get_post($post_id); if ( ! $post ) return $slug; return wp_unique_post_slug($slug, $post_id, $post->post_status, $post->post_type, $post->post_parent); } /* ----- Turn14 helpers (optional fixers) ----- */ private static function ensure_token(): string { $tok = get_option(self::OPT_TOKEN, []); if ( ! empty($tok['access_token']) && ! empty($tok['expires_at']) && $tok['expires_at'] > time()+60 ){ return (string)$tok['access_token']; } // Try to fetch one silently if we have creds $o = get_option(self::OPT_SETTINGS, []); $base = rtrim((string)($o['base_url'] ?? ''), '/'); $cid = (string)($o['client_id'] ?? ''); $sec = (string)($o['client_secret'] ?? ''); if ( ! $base || ! $cid || ! $sec ) return ''; $resp = wp_remote_post($base.'/v1/token', [ 'timeout' => 15, 'headers' => [ 'Authorization' => 'Basic '.base64_encode($cid.':'.$sec), 'Content-Type' => 'application/x-www-form-urlencoded', 'Accept' => 'application/json', ], 'body' => 'grant_type=client_credentials', ]); if ( is_wp_error($resp) || (int)wp_remote_retrieve_response_code($resp)!==200 ) return ''; $data = json_decode(wp_remote_retrieve_body($resp), true); if ( empty($data['access_token']) ) return ''; $ttl = (int)($data['expires_in'] ?? 3600); $store = [ 'access_token' => (string)$data['access_token'], 'expires_at' => time()+max(300,$ttl-60), ]; update_option(self::OPT_TOKEN, $store, false); return $store['access_token']; } private static function fetch_overview(int $item_id, string $token): string { $o = get_option(self::OPT_SETTINGS, []); $base = rtrim((string)($o['base_url'] ?? ''), '/'); if ( ! $base ) return ''; $resp = wp_remote_get($base.'/v1/items/'.$item_id, [ 'timeout'=>20, 'headers'=>[ 'Authorization'=>'Bearer '.$token, 'Accept'=>'application/json', ], ]); if ( is_wp_error($resp) || (int)wp_remote_retrieve_response_code($resp)!==200 ) return ''; $json = json_decode(wp_remote_retrieve_body($resp), true); $attr = is_array($json['data']['attributes'] ?? null) ? $json['data']['attributes'] : []; // Try typical fields foreach (['overview','long_description','description','product_description'] as $k){ if ( ! empty($attr[$k]) ){ return wp_kses_post( (string)$attr[$k] ); } } return ''; } private static function maybe_upgrade_image(int $post_id, int $item_id, string $token): void { // If current featured image is already >= 300px wide, leave it $thumb_id = get_post_thumbnail_id($post_id); if ( $thumb_id ){ $meta = wp_get_attachment_metadata($thumb_id); if ( is_array($meta) && ! empty($meta['width']) && (int)$meta['width'] >= 300 ) return; } $o = get_option(self::OPT_SETTINGS, []); $base = rtrim((string)($o['base_url'] ?? ''), '/'); if ( ! $base ) return; $resp = wp_remote_get($base.'/v1/items/'.$item_id, [ 'timeout'=>20, 'headers'=>[ 'Authorization'=>'Bearer '.$token, 'Accept'=>'application/json', ], ]); if ( is_wp_error($resp) || (int)wp_remote_retrieve_response_code($resp)!==200 ) return; $json = json_decode(wp_remote_retrieve_body($resp), true); $attr = is_array($json['data']['attributes'] ?? null) ? $json['data']['attributes'] : []; $url = ''; if ( ! empty($attr['images']) && is_array($attr['images']) ){ // Pick the first non-thumb if available foreach ($attr['images'] as $img){ $candidate = (string)($img['url'] ?? $img['path'] ?? ''); if ($candidate){ $url = $candidate; break; } } } if ( ! $url && ! empty($attr['thumbnail']) ) $url = (string)$attr['thumbnail']; if ( ! $url ) return; include_once ABSPATH.'wp-admin/includes/file.php'; include_once ABSPATH.'wp-admin/includes/media.php'; include_once ABSPATH.'wp-admin/includes/image.php'; $tmp = download_url($url, 20); if ( is_wp_error($tmp) ) return; $basename = basename(parse_url($url, PHP_URL_PATH)); if(!$basename || $basename==='/' || strpos($basename,'.')===false) $basename='t14-image-'.md5($url).'.jpg'; $ctype = function_exists('mime_content_type') ? @mime_content_type($tmp) : ''; if(!$ctype) $ctype='image/jpeg'; $file = [ 'name'=>$basename, 'type'=>$ctype, 'tmp_name'=>$tmp, 'error'=>0, 'size'=>@filesize($tmp) ]; $att_id = media_handle_sideload($file, $post_id); if ( is_wp_error($att_id) ){ @unlink($tmp); return; } update_post_meta($att_id, '_t14_src_url', esc_url_raw($url)); set_post_thumbnail($post_id, $att_id); } } endif;