Añadir mu-plugins y scripts de feadulta

This commit is contained in:
2026-06-28 15:10:46 -04:00
parent bce7e42f44
commit b6116b066d
106 changed files with 17600 additions and 2 deletions
+291
View File
@@ -0,0 +1,291 @@
<?php
/**
* Plugin Name: Fe Adulta — Feedback Beta
* Description: Barra sutil de aviso "Beta" en todo el sitio + mini formulario (👍/👎 +
* comentario opcional) que se abre a demanda, para que el público ayude a
* encontrar errores. Guarda cada voto como "Beta Feedback" (CPT propio),
* legible en wp-admin en una sola lista. No usa el sistema de comentarios.
* Version: 1.1
*
* Ver issue rafa/feadulta#78.
*/
if (!defined('ABSPATH')) exit;
const FEA_FB_CPT = 'fea_feedback';
const FEA_FB_RATE_MAX = 12; // máximo de envíos por IP por hora
const FEA_FB_COMMENT_MAX = 2000;
/* ── 1) CPT donde se guardan los votos (solo backend) ─────────────────────── */
add_action('init', function () {
register_post_type(FEA_FB_CPT, [
'labels' => [
'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<id>\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 '<a href="' . esc_url($u) . '" target="_blank" rel="noopener">' . esc_html(wp_parse_url($u, PHP_URL_PATH) ?: $u) . '</a>';
} 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 lavis','fbregion'=>'Retour sur la page',
'close'=>'Fermer','q'=>'Cette page saffiche-t-elle bien ?','up'=>'Oui, cest 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 laiuto! 🙏'],
'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();
?>
<style>
/* Barra sutil de aviso Beta, abajo, full-width */
#fea-beta-bar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 99997;
background: #faf6f2; border-top: 1px solid #e6ddd5; color: #4a3b34;
font-family: inherit; font-size: .86rem; line-height: 1.3;
padding: 8px 44px 8px 16px; text-align: center; }
#fea-beta-bar strong { color: #8b1a2e; }
#fea-beta-bar .fea-beta-open { margin-left: 10px; cursor: pointer; border: 1px solid #8b1a2e;
background: #8b1a2e; color: #fff; border-radius: 6px; padding: 4px 12px; font-size: .82rem; font-weight: 600; }
#fea-beta-bar .fea-beta-open:hover { background: #761526; }
#fea-beta-bar .fea-beta-collab { margin-left: 8px; cursor: pointer; display: inline-block;
border: 1px solid #1b7a34; background: #1b7a34; color: #fff; border-radius: 6px;
padding: 4px 12px; font-size: .82rem; font-weight: 600; text-decoration: none; }
#fea-beta-bar .fea-beta-collab:hover { background: #15642a; }
#fea-beta-bar .fea-beta-dismiss { position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
border: 0; background: none; font-size: 1.15rem; cursor: pointer; color: #8a7a72; padding: 2px 6px; line-height: 1; }
#fea-beta-bar.hidden { display: none; }
/* Tarjeta de feedback: oculta hasta que el usuario la abre desde la barra */
#fea-fb { position: fixed; right: 16px; bottom: 56px; z-index: 99998; font-family: inherit; max-width: 300px; }
#fea-fb[hidden] { display: none; }
#fea-fb .fea-fb-card { background:#fff; border:1px solid #e2e2e2; border-radius:12px;
box-shadow:0 8px 28px rgba(0,0,0,.16); padding:12px 14px; font-size:.9rem; color:#222; position:relative; }
#fea-fb .fea-fb-q { margin:0 0 8px; line-height:1.3; padding-right:16px; }
#fea-fb .fea-fb-btns { display:flex; gap:8px; }
#fea-fb button.fea-fb-vote { cursor:pointer; border:1px solid #ccc; background:#fafafa; border-radius:8px;
padding:6px 12px; font-size:1.05rem; line-height:1; }
#fea-fb button.fea-fb-vote:hover { background:#f0f0f0; }
#fea-fb button.fea-fb-vote.sel { border-color:#8b1a2e; background:#f7e9ec; }
#fea-fb textarea { width:100%; margin:9px 0 8px; border:1px solid #ccc; border-radius:8px; padding:7px;
font:inherit; font-size:.85rem; resize:vertical; min-height:58px; box-sizing:border-box; }
#fea-fb .fea-fb-send { background:#8b1a2e; color:#fff; border:1px solid #8b1a2e; border-radius:8px;
padding:6px 12px; font-size:.85rem; width:100%; cursor:pointer; }
#fea-fb .fea-fb-hp { position:absolute; left:-9999px; }
#fea-fb .fea-fb-close { position:absolute; top:4px; right:8px; border:0; background:none; font-size:1rem; cursor:pointer; padding:2px 4px; line-height:1; }
@media (max-width:600px){ #fea-fb{ right:10px; left:10px; max-width:none; } #fea-beta-bar{ font-size:.8rem; } }
</style>
<div id="fea-beta-bar" class="hidden" role="region" aria-label="<?php echo esc_attr($t['region']); ?>">
🌱 <?php echo esc_html($t['intro']); ?> <strong>Beta</strong>. <?php echo esc_html($t['help']); ?>
<button type="button" class="fea-beta-open"><?php echo esc_html($t['opinion']); ?></button>
<a class="fea-beta-collab" href="https://edicionesfeadulta.com/colabora/" target="_blank" rel="noopener"><?php echo esc_html($t['collab']); ?></a>
<button type="button" class="fea-beta-dismiss" aria-label="<?php echo esc_attr($t['dismiss']); ?>">×</button>
</div>
<div id="fea-fb" hidden role="complementary" aria-label="<?php echo esc_attr($t['fbregion']); ?>">
<div class="fea-fb-card">
<button type="button" class="fea-fb-close" aria-label="<?php echo esc_attr($t['close']); ?>">×</button>
<p class="fea-fb-q"><?php echo esc_html($t['q']); ?></p>
<div class="fea-fb-btns">
<button type="button" class="fea-fb-vote" data-vote="up" aria-label="<?php echo esc_attr($t['up']); ?>">👍</button>
<button type="button" class="fea-fb-vote" data-vote="down" aria-label="<?php echo esc_attr($t['down']); ?>">👎</button>
</div>
<div class="fea-fb-more" hidden>
<input type="text" class="fea-fb-hp" tabindex="-1" autocomplete="off" aria-hidden="true" placeholder="No rellenar">
<textarea placeholder="<?php echo esc_attr($t['ph']); ?>"></textarea>
<button type="button" class="fea-fb-send"><?php echo esc_html($t['send']); ?></button>
</div>
<div class="fea-fb-thanks" hidden><?php echo esc_html($t['thanks']); ?></div>
</div>
</div>
<script>
(function(){
var bar = document.getElementById('fea-beta-bar');
var box = document.getElementById('fea-fb');
if(!bar || !box) return;
var REST = <?php echo json_encode($rest); ?>;
var pid = <?php echo (int) (is_singular() ? get_queried_object_id() : 0); ?>;
var lang = <?php echo json_encode(function_exists('pll_current_language') ? (string) pll_current_language() : ''); ?>;
var chosen = null;
var moreEl = box.querySelector('.fea-fb-more');
var votes = box.querySelectorAll('.fea-fb-vote');
var thanks = box.querySelector('.fea-fb-thanks');
// Mostrar la barra salvo que el usuario la haya descartado antes.
try { if (!localStorage.getItem('fea_beta_bar_off')) bar.classList.remove('hidden'); }
catch(e){ bar.classList.remove('hidden'); }
function openCard(){ box.hidden = false; }
function closeCard(){ box.hidden = true; }
bar.querySelector('.fea-beta-open').addEventListener('click', openCard);
bar.querySelector('.fea-beta-dismiss').addEventListener('click', function(){
bar.classList.add('hidden');
try { localStorage.setItem('fea_beta_bar_off','1'); } catch(e){}
});
box.querySelector('.fea-fb-close').addEventListener('click', closeCard);
votes.forEach(function(b){ b.addEventListener('click', function(){
chosen = b.getAttribute('data-vote');
votes.forEach(function(x){ x.classList.toggle('sel', x===b); });
moreEl.hidden = false;
});});
box.querySelector('.fea-fb-send').addEventListener('click', function(){
if(!chosen) return;
var hp = box.querySelector('.fea-fb-hp').value;
var comment = box.querySelector('textarea').value;
fetch(REST, { method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ vote:chosen, comment:comment, url:location.href, post_id:pid,
lang:lang, title:document.title, website:hp }) }).catch(function(){});
box.querySelector('.fea-fb-btns').hidden = true;
box.querySelector('.fea-fb-q').hidden = true;
moreEl.hidden = true; thanks.hidden = false;
setTimeout(closeCard, 2200);
});
})();
</script>
<?php
}, 40);