[ 'name' => 'Beta Feedback', 'singular_name' => 'Feedback', 'menu_name' => 'Beta Feedback', ], 'public' => false, 'show_ui' => true, 'show_in_menu' => true, 'menu_icon' => 'dashicons-feedback', 'menu_position' => 26, 'capability_type' => 'post', 'capabilities' => ['create_posts' => 'do_not_allow'], // solo se crean por API 'map_meta_cap' => true, 'supports' => ['title', 'editor'], 'exclude_from_search' => true, ]); }); /* ── 2a) Endpoint de consulta de idioma Polylang (para publicabot / integración externa) ── */ add_action('rest_api_init', function () { register_rest_route('fea/v1', '/lang/(?P\d+)', [ 'methods' => 'GET', 'permission_callback' => '__return_true', 'callback' => function (WP_REST_Request $req) { $id = (int) $req->get_param('id'); $post = get_post($id); if (!$post || $post->post_type !== 'post') { return new WP_REST_Response(['error' => 'post not found'], 404); } $langs = wp_get_object_terms($id, 'language', ['fields' => 'slugs']); $lang = (!is_wp_error($langs) && !empty($langs)) ? $langs[0] : null; return new WP_REST_Response(['id' => $id, 'lang' => $lang], 200); }, ]); }); /* ── 2) Endpoint REST para recibir el voto ────────────────────────────────── */ add_action('rest_api_init', function () { register_rest_route('fea/v1', '/feedback', [ 'methods' => 'POST', 'permission_callback' => '__return_true', // público (Beta); protegido con honeypot + rate-limit 'callback' => 'fea_feedback_submit', ]); }); function fea_feedback_submit(WP_REST_Request $req) { // Honeypot: si el campo oculto viene relleno, es un bot. if (trim((string) $req->get_param('website')) !== '') { return new WP_REST_Response(['ok' => true], 200); // fingir éxito } $vote = $req->get_param('vote') === 'up' ? 'up' : ($req->get_param('vote') === 'down' ? 'down' : ''); if ($vote === '') { return new WP_REST_Response(['ok' => false, 'error' => 'voto inválido'], 400); } // Rate-limit por IP. $ip = fea_feedback_client_ip(); $key = 'fea_fb_rl_' . md5($ip); $n = (int) get_transient($key); if ($n >= FEA_FB_RATE_MAX) { return new WP_REST_Response(['ok' => false, 'error' => 'demasiados envíos'], 429); } set_transient($key, $n + 1, HOUR_IN_SECONDS); $comment = trim((string) $req->get_param('comment')); $comment = mb_substr(wp_strip_all_tags($comment), 0, FEA_FB_COMMENT_MAX); $url = esc_url_raw((string) $req->get_param('url')); $src_id = (int) $req->get_param('post_id'); $lang = preg_replace('/[^a-z]/', '', (string) $req->get_param('lang')); $title = (string) $req->get_param('title'); $emoji = $vote === 'up' ? '👍' : '👎'; $post_id = wp_insert_post([ 'post_type' => FEA_FB_CPT, 'post_status' => 'private', 'post_title' => sprintf('%s %s', $emoji, $title ?: $url), 'post_content' => $comment, ], true); if (is_wp_error($post_id)) { return new WP_REST_Response(['ok' => false, 'error' => 'no se pudo guardar'], 500); } update_post_meta($post_id, '_fea_fb_vote', $vote); update_post_meta($post_id, '_fea_fb_url', $url); update_post_meta($post_id, '_fea_fb_source_id', $src_id); update_post_meta($post_id, '_fea_fb_lang', $lang); update_post_meta($post_id, '_fea_fb_ua', mb_substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 255)); update_post_meta($post_id, '_fea_fb_ip', md5($ip)); // hash, no IP en claro return new WP_REST_Response(['ok' => true], 200); } function fea_feedback_client_ip(): string { foreach (['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'] as $k) { if (!empty($_SERVER[$k])) return trim(explode(',', $_SERVER[$k])[0]); } return '0.0.0.0'; } /* ── 3) Columnas en el listado del wp-admin ───────────────────────────────── */ add_filter('manage_' . FEA_FB_CPT . '_posts_columns', function ($cols) { return [ 'cb' => $cols['cb'] ?? '', 'fb_vote' => 'Voto', 'fb_url' => 'Página', 'fb_lang' => 'Idioma', 'fb_comment'=> 'Comentario', 'date' => 'Fecha', ]; }); add_action('manage_' . FEA_FB_CPT . '_posts_custom_column', function ($col, $post_id) { if ($col === 'fb_vote') { echo get_post_meta($post_id, '_fea_fb_vote', true) === 'up' ? '👍' : '👎'; } elseif ($col === 'fb_url') { $u = get_post_meta($post_id, '_fea_fb_url', true); if ($u) echo '' . esc_html(wp_parse_url($u, PHP_URL_PATH) ?: $u) . ''; } elseif ($col === 'fb_lang') { echo esc_html(strtoupper(get_post_meta($post_id, '_fea_fb_lang', true) ?: '—')); } elseif ($col === 'fb_comment') { echo esc_html(wp_trim_words(get_post_field('post_content', $post_id), 24)); } }, 10, 2); /* ── 4) Barra Beta sutil (persistente) + tarjeta de feedback (a demanda) ──── */ /** Etiquetas del widget Beta por idioma (Polylang). */ function fea_beta_labels(): array { $all = [ 'es' => ['region'=>'Aviso Beta','intro'=>'Estamos en','help'=>'¿Nos ayudas a mejorar FeAdulta?', 'opinion'=>'Dar mi opinión','collab'=>'Colaborar','dismiss'=>'Cerrar aviso','fbregion'=>'Feedback de la página', 'close'=>'Cerrar','q'=>'¿Se ve bien esta página?','up'=>'Sí, se ve bien','down'=>'No, hay algo mal', 'ph'=>'¿Algo falla o se ve mal? Cuéntanoslo (opcional)','send'=>'Enviar','thanks'=>'¡Gracias por ayudar! 🙏'], 'en' => ['region'=>'Beta notice','intro'=>'We are in','help'=>'Will you help us improve FeAdulta?', 'opinion'=>'Give feedback','collab'=>'Collaborate','dismiss'=>'Close notice','fbregion'=>'Page feedback', 'close'=>'Close','q'=>'Does this page look right?','up'=>'Yes, looks good','down'=>'No, something is wrong', 'ph'=>'Something broken or off? Tell us (optional)','send'=>'Send','thanks'=>'Thanks for helping! 🙏'], 'fr' => ['region'=>'Avis Bêta','intro'=>'Nous sommes en','help'=>'Voulez-vous nous aider à améliorer FeAdulta ?', 'opinion'=>'Donner mon avis','collab'=>'Collaborer','dismiss'=>'Fermer l’avis','fbregion'=>'Retour sur la page', 'close'=>'Fermer','q'=>'Cette page s’affiche-t-elle bien ?','up'=>'Oui, c’est bien','down'=>'Non, il y a un problème', 'ph'=>'Un souci ou un affichage incorrect ? Dites-le-nous (facultatif)','send'=>'Envoyer','thanks'=>'Merci de votre aide ! 🙏'], 'it' => ['region'=>'Avviso Beta','intro'=>'Siamo in','help'=>'Ci aiuti a migliorare FeAdulta?', 'opinion'=>'Dai la tua opinione','collab'=>'Collabora','dismiss'=>'Chiudi avviso','fbregion'=>'Feedback della pagina', 'close'=>'Chiudi','q'=>'Questa pagina si vede bene?','up'=>'Sì, si vede bene','down'=>'No, c’è qualcosa che non va', 'ph'=>'Qualcosa non va o si vede male? Faccelo sapere (facoltativo)','send'=>'Invia','thanks'=>'Grazie per l’aiuto! 🙏'], 'pt' => ['region'=>'Aviso Beta','intro'=>'Estamos em','help'=>'Ajuda-nos a melhorar a FeAdulta?', 'opinion'=>'Dar a minha opinião','collab'=>'Colaborar','dismiss'=>'Fechar aviso','fbregion'=>'Feedback da página', 'close'=>'Fechar','q'=>'Esta página vê-se bem?','up'=>'Sim, vê-se bem','down'=>'Não, há algo errado', 'ph'=>'Algo falha ou vê-se mal? Conta-nos (opcional)','send'=>'Enviar','thanks'=>'Obrigado por ajudar! 🙏'], ]; $lang = function_exists('pll_current_language') ? (string) pll_current_language() : 'es'; return $all[$lang] ?? $all['es']; } add_action('wp_footer', function () { if (is_admin()) return; $rest = esc_url_raw(rest_url('fea/v1/feedback')); $t = fea_beta_labels(); ?>