AlkantarClanX12

Your IP : 216.73.217.24


Current Path : /www/capitalgmcbuickregina_830/public/wp-content/plugins/leadbox/lib/
Upload File :
Current File : /www/capitalgmcbuickregina_830/public/wp-content/plugins/leadbox/lib/llms-txt.php

<?php

/**
 * LBT-1506 (OEM AI-Readiness A2): sirve /llms.txt dinámico por site.
 *
 * Markdown plano (spec llmstxt.org) con la identidad del dealer (NAP,
 * horarios, marcas) desde Dealer Settings y links a los hubs del site
 * (SRP new/used, service, parts, financing, contact, about). Se genera
 * on-the-fly en cada request de /llms.txt, así que refleja cambios de
 * settings al instante y no necesita cache ni invalidación.
 *
 * @package LBX
 */

// Exit if accessed directly.
if (!defined('ABSPATH')) {
  exit;
}

/**
 * Intercepta las requests a /llms.txt y responde text/plain.
 *
 * Se cuelga de `init` comparando el path del request en vez de usar
 * add_rewrite_rule: funciona en los ~95 sites al deployar el plugin sin
 * depender de un flush de rewrite rules por site.
 *
 * @return void
 */
function lbx_llms_txt_maybe_serve()
{
  if (is_admin() || wp_doing_ajax() || wp_doing_cron() || (defined('WP_CLI') && WP_CLI)) {
    return;
  }

  $path = (string) parse_url((string) ($_SERVER['REQUEST_URI'] ?? ''), PHP_URL_PATH);

  // Sites servidos desde un subdirectorio: se descuenta el path del home.
  $home_path = rtrim((string) parse_url(home_url('/'), PHP_URL_PATH), '/');
  if ($home_path !== '' && strpos($path, $home_path) === 0) {
    $path = substr($path, strlen($home_path));
  }

  if (trim($path, '/') !== 'llms.txt') {
    return;
  }

  nocache_headers();
  header('Content-Type: text/plain; charset=utf-8');
  header('Cache-Control: public, max-age=3600');

  echo lbx_llms_txt_content(); // phpcs:ignore WordPress.Security.EscapeOutput -- text/plain generado internamente.
  exit;
}
add_action('init', 'lbx_llms_txt_maybe_serve', 1);

/**
 * Construye el contenido Markdown del llms.txt del site.
 *
 * Secciones y bullets sin data se omiten: cada site emite solo lo que
 * tiene configurado.
 *
 * @return string
 */
function lbx_llms_txt_content()
{
  $contact = get_option('dealer-settings-contact-options');
  $contact = is_array($contact) ? $contact : [];
  $lb = get_option('leadbox-settings');
  $lb = is_array($lb) ? $lb : [];

  $name = trim((string) ($contact['dealer_name'] ?? ''));
  if ($name === '') {
    $name = (string) get_bloginfo('name');
  }

  // Marcas: manufacturer + all_brands (mismas fuentes que el schema AutoDealer).
  $brands = [];
  foreach (array_merge([(string) ($lb['manufacturer'] ?? '')], explode(',', (string) ($lb['all_brands'] ?? ''))) as $brand) {
    $brand = trim($brand);
    $key = strtolower($brand);
    if ($brand !== '' && !isset($brands[$key])) {
      $brands[$key] = $brand === $key ? ucwords($brand) : $brand;
    }
  }
  $brands = array_values($brands);

  $city     = trim((string) ($contact['address_city'] ?? ''));
  $province = trim((string) ($contact['address_province'] ?? ''));
  $street   = trim((string) ($contact['address_line_1'] ?? ''));
  $postal   = trim((string) ($contact['address_postal_code'] ?? ''));

  $lines   = [];
  $lines[] = '# ' . $name;
  $lines[] = '';

  $summary = $name . ' is a' . (!empty($brands) ? ' ' . implode(', ', $brands) : 'n automotive') . ' dealership';
  if ($city !== '') {
    $summary .= ' in ' . $city . ($province !== '' ? ', ' . $province : '') . ', Canada';
  }
  $summary .= '. New and used vehicle sales, service, parts and financing.';
  $lines[] = '> ' . $summary;
  $lines[] = '';

  $address = trim(implode(', ', array_filter([$street, $city, trim($province . ' ' . $postal)])));
  if ($address !== '') {
    $lines[] = '- Address: ' . $address;
  }
  foreach (['Sales' => 'sale_phone', 'Service' => 'service_phone', 'Parts' => 'parts_phone'] as $label => $key) {
    $phone = trim((string) ($contact[$key] ?? ''));
    if ($phone !== '') {
      $lines[] = '- ' . $label . ' phone: ' . $phone;
    }
  }
  $lines[] = '- Website: ' . home_url('/');
  $lines[] = '';

  // Horarios por departamento, un bullet por depto.
  $hours_lines = [];
  foreach (['Sales' => 'sale', 'Service' => 'service', 'Parts' => 'parts'] as $label => $type) {
    $line = lbx_llms_txt_hours_line($type);
    if ($line !== '') {
      $hours_lines[] = '- ' . $label . ': ' . $line;
    }
  }
  if (!empty($hours_lines)) {
    $lines[] = '## Hours';
    $lines[] = '';
    $lines   = array_merge($lines, $hours_lines);
    $lines[] = '';
  }

  // Hubs de inventario: páginas con el bloque inventory clasificadas por status.
  $srp = lbx_llms_txt_find_srp_pages();
  $inventory_lines = [];
  if (!empty($srp['new'])) {
    $inventory_lines[] = '- [New vehicles](' . $srp['new'] . '): New' . (!empty($brands) ? ' ' . implode(', ', $brands) : '') . ' inventory with pricing and availability';
  }
  if (!empty($srp['used'])) {
    $inventory_lines[] = '- [Used vehicles](' . $srp['used'] . '): Pre-owned inventory with pricing and availability';
  }
  if (empty($inventory_lines) && !empty($srp['all'])) {
    $inventory_lines[] = '- [Vehicle inventory](' . $srp['all'] . '): Full inventory with pricing and availability';
  }
  if (!empty($inventory_lines)) {
    $lines[] = '## Inventory';
    $lines[] = '';
    $lines   = array_merge($lines, $inventory_lines);
    $lines[] = '';
  }

  // Hubs de servicios y contacto: primera página publicada que matchee un slug candidato.
  $sections = [
    'Services' => [
      ['Service', ['service', 'service-department', 'schedule-service', 'book-a-service', 'book-service', 'auto-service', 'entretien', 'rendez-vous-service'], 'Vehicle service and maintenance appointments'],
      ['Parts', ['parts', 'order-parts', 'parts-department', 'auto-parts', 'pieces', 'commander-des-pieces'], 'OEM parts and accessories'],
      ['Financing', ['financing', 'finance', 'apply-for-financing', 'get-approved', 'car-loans', 'credit', 'financement', 'demande-de-credit'], 'Vehicle financing and credit applications'],
    ],
    'About' => [
      ['Contact', ['contact', 'contact-us', 'contactez-nous', 'nous-joindre'], 'Location, directions and contact form'],
      ['About us', ['about', 'about-us', 'dealership', 'our-dealership', 'our-story', 'a-propos'], 'Dealership information'],
    ],
  ];
  foreach ($sections as $section => $entries) {
    $section_lines = [];
    foreach ($entries as $entry) {
      list($label, $slugs, $description) = $entry;
      $url = lbx_llms_txt_find_page($slugs);
      if ($url !== '') {
        $section_lines[] = '- [' . $label . '](' . $url . '): ' . $description;
      }
    }
    if (!empty($section_lines)) {
      $lines[] = '## ' . $section;
      $lines[] = '';
      $lines   = array_merge($lines, $section_lines);
      $lines[] = '';
    }
  }

  return implode("\n", $lines);
}

