/* 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 '
'; echo '

Product Optimization

'; echo '

Build: '.esc_html($version).'

'; echo '

'.esc_html($tok_msg).'

'; // tabs echo ''; if ($active==='brands'){ self::render_brand_alias_ui($aliases); } else { self::render_slug_optimizer_ui($fmt, $aliases); } // small CSS tidy echo ''; echo '
'; } /* ---------- Slug/Title Optimizer ---------- */ private static function render_slug_optimizer_ui(string $fmt, array $aliases){ $defaults = [ 'mode' => 'preview', 'scope' => 'missing', // all | missing | brand | ids 'brand_id' => '', 'ids' => '', 'limit' => '250', 'sku_where' => 'suffix', // prefix | suffix 'delimiter' => '-', // -, _, '' (none) 'slug_case' => 'lower', // lower | keep 'include_sku' => '1', 'try_overview' => '', // fetch long/overview (needs token) 'fix_images' => '', // try full-size image from item detail 'title_format' => $fmt, // pattern ]; $v = array_merge($defaults, array_map('strval', $_POST ?: [])); echo '
'; // Left: Controls echo '
'; echo '

Configure

'; echo '
'; wp_nonce_field(self::ACT_RUN); echo ''; echo ''; // Mode echo ''; // Scope echo ''; // Title format echo ''; // Slug options echo ''; // Fixers echo ''; // Batch size echo ''; echo '
Mode'; echo ''; echo ''; echo 'Preview shows results, Apply writes post_title / post_name and optional fixes below.'; echo '
Scope'; echo '
'; echo '
'; echo ' '; echo ' '; echo 'Matches product meta _t14_brand_id.
'; echo ' '; echo ''; echo '
Title format'; echo ''; echo '

Placeholders: {brand_short}, {brand}, {make}, {model}, {year}, {sku}, {name}. Example: {brand_short} {make} {model} {year} - {name}

'; echo '
Slug options'; echo ''; echo ''; echo ''; echo ''; echo '
Optional fixers'; echo ''; echo ''; echo '
Batch size'; echo ''; echo '
'; echo '

'; echo ''; // Apply/Cancel row appears after preview via JS if needed as well echo '

'; echo '
'; echo '
'; // Right: Legend echo '
'; echo '

Brand short names

'; echo '

You can set short names under the “Brand Short Names” tab.

'; if ($aliases){ echo ''; } else { echo '

No aliases yet.

'; } echo '
'; echo '
'; // split // If POSTed, show preview/apply results inline if ( ! empty($_POST) && check_admin_referer(self::ACT_RUN) ){ $res = self::run_optimizer($v, true); self::render_preview_table($res); } } /* ---------- Brand Aliases ---------- */ private static function render_brand_alias_ui(array $aliases){ // Known brands list (from earlier sync cache if any) $known = get_option('t14sync_brand_cats', []); // [bid => ['name'=>...]] $all_ids = array_unique(array_merge(array_keys($known), array_keys($aliases))); sort($all_ids, SORT_NUMERIC); echo '
'; echo '

Short Names

'; echo '
'; wp_nonce_field(self::ACT_SAVE_ALIASES); echo ''; echo ''; if ( ! $all_ids ){ echo ''; } else { foreach ($all_ids as $bid){ $full = isset($known[$bid]['name']) ? $known[$bid]['name'] : ''; $alias = $aliases[$bid] ?? ''; printf('', (int)$bid, esc_html($full), (int)$bid, esc_attr($alias) ); } } echo '
Brand IDFull NameShort Name
No brands recorded yet.
%d%s
'; echo '

'; echo '
'; echo '
'; } public function handle_save_aliases(){ if ( ! current_user_can('manage_options') ) wp_die('Forbidden'); check_admin_referer(self::ACT_SAVE_ALIASES); $in = isset($_POST['alias']) && is_array($_POST['alias']) ? array_map('wp_unslash', $_POST['alias']) : []; $clean = []; foreach ($in as $bid => $short){ $bid = (int)$bid; $short = trim( (string) $short ); if ($bid > 0 && $short !== '') $clean[$bid] = $short; } update_option(self::OPT_ALIASES, $clean, false); wp_safe_redirect( admin_url('admin.php?page='.self::PAGE_SLUG.'&tab=brands&saved=1') ); exit; } /* ---------- Run / Preview ---------- */ private static function run_optimizer(array $v, bool $force_preview){ global $wpdb; $mode = $force_preview ? 'preview' : (($v['mode'] ?? 'preview') === 'apply' ? 'apply' : 'preview'); $limit = max(1, min(2000, (int)($v['limit'] ?? 250))); $scope = $v['scope'] ?? 'missing'; $brand_id = (int)($v['brand_id'] ?? 0); $ids_raw = (string)($v['ids'] ?? ''); $fmt = (string)($v['title_format'] ?? '{brand_short} {make} {model} {year} - {name}'); $includeSku = ! empty($v['include_sku']); $skuWhere = ($v['sku_where'] ?? 'suffix') === 'prefix' ? 'prefix' : 'suffix'; $slugCase = ($v['slug_case'] ?? 'lower') === 'keep' ? 'keep' : 'lower'; $delim = (string)($v['delimiter'] ?? '-'); $doOverview = ! empty($v['try_overview']); $doFixImg = ! empty($v['fix_images']); // Query $where = "p.post_type='product' AND p.post_status IN ('publish','draft','pending','private')"; $join = ''; $prep = []; if ( $scope === 'missing' ){ $where .= " AND (p.post_name='' OR p.post_name REGEXP '^[0-9]+$' OR p.post_name REGEXP '^product(-[0-9]+)?$')"; } elseif ( $scope === 'brand' ){ if ($brand_id > 0){ $join .= " INNER JOIN {$wpdb->postmeta} pm ON pm.post_id=p.ID AND pm.meta_key='_t14_brand_id' AND pm.meta_value=%d "; $prep[] = $brand_id; } else { $where .= " AND 1=0 "; } } elseif ( $scope === 'ids' ){ $ids = self::parse_id_list($ids_raw); if ($ids){ $where .= " AND p.ID IN (".implode(',', array_map('intval',$ids)).")"; } else { $where .= " AND 1=0 "; } } $sql = "SELECT p.ID, p.post_title, p.post_name FROM {$wpdb->posts} p {$join} WHERE {$where} ORDER BY p.ID DESC LIMIT %d"; $prep[] = $limit; $rows = $wpdb->get_results( $wpdb->prepare($sql, ...$prep), ARRAY_A ); $aliases = get_option(self::OPT_ALIASES, []); $stats = ['updated'=>0,'skipped'=>0,'errors'=>0]; $items = []; // Token (for optional fixers) $token = ''; if ( $doOverview || $doFixImg ){ $token = self::ensure_token(); } foreach ($rows as $r){ $pid = (int)$r['ID']; $old_t = (string)$r['post_title']; $old_s = (string)$r['post_name']; $data = self::gather_bits($pid); // sku, brand_id, brand_name, make, model, year, item_id $title = self::build_title($fmt, $data, $aliases, $old_t); $slug = self::build_slug($title, $data['sku'], $includeSku, $skuWhere, $slugCase, $delim); $slug = self::unique_slug_for($pid, $slug); $items[] = [ 'ID' => $pid, 'sku' => $data['sku'], 'brand' => $data['brand_name'], 'old_t' => $old_t, 'new_t' => $title, 'old_s' => $old_s, 'new_s' => $slug, ]; if ($mode === 'apply'){ // Title + slug $ok = wp_update_post([ 'ID'=>$pid, 'post_title'=>$title, 'post_name'=>$slug ], true); if ( is_wp_error($ok) ){ $stats['errors']++; continue; } // Optional: description from Turn14 overview if ( $doOverview && $token && $data['item_id'] ){ $desc = self::fetch_overview((int)$data['item_id'], $token); if ($desc !== ''){ wp_update_post([ 'ID'=>$pid, 'post_excerpt'=>$desc ]); // short description } } // Optional: upgrade featured image if small, from item detail if ( $doFixImg && $token && $data['item_id'] ){ self::maybe_upgrade_image($pid, (int)$data['item_id'], $token); } $stats['updated']++; } } return [ 'mode' => $mode, 'items' => $items, 'stats' => $stats, ]; } private static function render_preview_table(array $res){ $mode = $res['mode'] ?? 'preview'; $items = $res['items'] ?? []; $stats = $res['stats'] ?? ['updated'=>0,'skipped'=>0,'errors'=>0]; if ( ! $items ){ echo '

No matching products found for this scope.

'; return; } echo '

Preview

'; echo ''; echo '' . '' . '' . '' . '' . ''; foreach ($items as $it){ printf( '', (int)$it['ID'], esc_html($it['sku']), esc_html($it['old_t']), esc_html($it['new_t']), esc_html($it['old_s']), esc_html($it['new_s']) ); } echo '
IDSKUOld TitleNew TitleOld SlugNew Slug
%d%s%s%s%s%s
'; echo '

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; https://kteller.com/post-sitemap.xml 2024-10-26T16:04:24+00:00 https://kteller.com/page-sitemap.xml 2025-09-14T06:00:02+00:00 https://kteller.com/attachment-sitemap.xml 2025-09-19T15:19:04+00:00 https://kteller.com/category-sitemap.xml 2024-10-26T16:04:24+00:00 https://kteller.com/post_tag-sitemap.xml 2021-04-04T04:04:54+00:00 https://kteller.com/author-sitemap.xml 2024-01-11T05:44:54+00:00