292 lines
16 KiB
PHP
292 lines
16 KiB
PHP
<?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 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();
|
||
?>
|
||
<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);
|