/**
 * Encuentra los SRP del site: páginas publicadas con el bloque
 * leadbox/inventory-block con el filtro activo y sin preset de modelo
 * (los showcases tipo "Bronco" no son hubs de inventario). Clasifica por
 * el preset de status en new/used/all y devuelve el permalink de la
 * primera página (ID ascendente) de cada grupo.
 *
 * @return array{new: string, used: string, all: string}
 */
function lbx_llms_txt_find_srp_pages()
{
  global $wpdb;

  $hubs = ['new' => '', 'used' => '', 'all' => ''];

  $rows = $wpdb->get_results(
    "SELECT ID, post_content FROM {$wpdb->posts}
     WHERE post_type = 'page' AND post_status = 'publish'
       AND post_content LIKE '%wp:leadbox/inventory-block%'
     ORDER BY ID ASC"
  );

  foreach ((array) $rows as $row) {
    if (!preg_match_all('/<!--\s*wp:leadbox\/inventory-block\s*(\{.*?\})?\s*\/?-->/s', (string) $row->post_content, $matches)) {
      continue;
    }

    foreach ($matches[1] as $json) {
      $attrs = $json !== '' ? json_decode($json, true) : [];
      if (!is_array($attrs)) {
        $attrs = [];
      }

      if (!filter_var($attrs['filter'] ?? false, FILTER_VALIDATE_BOOLEAN)) {
        continue;
      }
      if (!empty($attrs['model_selected']) || (!empty($attrs['model']) && $attrs['model'] !== 'all')) {
        continue;
      }

      $status = strtolower(trim((string) ($attrs['status'] ?? 'all')));
      $key = in_array($status, ['new', 'used'], true) ? $status : 'all';
      if ($hubs[$key] === '') {
        $hubs[$key] = (string) get_permalink($row->ID);
      }
    }
  }

  return $hubs;
}

/**
 * Permalink de la primera página publicada cuyo slug matchee la lista de
 * candidatos, o '' si ninguno existe.
 *
 * @param  array $slugs Slugs candidatos en orden de preferencia.
 * @return string
 */
function lbx_llms_txt_find_page(array $slugs)
{
  foreach ($slugs as $slug) {
    $page = get_page_by_path($slug);
    if ($page instanceof WP_Post && $page->post_status === 'publish') {
      return (string) get_permalink($page);
    }
  }

  return '';
}

/**
 * Línea compacta de horarios de un departamento a partir de la option
 * dealer-settings-hours-options: "Monday 9:00 am - 6:00 pm | ... | Sunday Closed".
 * Días sin datos se omiten; devuelve '' si el departamento no tiene nada.
 *
 * @param  string $type Prefijo del departamento: sale | service | parts.
 * @return string
 */
function lbx_llms_txt_hours_line($type)
{
  $hours = get_option('dealer-settings-hours-options');
  if (!is_array($hours)) {
    return '';
  }

  $parts = [];
  foreach (['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] as $day) {
    if (!empty($hours["{$type}_hours_{$day}_closed"])) {
      $parts[] = ucfirst($day) . ' Closed';
      continue;
    }
    $start = trim((string) ($hours["{$type}_hours_{$day}_start"] ?? ''));
    $end   = trim((string) ($hours["{$type}_hours_{$day}_end"] ?? ''));
    if ($start !== '' && $end !== '') {
      $parts[] = ucfirst($day) . ' ' . $start . ' - ' . $end;
    }
  }

  return implode(' | ', $parts);
}