'_yoast_wpseo_title', 'description' => '_yoast_wpseo_metadesc'); case 'rankmath': return array('title' => 'rank_math_title', 'description' => 'rank_math_description'); case 'seopress': return array('title' => '_seopress_titles_title', 'description' => '_seopress_titles_desc'); default: return array('title' => '_webagentica_meta_title', 'description' => '_webagentica_meta_desc'); } } // --------------------------------------------------------------------------- // Lectura y escritura de SEO y FAQs // --------------------------------------------------------------------------- function wdbasic_get_seo($type, $id, $taxonomy = '') { $plugin = wdbasic_seo_plugin(); if ('term' === $type) { if ('yoast' === $plugin && class_exists('WPSEO_Taxonomy_Meta')) { return array( 'title' => (string) WPSEO_Taxonomy_Meta::get_term_meta($id, $taxonomy, 'title'), 'description' => (string) WPSEO_Taxonomy_Meta::get_term_meta($id, $taxonomy, 'desc'), ); } $keys = wdbasic_post_keys(); return array( 'title' => (string) get_term_meta($id, $keys['title'], true), 'description' => (string) get_term_meta($id, $keys['description'], true), ); } $keys = wdbasic_post_keys(); return array( 'title' => (string) get_post_meta($id, $keys['title'], true), 'description' => (string) get_post_meta($id, $keys['description'], true), ); } function wdbasic_set_seo($type, $id, $taxonomy, $seo) { $plugin = wdbasic_seo_plugin(); if ('term' === $type && 'yoast' === $plugin) { // Yoast guarda el SEO de los términos en una opción; se escribe en una sola operación // (varias llamadas seguidas a WPSEO_Taxonomy_Meta::set_value se pisan entre sí). $all = get_option('wpseo_taxonomy_meta', array()); $all = is_array($all) ? $all : array(); if (!isset($all[$taxonomy][$id]) || !is_array($all[$taxonomy][$id])) { $all[$taxonomy][$id] = array(); } if (isset($seo['title'])) { $all[$taxonomy][$id]['wpseo_title'] = sanitize_text_field($seo['title']); } if (isset($seo['description'])) { $all[$taxonomy][$id]['wpseo_desc'] = sanitize_text_field($seo['description']); } update_option('wpseo_taxonomy_meta', $all); return; } $keys = wdbasic_post_keys(); $update = 'term' === $type ? 'update_term_meta' : 'update_post_meta'; foreach (array('title', 'description') as $field) { if (isset($seo[$field])) { $update($id, $keys[$field], sanitize_text_field($seo[$field])); } } } /** Regenera el «indexable» de Yoast, de donde Yoast saca el HTML de título, descripción y schema. */ function wdbasic_rebuild_yoast($type, $id) { if ('yoast' !== wdbasic_seo_plugin() || !function_exists('YoastSEO')) { return; } try { $object_type = 'term' === $type ? 'term' : 'post'; $repository = YoastSEO()->classes->get('Yoast\WP\SEO\Repositories\Indexable_Repository'); $builder = YoastSEO()->classes->get('Yoast\WP\SEO\Builders\Indexable_Builder'); $indexable = $repository->find_by_id_and_type($id, $object_type, false); $builder->build_for_id_and_type($id, $object_type, $indexable ? $indexable : false); } catch (\Throwable $e) { // Si cambia la API interna de Yoast, basta con la actualización estándar de wdbasic_refresh(). unset($e); } } /** * Dispara los ganchos estándar de guardado para que los plugins de SEO que cachean su salida * (p. ej. los «indexables» de Yoast) regeneren el HTML, igual que al guardar desde el editor. */ /** Texto del cuerpo (entradas y páginas) o descripción (categorías), saneado con las reglas de WordPress. */ function wdbasic_set_content($type, $id, $taxonomy, $content) { $clean = wp_kses_post($content); if ('term' === $type) { wp_update_term($id, $taxonomy, array('description' => $clean)); return; } wp_update_post(array('ID' => $id, 'post_content' => $clean)); } function wdbasic_refresh($type, $id, $taxonomy) { wdbasic_rebuild_yoast($type, $id); if ('term' === $type) { $term = get_term($id, $taxonomy); if ($term && !is_wp_error($term)) { clean_term_cache($id, $taxonomy); // Actualización estándar: WordPress avisa a los plugins de SEO como al guardar desde el escritorio. wp_update_term($id, $taxonomy, array()); } return; } clean_post_cache($id); if (get_post($id)) { // Actualización estándar: WordPress avisa a los plugins de SEO como al guardar desde el editor. wp_update_post(array('ID' => $id)); } } function wdbasic_get_faqs($type, $id) { $raw = 'term' === $type ? get_term_meta($id, WDBASIC_FAQS, true) : get_post_meta($id, WDBASIC_FAQS, true); $faqs = json_decode((string) $raw, true); return is_array($faqs) ? array_values($faqs) : array(); } function wdbasic_set_faqs($type, $id, $faqs) { $clean = array(); foreach ((array) $faqs as $faq) { $q = isset($faq['question']) ? sanitize_text_field($faq['question']) : ''; $a = isset($faq['answer']) ? sanitize_textarea_field($faq['answer']) : ''; if ('' !== $q && '' !== $a) { $clean[] = array('question' => $q, 'answer' => $a); } } $value = wp_json_encode($clean, JSON_UNESCAPED_UNICODE); $update = 'term' === $type ? 'update_term_meta' : 'update_post_meta'; $update($id, WDBASIC_FAQS, wp_slash($value)); } function wdbasic_entry($type, $id) { if ('term' === $type) { $term = get_term($id); if (!$term || is_wp_error($term)) { return null; } return array( 'type' => 'term', 'id' => (int) $term->term_id, 'subtype' => $term->taxonomy, 'title' => $term->name, 'url' => get_term_link($term), 'seo' => wdbasic_get_seo('term', $term->term_id, $term->taxonomy), 'faqs' => wdbasic_get_faqs('term', $term->term_id), 'content' => (string) $term->description, 'editable' => current_user_can('edit_term', $term->term_id), ); } $post = get_post($id); if (!$post || 'publish' !== $post->post_status) { return null; } return array( 'type' => 'post', 'id' => (int) $post->ID, 'subtype' => $post->post_type, 'title' => get_the_title($post), 'url' => get_permalink($post), 'seo' => wdbasic_get_seo('post', $post->ID), 'faqs' => wdbasic_get_faqs('post', $post->ID), 'content' => (string) $post->post_content, 'noindex' => wdbasic_get_noindex($post->ID), 'editable' => current_user_can('edit_post', $post->ID), ); } /** URL pública → contenido (entrada/página de cualquier tipo, o término de una taxonomía). */ function wdbasic_resolve($url) { $post_id = url_to_postid($url); if (!$post_id && untrailingslashit($url) === untrailingslashit(home_url('/'))) { $post_id = (int) get_option('page_on_front'); } if ($post_id) { return array('type' => 'post', 'id' => $post_id); } $path = trim((string) wp_parse_url($url, PHP_URL_PATH), '/'); $slug = basename($path); foreach (get_taxonomies(array('public' => true), 'names') as $taxonomy) { $term = get_term_by('slug', $slug, $taxonomy); if ($term && !is_wp_error($term) && untrailingslashit(get_term_link($term)) === untrailingslashit($url)) { return array('type' => 'term', 'id' => (int) $term->term_id); } } return null; } /** Datos para la auditoría inicial: ajustes de WordPress que afectan al SEO. */ function wdbasic_health() { $sitemaps = function_exists('wp_sitemaps_get_server') ? wp_sitemaps_get_server()->sitemaps_enabled() : false; $theme = wp_get_theme(); $plugins = array(); $versions = array(); if (! function_exists('get_plugins')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $all = get_plugins(); foreach ((array) get_option('active_plugins', array()) as $file) { $slug = dirname($file) === '.' ? basename($file, '.php') : dirname($file); $plugins[] = $slug; // Versión instalada: WdBasic la cruza con la base de datos pública de vulnerabilidades conocidas (plan Monitor). if (isset($all[ $file ]['Version'])) { $versions[ $slug ] = (string) $all[ $file ]['Version']; } } return array( 'wordpress' => get_bloginfo('version'), 'php' => PHP_VERSION, 'locale' => get_locale(), 'searchEngines' => '1' === (string) get_option('blog_public'), 'permalinks' => (string) get_option('permalink_structure'), 'https' => 'https' === wp_parse_url(home_url(), PHP_URL_SCHEME), 'coreSitemap' => $sitemaps ? home_url('/wp-sitemap.xml') : null, 'theme' => $theme->get('Name'), 'plugins' => $plugins, 'pluginVersions' => (object) $versions, 'themeSlug' => get_stylesheet(), 'themeVersion' => (string) $theme->get('Version'), 'posts' => (int) wp_count_posts('post')->publish, 'pages' => (int) wp_count_posts('page')->publish, 'frontPage' => 'page' === get_option('show_on_front') ? get_permalink((int) get_option('page_on_front')) : 'latest-posts', 'googleVerification' => (bool) get_option(WDBASIC_GOOGLE_VERIFICATION), 'schema' => (bool) get_option('webagentica_schema'), 'redirects' => count((array) get_option('webagentica_redirects', array())), 'isAdmin' => current_user_can('manage_options'), ); } // --------------------------------------------------------------------------- // API REST: /wp-json/wdbasic/v1/... (requiere usuario con permiso de edición) // --------------------------------------------------------------------------- add_action('rest_api_init', function () { $can_edit = function () { return current_user_can('edit_posts'); }; $entry_args = array( 'type' => array('required' => true, 'enum' => array('post', 'term')), // Closure: WordPress pasa 3 argumentos y las funciones nativas de PHP 8 (is_numeric) no los aceptan. 'id' => array( 'required' => true, 'validate_callback' => function ($value) { return is_numeric($value); }, ), ); wdbasic_route('/status', array( 'methods' => 'GET', 'permission_callback' => $can_edit, 'callback' => function (WP_REST_Request $request) { // WdBasic consulta esta ruta en cada revisión: sirve para mostrar en el escritorio que la web está conectada. update_option('wdbasic_last_contact', time(), false); // La misma auditoría del escritorio (puntuación y problemas): WdBasic muestra esa puntuación, así el dueño ve un // único número en WordPress y en su panel. Con ?fresh=1 se recalcula. $audit = wdbasic_get_audit((bool) $request->get_param('fresh')); return array( 'version' => WDBASIC_VERSION, 'seoPlugin' => wdbasic_seo_plugin(), 'user' => wp_get_current_user()->user_login, 'site' => home_url('/'), 'health' => wdbasic_health(), 'audit' => array( 'score' => (int) $audit['score'], 'time' => (int) $audit['time'], 'issues' => array_map( function ($i) { return array( 'id' => $i['id'], 'severity' => $i['severity'], 'title' => $i['title'], 'count' => isset($i['count']) ? (int) $i['count'] : null, ); }, $audit['issues'] ), ), ); }, )); // Artículo nuevo (aprobado por el propietario): se publica directamente. wdbasic_route('/posts', array( 'methods' => 'POST', 'permission_callback' => function () { return current_user_can('publish_posts'); }, 'callback' => function (WP_REST_Request $request) { $b = $request->get_json_params(); $title = isset($b['title']) ? sanitize_text_field($b['title']) : ''; if ('' === $title || empty($b['content'])) { return new WP_Error('wdbasic_invalid', 'Title and content are required', array('status' => 400)); } $cats = array(); foreach ((array) ($b['categories'] ?? array()) as $name) { $term = term_exists(sanitize_text_field($name), 'category'); if (!$term) { $term = wp_insert_term(sanitize_text_field($name), 'category'); } if ($term && !is_wp_error($term)) { $cats[] = (int) $term['term_id']; } } $id = wp_insert_post( array( 'post_title' => $title, 'post_name' => isset($b['slug']) ? sanitize_title($b['slug']) : '', 'post_content' => wp_kses_post($b['content']), 'post_excerpt' => isset($b['excerpt']) ? sanitize_textarea_field($b['excerpt']) : '', 'post_status' => 'publish', 'post_type' => 'post', 'post_author' => get_current_user_id(), 'post_category' => array_filter($cats), ), true ); if (is_wp_error($id)) { return $id; } if (!empty($b['seo']) && is_array($b['seo'])) { wdbasic_set_seo('post', $id, 'post', $b['seo']); } if (!empty($b['faqs'])) { wdbasic_set_faqs('post', $id, $b['faqs']); } wdbasic_refresh('post', $id, 'post'); return wdbasic_entry('post', $id); }, )); // Retirar o volver a publicar un artículo creado por WdBasic (deshacer). wdbasic_route('/posts/(?P\d+)/status', array( 'methods' => 'POST', 'permission_callback' => function (WP_REST_Request $request) { return current_user_can('edit_post', (int) $request['id']); }, 'callback' => function (WP_REST_Request $request) { $status = (string) ($request->get_json_params()['status'] ?? ''); if (!in_array($status, array('draft', 'publish'), true)) { return new WP_Error('wdbasic_invalid', 'Status must be draft or publish', array('status' => 400)); } $r = wp_update_post(array('ID' => (int) $request['id'], 'post_status' => $status), true); return is_wp_error($r) ? $r : array('id' => (int) $request['id'], 'status' => $status, 'url' => get_permalink((int) $request['id'])); }, )); // Código de verificación de Google Search Console (solo administradores). wdbasic_route('/google-verification', array( 'methods' => 'POST', 'permission_callback' => function () { return current_user_can('manage_options'); }, 'args' => array( 'token' => array( 'required' => true, 'validate_callback' => function ($value) { return is_string($value) && preg_match('/^[A-Za-z0-9_-]{0,100}$/', $value); }, ), ), 'callback' => function (WP_REST_Request $request) { $token = (string) $request['token']; if ('' === $token) { delete_option(WDBASIC_GOOGLE_VERIFICATION); } else { update_option(WDBASIC_GOOGLE_VERIFICATION, $token, true); } return array('token' => $token); }, )); wdbasic_route('/resolve', array( 'methods' => 'GET', 'permission_callback' => $can_edit, 'args' => array('url' => array('required' => true)), 'callback' => function (WP_REST_Request $request) { $ref = wdbasic_resolve(esc_url_raw($request['url'])); if (!$ref) { return new WP_Error('wdbasic_not_found', 'No editable content at this URL', array('status' => 404)); } return wdbasic_entry($ref['type'], $ref['id']); }, )); wdbasic_route('/entry/(?Ppost|term)/(?P\d+)', array( array( 'methods' => 'GET', 'permission_callback' => $can_edit, 'args' => $entry_args, 'callback' => function (WP_REST_Request $request) { $entry = wdbasic_entry($request['type'], (int) $request['id']); return $entry ? $entry : new WP_Error('wdbasic_not_found', 'Not found', array('status' => 404)); }, ), array( 'methods' => 'POST', 'args' => $entry_args, 'permission_callback' => function (WP_REST_Request $request) { $id = (int) $request['id']; return 'term' === $request['type'] ? current_user_can('edit_term', $id) : current_user_can('edit_post', $id); }, 'callback' => function (WP_REST_Request $request) { $type = $request['type']; $id = (int) $request['id']; $entry = wdbasic_entry($type, $id); if (!$entry) { return new WP_Error('wdbasic_not_found', 'Not found', array('status' => 404)); } $body = $request->get_json_params(); if (isset($body['seo']) && is_array($body['seo'])) { wdbasic_set_seo($type, $id, $entry['subtype'], $body['seo']); } if (isset($body['faqs'])) { wdbasic_set_faqs($type, $id, $body['faqs']); } if (isset($body['noindex']) && 'post' === $type) { wdbasic_set_noindex($id, (bool) $body['noindex']); } if (isset($body['content']) && is_string($body['content'])) { wdbasic_set_content($type, $id, $entry['subtype'], $body['content']); } wdbasic_refresh($type, $id, $entry['subtype']); return wdbasic_entry($type, $id); }, ), )); }); // --------------------------------------------------------------------------- // Salida en la web // --------------------------------------------------------------------------- /** Sin plugin de SEO: título y metadescripción propios. */ add_filter('pre_get_document_title', function ($title) { if ('none' !== wdbasic_seo_plugin()) { return $title; } $custom = ''; if (is_singular()) { $custom = get_post_meta(get_queried_object_id(), '_webagentica_meta_title', true); } elseif (is_category() || is_tag() || is_tax()) { $custom = get_term_meta(get_queried_object_id(), '_webagentica_meta_title', true); } return $custom ? $custom : $title; }, 20); /** Etiqueta de verificación de Google, con o sin plugin de SEO. */ add_action('wp_head', function () { $token = get_option(WDBASIC_GOOGLE_VERIFICATION); if ($token) { echo '' . "\n"; } }, 1); add_action('wp_head', function () { if ('none' !== wdbasic_seo_plugin()) { return; } $desc = ''; if (is_singular()) { $desc = get_post_meta(get_queried_object_id(), '_webagentica_meta_desc', true); } elseif (is_category() || is_tag() || is_tax()) { $desc = get_term_meta(get_queried_object_id(), '_webagentica_meta_desc', true); } if ($desc) { echo '' . "\n"; } }, 1); /** HTML y datos estructurados (FAQPage) de las FAQs. */ function wdbasic_faqs_html($faqs) { if (!$faqs) { return ''; } $items = array(); $html = '

