'', 'secret' => '', 'enabled' => 1, 'allowed_ips' => '', 'author_id' => 0, ], $opt); } /** * Autor de las entradas nuevas que publica Ailoquence. La petición llega firmada por HMAC, sin * sesión de WordPress, así que sin esto wp_insert_post las crea con post_author = 0 ("(No * author)" en el editor, sin firma en muchos temas ni autor en el schema de los plugins SEO). * * 'author_id' se guarda al activar (el admin que activó el plugin) y se puede cambiar en ajustes. * Si falta — sitios que vienen de 0.2.0 —, o ese usuario se borró o ya no puede publicar, se usa * el primer administrador, para que una entrada nueva nunca vuelva a quedarse sin autor. */ function ailoquence_resolve_author_id() { $opt = ailoquence_get_options(); $id = (int) $opt['author_id']; if ($id > 0 && user_can($id, 'publish_posts')) return $id; $admins = get_users([ 'role' => 'administrator', 'orderby' => 'ID', 'order' => 'ASC', 'number' => 1, 'fields' => 'ID', ]); return !empty($admins) ? (int) $admins[0] : 0; } /** * Longitudes 24/48 NO son arbitrarias: el backend de Ailoquence * (WordPressServiceImpl.shouldUseAiloquenceConnector) decide si un blog usa este plugin o la API * REST estándar de WordPress mirando que usuario/contraseña midan >= 20 y >= 40 caracteres. */ function ailoquence_generate_blog_credentials() { return [ 'public_key' => wp_generate_password(24, false, false), 'secret' => wp_generate_password(48, true, true), ]; } function ailoquence_client_ip() { return isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : ''; } /** * Normaliza la lista de IPs permitidas: descarta lo que no sea una IP válida para que un error * tipográfico no deje el plugin bloqueando todas las peticiones sin que se note al guardar. * Devuelve [lista normalizada, número de entradas descartadas]. */ function ailoquence_sanitize_ip_list($raw) { $entries = array_filter(array_map('trim', explode(',', (string) $raw)), 'strlen'); $valid = array_values(array_filter($entries, function ($ip) { return filter_var($ip, FILTER_VALIDATE_IP) !== false; })); return [implode(', ', $valid), count($entries) - count($valid)]; } function ailoquence_ip_allowed() { $opt = ailoquence_get_options(); $allowed = trim((string) ($opt['allowed_ips'] ?? '')); if ($allowed === '') return true; $list = array_filter(array_map('trim', explode(',', $allowed))); return in_array(ailoquence_client_ip(), $list, true); } /** * HMAC: * signature = hex(hmac_sha256(secret, timestamp + "." + rawBody)) * * Devuelve true o WP_Error — sirve tal cual como permission_callback de las rutas REST. */ function ailoquence_verify_request(WP_REST_Request $request) { $opt = ailoquence_get_options(); if (empty($opt['enabled'])) { return new WP_Error('ailoquence_disabled', 'Ailoquence Connector disabled', ['status' => 403]); } if (!ailoquence_ip_allowed()) { return new WP_Error('ailoquence_ip_denied', 'IP not allowed', ['status' => 403]); } $key = $request->get_header('x-ailoquence-key'); $ts = $request->get_header('x-ailoquence-timestamp'); $sig = $request->get_header('x-ailoquence-signature'); if (!$key || !$ts || !$sig) { return new WP_Error('ailoquence_missing_headers', 'Missing auth headers', ['status' => 401]); } if (!hash_equals((string) $opt['public_key'], (string) $key)) { return new WP_Error('ailoquence_bad_key', 'Invalid key', ['status' => 401]); } if (abs(time() - intval($ts)) > 300) { return new WP_Error('ailoquence_ts_skew', 'Timestamp out of range', ['status' => 401]); } $calc = hash_hmac('sha256', $ts . '.' . $request->get_body(), (string) $opt['secret']); if (!hash_equals(strtolower($calc), strtolower((string) $sig))) { return new WP_Error('ailoquence_bad_sig', 'Invalid signature', ['status' => 401]); } return true; }