' . esc_html__('Frequently asked questions', 'wdbasic') . '

'; foreach ($faqs as $faq) { $html .= '
' . esc_html($faq['question']) . '

' . nl2br(esc_html($faq['answer'])) . '

'; $items[] = array( '@type' => 'Question', 'name' => $faq['question'], 'acceptedAnswer' => array('@type' => 'Answer', 'text' => $faq['answer']), ); } $schema = array('@context' => 'https://schema.org', '@type' => 'FAQPage', 'mainEntity' => $items); $html .= '
'; return $html; } add_filter('the_content', function ($content) { if (!is_singular() || !in_the_loop() || !is_main_query()) { return $content; } return $content . wdbasic_faqs_html(wdbasic_get_faqs('post', get_the_ID())); }, 20); add_filter('get_the_archive_description', function ($description) { if (!(is_category() || is_tag() || is_tax())) { return $description; } return $description . wdbasic_faqs_html(wdbasic_get_faqs('term', get_queried_object_id())); }, 20); // --------------------------------------------------------------------------- // Arreglos de SEO del sitio (1.4.0): noindex, texto alternativo, datos del negocio, redirecciones, visibilidad // --------------------------------------------------------------------------- /** ¿La entrada está marcada como «no indexar» en el plugin de SEO activo (o en el nuestro)? */ function wdbasic_get_noindex($id) { switch (wdbasic_seo_plugin()) { case 'yoast': return '1' === (string) get_post_meta($id, '_yoast_wpseo_meta-robots-noindex', true); case 'rankmath': return in_array('noindex', (array) get_post_meta($id, 'rank_math_robots', true), true); case 'seopress': return 'yes' === (string) get_post_meta($id, '_seopress_robots_index', true); default: return '1' === (string) get_post_meta($id, '_webagentica_noindex', true); } } function wdbasic_set_noindex($id, $noindex) { switch (wdbasic_seo_plugin()) { case 'yoast': $noindex ? update_post_meta($id, '_yoast_wpseo_meta-robots-noindex', '1') : delete_post_meta($id, '_yoast_wpseo_meta-robots-noindex'); break; case 'rankmath': $robots = array_values(array_diff((array) get_post_meta($id, 'rank_math_robots', true), array('noindex', 'index'))); $robots[] = $noindex ? 'noindex' : 'index'; update_post_meta($id, 'rank_math_robots', $robots); break; case 'seopress': $noindex ? update_post_meta($id, '_seopress_robots_index', 'yes') : delete_post_meta($id, '_seopress_robots_index'); break; default: $noindex ? update_post_meta($id, '_webagentica_noindex', '1') : delete_post_meta($id, '_webagentica_noindex'); } wdbasic_refresh('post', $id, 'post'); } /** Sin plugin de SEO, nuestro «no indexar» se aplica con el filtro de robots de WordPress. */ add_filter('wp_robots', function ($robots) { if ('none' === wdbasic_seo_plugin() && is_singular() && wdbasic_get_noindex(get_queried_object_id())) { $robots['noindex'] = true; } return $robots; }); /** Imágenes de la biblioteca sin texto alternativo, con contexto para describirlas. */ function wdbasic_images_missing_alt($limit = 30) { $q = new WP_Query( array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'post_mime_type' => 'image', 'posts_per_page' => $limit, 'meta_query' => array( 'relation' => 'OR', array('key' => '_wp_attachment_image_alt', 'compare' => 'NOT EXISTS'), array('key' => '_wp_attachment_image_alt', 'value' => ''), ), ) ); $out = array(); foreach ($q->posts as $img) { $parent = $img->post_parent ? get_post($img->post_parent) : null; $out[] = array( 'id' => (int) $img->ID, 'url' => wp_get_attachment_image_url($img->ID, 'medium'), 'file' => basename((string) get_attached_file($img->ID)), 'title' => get_the_title($img), 'caption' => wp_get_attachment_caption($img->ID), 'usedOn' => $parent && 'publish' === $parent->post_status ? array('title' => get_the_title($parent), 'url' => get_permalink($parent)) : null, ); } return $out; } /** En el contenido, las imágenes sin alt toman el de la biblioteca (así el arreglo vale también para imágenes ya insertadas). */ add_filter('the_content', function ($html) { return preg_replace_callback('/]*\bwp-image-(\d+)\b[^>]*>/i', function ($m) { $tag = $m[0]; if (preg_match('/\balt\s*=\s*"[^"]+"/i', $tag)) { return $tag; } $alt = trim((string) get_post_meta((int) $m[1], '_wp_attachment_image_alt', true)); if ('' === $alt) { return $tag; } $tag = preg_replace('/\salt\s*=\s*"[^"]*"/i', '', $tag); return preg_replace('/^ $v) { $key = is_int($k) ? $k : preg_replace('/[^A-Za-z@]/', '', (string) $k); if ('' !== $key) { $out[$key] = wdbasic_clean_schema($v); } } return $out; } return is_scalar($value) ? sanitize_text_field((string) $value) : ''; } /** Datos del negocio como JSON-LD en todas las páginas. */ add_action('wp_head', function () { $schema = get_option('webagentica_schema'); if ($schema && is_array($schema)) { echo '\n"; } }, 5); /** Redirecciones: solo para direcciones que no existen (404), para no tapar nunca una página real. */ add_action('template_redirect', function () { if (!is_404()) { return; } $uri = isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : ''; $path = trailingslashit((string) wp_parse_url($uri, PHP_URL_PATH)); foreach ((array) get_option('webagentica_redirects', array()) as $r) { if (isset($r['from'], $r['to']) && trailingslashit($r['from']) === $path) { wp_safe_redirect($r['to'], 301, 'WdBasic'); exit; } } }, 1); add_action('rest_api_init', function () { $admin = function () { return current_user_can('manage_options'); }; wdbasic_route('/images', array( 'methods' => 'GET', 'permission_callback' => function () { return current_user_can('upload_files'); }, 'callback' => function () { return wdbasic_images_missing_alt(); }, )); wdbasic_route('/images/(?P\d+)/alt', array( 'methods' => 'POST', 'permission_callback' => function (WP_REST_Request $request) { return current_user_can('edit_post', (int) $request['id']); }, 'callback' => function (WP_REST_Request $request) { $id = (int) $request['id']; $alt = sanitize_text_field((string) ($request->get_json_params()['alt'] ?? '')); if ('attachment' !== get_post_type($id)) { return new WP_Error('wdbasic_not_found', 'Not found', array('status' => 404)); } '' === $alt ? delete_post_meta($id, '_wp_attachment_image_alt') : update_post_meta($id, '_wp_attachment_image_alt', $alt); return array('id' => $id, 'alt' => $alt); }, )); wdbasic_route('/schema', array( array( 'methods' => 'GET', 'permission_callback' => $admin, 'callback' => function () { return array('schema' => get_option('webagentica_schema') ?: null); }, ), array( 'methods' => 'POST', 'permission_callback' => $admin, 'callback' => function (WP_REST_Request $request) { $schema = $request->get_json_params()['schema'] ?? null; if (empty($schema)) { delete_option('webagentica_schema'); return array('schema' => null); } update_option('webagentica_schema', wdbasic_clean_schema((array) $schema), true); return array('schema' => get_option('webagentica_schema')); }, ), )); wdbasic_route('/redirects', array( array( 'methods' => 'GET', 'permission_callback' => $admin, 'callback' => function () { return array_values((array) get_option('webagentica_redirects', array())); }, ), array( 'methods' => 'POST', 'permission_callback' => $admin, 'callback' => function (WP_REST_Request $request) { $b = $request->get_json_params(); $from = '/' . ltrim((string) wp_parse_url((string) ($b['from'] ?? ''), PHP_URL_PATH), '/'); $to = esc_url_raw((string) ($b['to'] ?? '')); $del = !empty($b['delete']); if ('/' === $from || (!$del && ('' === $to || !preg_match('#^(https?://|/)#', (string) $b['to'])))) { return new WP_Error('wdbasic_invalid', 'from and to are required', array('status' => 400)); } $list = array_values(array_filter((array) get_option('webagentica_redirects', array()), function ($r) use ($from) { return trailingslashit($r['from']) !== trailingslashit($from); })); if (!$del) { $list[] = array('from' => $from, 'to' => $to); } update_option('webagentica_redirects', $list, true); return $list; }, ), )); wdbasic_route('/settings', array( 'methods' => 'POST', 'permission_callback' => $admin, 'callback' => function (WP_REST_Request $request) { $b = $request->get_json_params(); if (isset($b['searchEngines'])) { update_option('blog_public', $b['searchEngines'] ? '1' : '0'); } return array('searchEngines' => '1' === (string) get_option('blog_public')); }, )); // Resumen de la cuenta de WdBasic para mostrarlo en el escritorio (plan, propuestas pendientes, últimos cambios, // salud de la web, oportunidades). Lo envía WdBasic a diario y tras cada cambio; el plugin solo lo guarda y lo muestra, // sin llamar a ningún servidor al abrir la página. wdbasic_route('/summary', array( 'methods' => 'POST', 'permission_callback' => $can_edit, 'callback' => function (WP_REST_Request $request) { update_option('wdbasic_summary', wdbasic_sanitize_summary((array) $request->get_json_params()), false); return array('ok' => true); }, )); }); /** Solo los campos esperados, con su tipo: el resumen se muestra en el escritorio. */ function wdbasic_sanitize_summary(array $b) { $text = function ($v, $max = 200) { return mb_substr(sanitize_text_field((string) $v), 0, $max); }; $int = function ($v) { return null === $v || '' === $v ? null : (int) $v; }; $plans = array('free', 'monitor', 'assistant', 'growth'); $health = isset($b['health']) && is_array($b['health']) ? $b['health'] : null; $out = array( 'updated' => time(), 'plan' => in_array($b['plan'] ?? '', $plans, true) ? $b['plan'] : 'free', 'firstScore' => $int($b['firstScore'] ?? null), 'pending' => max(0, (int) ($b['pending'] ?? 0)), 'todo' => max(0, (int) ($b['todo'] ?? 0)), 'recent' => array(), 'opportunities' => array(), 'upsell' => array(), 'health' => null, ); foreach (array_slice((array) ($b['recent'] ?? array()), 0, 5) as $r) { $out['recent'][] = array( 'field' => $text($r['field'] ?? '', 40), 'page' => $text($r['page'] ?? '', 120), 'date' => $int($r['date'] ?? null), ); } foreach (array_slice((array) ($b['opportunities'] ?? array()), 0, 3) as $o) { $out['opportunities'][] = $text($o, 100); } foreach (array_slice((array) ($b['upsell'] ?? array()), 0, 5) as $u) { $out['upsell'][] = $text($u, 160); } if ($health) { $out['health'] = array( 'up' => !empty($health['up']), 'sslDays' => $int($health['sslDays'] ?? null), 'broken' => $int($health['broken'] ?? null), 'speed' => $int($health['speed'] ?? null), 'speedOk' => !empty($health['speedOk']), 'vulns' => $int($health['vulns'] ?? null), 'checked' => $int($health['checked'] ?? null), ); } return $out; }