321 lines
17 KiB
PHP
321 lines
17 KiB
PHP
<?php
|
|
/**
|
|
* Plugin Name: AutoBooking Security
|
|
* Description: Rate limiting, IP blocking, hCaptcha, email confirmation, driver documents.
|
|
* Version: 1.0.0
|
|
* Author: AutoBooking
|
|
*/
|
|
if ( ! defined( 'ABSPATH' ) ) exit;
|
|
|
|
/* ── ACTIVACIÓN: crea tablas ──────────────────────────────── */
|
|
register_activation_hook( __FILE__, 'abs_activate' );
|
|
function abs_activate() {
|
|
global $wpdb; $charset = $wpdb->get_charset_collate();
|
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
|
dbDelta( "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}ab_rate_limits (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
ip VARCHAR(45) NOT NULL DEFAULT '',
|
|
endpoint VARCHAR(80) NOT NULL DEFAULT '',
|
|
hits INT UNSIGNED NOT NULL DEFAULT 1,
|
|
window_start DATETIME NOT NULL,
|
|
PRIMARY KEY (id), KEY ip_ep_win (ip, endpoint, window_start)
|
|
) $charset;" );
|
|
dbDelta( "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}ab_blocked_ips (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
ip VARCHAR(45) NOT NULL DEFAULT '',
|
|
reason VARCHAR(120) NOT NULL DEFAULT '',
|
|
blocked_at DATETIME NOT NULL,
|
|
expires_at DATETIME NOT NULL,
|
|
PRIMARY KEY (id), UNIQUE KEY ip (ip), KEY ip_exp (ip, expires_at)
|
|
) $charset;" );
|
|
dbDelta( "CREATE TABLE IF NOT EXISTS {$wpdb->prefix}ab_driver_documents (
|
|
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|
driver_id BIGINT UNSIGNED NOT NULL,
|
|
doc_type ENUM('license','insurance','vehicle_photo') NOT NULL,
|
|
file_url VARCHAR(512) NOT NULL DEFAULT '',
|
|
expiry_date DATE NULL,
|
|
status ENUM('pending','approved','rejected') NOT NULL DEFAULT 'pending',
|
|
reviewed_by BIGINT UNSIGNED NULL,
|
|
reviewed_at DATETIME NULL,
|
|
created_at DATETIME NOT NULL,
|
|
PRIMARY KEY (id), KEY driver_doc (driver_id, doc_type), KEY status (status)
|
|
) $charset;" );
|
|
}
|
|
|
|
/* ── HELPERS ──────────────────────────────────────────────── */
|
|
function abs_get_ip() {
|
|
$ip = '';
|
|
if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
|
|
$parts = explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] );
|
|
$ip = trim( $parts[0] );
|
|
}
|
|
if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
|
|
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
|
|
}
|
|
return sanitize_text_field( $ip );
|
|
}
|
|
|
|
function abs_is_whitelisted( $ip ) {
|
|
$raw = get_option( 'ab_sec_whitelist', '' );
|
|
if ( ! $raw ) return false;
|
|
return in_array( $ip, array_map( 'trim', explode( "\n", $raw ) ), true );
|
|
}
|
|
|
|
function abs_is_blocked( $ip ) {
|
|
global $wpdb;
|
|
return (bool) $wpdb->get_var( $wpdb->prepare(
|
|
"SELECT id FROM {$wpdb->prefix}ab_blocked_ips WHERE ip = %s AND expires_at > %s",
|
|
$ip, current_time( 'mysql' )
|
|
) );
|
|
}
|
|
|
|
function abs_block_ip( $ip, $reason, $hours ) {
|
|
global $wpdb;
|
|
$wpdb->query( $wpdb->prepare(
|
|
"INSERT INTO {$wpdb->prefix}ab_blocked_ips (ip, reason, blocked_at, expires_at) VALUES (%s,%s,%s,%s)
|
|
ON DUPLICATE KEY UPDATE reason=VALUES(reason), blocked_at=VALUES(blocked_at), expires_at=VALUES(expires_at)",
|
|
$ip, $reason, current_time('mysql'), date('Y-m-d H:i:s', time() + $hours * 3600)
|
|
) );
|
|
}
|
|
|
|
function abs_check_rate( $ip, $endpoint, $limit, $window_sec, $block_hours = 0 ) {
|
|
if ( ! $ip || abs_is_whitelisted( $ip ) ) return true;
|
|
if ( abs_is_blocked( $ip ) ) return false;
|
|
global $wpdb; $table = $wpdb->prefix . 'ab_rate_limits';
|
|
$window = date( 'Y-m-d H:i:s', time() - $window_sec );
|
|
$hits = (int) $wpdb->get_var( $wpdb->prepare(
|
|
"SELECT SUM(hits) FROM $table WHERE ip=%s AND endpoint=%s AND window_start>=%s",
|
|
$ip, $endpoint, $window
|
|
) );
|
|
if ( $hits >= $limit ) {
|
|
if ( $block_hours > 0 ) abs_block_ip( $ip, "Rate limit: $endpoint", $block_hours );
|
|
return false;
|
|
}
|
|
$minute = date( 'Y-m-d H:i:00' );
|
|
$exists = $wpdb->get_var( $wpdb->prepare(
|
|
"SELECT id FROM $table WHERE ip=%s AND endpoint=%s AND window_start=%s",
|
|
$ip, $endpoint, $minute
|
|
) );
|
|
if ( $exists ) {
|
|
$wpdb->query( $wpdb->prepare(
|
|
"UPDATE $table SET hits=hits+1 WHERE ip=%s AND endpoint=%s AND window_start=%s",
|
|
$ip, $endpoint, $minute
|
|
) );
|
|
} else {
|
|
$wpdb->insert( $table, ['ip'=>$ip,'endpoint'=>$endpoint,'hits'=>1,'window_start'=>$minute] );
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* ── RATE LIMITING: login ─────────────────────────────────── */
|
|
add_action( 'wp_login_failed', function( $u ) {
|
|
abs_check_rate( abs_get_ip(), 'login', 10, 900, 2 );
|
|
} );
|
|
add_action( 'login_init', function() {
|
|
if ( abs_is_blocked( abs_get_ip() ) )
|
|
wp_die( 'Tu IP fue bloqueada temporalmente por demasiados intentos fallidos.', 'Bloqueado', ['response'=>429] );
|
|
} );
|
|
|
|
/* ── RATE LIMITING: registro ──────────────────────────────── */
|
|
add_filter( 'registration_errors', function( $errors, $login, $email ) {
|
|
if ( ! abs_check_rate( abs_get_ip(), 'register', 5, 3600, 24 ) )
|
|
$errors->add( 'rate_limit', '<strong>Error:</strong> Demasiados intentos desde tu IP. Intenta más tarde.' );
|
|
return $errors;
|
|
}, 10, 3 );
|
|
|
|
/* ── RATE LIMITING: REST ──────────────────────────────────── */
|
|
add_filter( 'rest_dispatch_request', function( $result, $request, $route, $handler ) {
|
|
if ( strpos( $route, '/autobooking/v1/' ) === false ) return $result;
|
|
if ( $request->get_method() === 'GET' ) return $result;
|
|
if ( ! abs_check_rate( abs_get_ip(), 'rest_post', 120, 60, 0 ) )
|
|
return new WP_Error( 'rate_limited', 'Demasiadas solicitudes.', ['status'=>429] );
|
|
return $result;
|
|
}, 10, 4 );
|
|
|
|
/* ── LIMPIEZA DIARIA ──────────────────────────────────────── */
|
|
add_action( 'abs_daily_cleanup', function() {
|
|
global $wpdb;
|
|
$wpdb->query("DELETE FROM {$wpdb->prefix}ab_rate_limits WHERE window_start < DATE_SUB(NOW(), INTERVAL 2 DAY)");
|
|
$wpdb->query("DELETE FROM {$wpdb->prefix}ab_blocked_ips WHERE expires_at < NOW()");
|
|
} );
|
|
if ( ! wp_next_scheduled('abs_daily_cleanup') ) wp_schedule_event( time(), 'daily', 'abs_daily_cleanup' );
|
|
|
|
/* ── HCAPTCHA ─────────────────────────────────────────────── */
|
|
add_action( 'register_form', function() {
|
|
$key = get_option('ab_hcaptcha_site_key','');
|
|
if ( ! $key ) return;
|
|
echo '<div class="h-captcha" data-sitekey="'.esc_attr($key).'" style="margin:12px 0;"></div>';
|
|
echo '<script src="https://js.hcaptcha.com/1/api.js" async defer></script>';
|
|
} );
|
|
add_filter( 'registration_errors', function( $errors, $login, $email ) {
|
|
$secret = get_option('ab_hcaptcha_secret','');
|
|
if ( ! $secret ) return $errors;
|
|
$token = sanitize_text_field( $_POST['h-captcha-response'] ?? '' );
|
|
if ( ! $token ) { $errors->add('captcha_missing','<strong>Error:</strong> Completa el captcha.'); return $errors; }
|
|
$res = wp_remote_post('https://api.hcaptcha.com/siteverify', [
|
|
'body' => ['secret'=>$secret,'response'=>$token,'remoteip'=>abs_get_ip()],
|
|
'timeout' => 10,
|
|
]);
|
|
if ( ! is_wp_error($res) ) {
|
|
$body = json_decode(wp_remote_retrieve_body($res), true);
|
|
if ( empty($body['success']) ) $errors->add('captcha_invalid','<strong>Error:</strong> Captcha inválido.');
|
|
}
|
|
return $errors;
|
|
}, 20, 3 );
|
|
|
|
/* ── EMAIL CONFIRMATION ───────────────────────────────────── */
|
|
add_action( 'user_register', function( $uid ) {
|
|
$user = get_userdata($uid);
|
|
if ( ! $user || ! in_array('driver_pending',(array)$user->roles,true) ) return;
|
|
$token = bin2hex(random_bytes(32));
|
|
update_user_meta($uid,'ab_email_token',$token);
|
|
update_user_meta($uid,'ab_email_token_expires',date('Y-m-d H:i:s',time()+86400));
|
|
update_user_meta($uid,'ab_email_confirmed',0);
|
|
$url = add_query_arg('token',$token,get_rest_url(null,'autobooking/v1/confirm-email'));
|
|
wp_mail($user->user_email,'Confirma tu email — AutoBooking',
|
|
"Hola {$user->display_name},\n\nConfirma tu email (válido 24h):\n\n$url\n\nAutoBooking");
|
|
} );
|
|
|
|
add_action( 'template_redirect', function() {
|
|
if ( ! is_page('driver-dashboard') && ! is_page('pending-driver') ) return;
|
|
if ( ! is_user_logged_in() ) return;
|
|
$user = wp_get_current_user(); $roles = (array)$user->roles;
|
|
if ( ! in_array('driver_pending',$roles,true) && ! in_array('driver',$roles,true) ) return;
|
|
if ( (int)get_user_meta($user->ID,'ab_email_confirmed',true) ) return;
|
|
$resend = esc_url(get_rest_url(null,'autobooking/v1/resend-confirmation'));
|
|
wp_die(
|
|
'<div style="font-family:system-ui;max-width:500px;margin:60px auto;text-align:center;padding:32px;background:#111;color:#fff;border-radius:16px;">'
|
|
.'<p style="font-size:20px;font-weight:700;color:#FF6F00;">Confirma tu email</p>'
|
|
.'<p style="color:rgba(255,255,255,.7);">Revisa tu bandeja de entrada y haz clic en el enlace de confirmación.</p>'
|
|
.'<form method="post" action="'.$resend.'">'.wp_nonce_field('wp_rest','_wpnonce',true,false)
|
|
.'<button type="submit" style="margin-top:16px;background:#FF6F00;color:#fff;border:none;padding:12px 28px;border-radius:10px;font-weight:700;cursor:pointer;">Reenviar email</button>'
|
|
.'</form></div>',
|
|
'Confirma tu email', ['response'=>403]
|
|
);
|
|
} );
|
|
|
|
/* ── REST ENDPOINTS ───────────────────────────────────────── */
|
|
add_action( 'rest_api_init', function() {
|
|
|
|
register_rest_route('autobooking/v1','/confirm-email',[
|
|
'methods'=>'GET','permission_callback'=>'__return_true',
|
|
'callback'=>function(WP_REST_Request $req){
|
|
global $wpdb;
|
|
$token = sanitize_text_field($req->get_param('token'));
|
|
if(!$token){wp_redirect(home_url('/?ab_confirm=invalid'));exit;}
|
|
$uid = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key='ab_email_token' AND meta_value=%s LIMIT 1",$token));
|
|
if(!$uid){wp_redirect(home_url('/?ab_confirm=invalid'));exit;}
|
|
if(strtotime(get_user_meta((int)$uid,'ab_email_token_expires',true))<time()){
|
|
wp_redirect(home_url('/?ab_confirm=expired'));exit;}
|
|
update_user_meta((int)$uid,'ab_email_confirmed',1);
|
|
delete_user_meta((int)$uid,'ab_email_token');
|
|
delete_user_meta((int)$uid,'ab_email_token_expires');
|
|
wp_redirect(home_url('/pending-driver/?ab_confirm=ok'));exit;
|
|
},
|
|
]);
|
|
|
|
register_rest_route('autobooking/v1','/resend-confirmation',[
|
|
'methods'=>'POST','permission_callback'=>'is_user_logged_in',
|
|
'callback'=>function(){
|
|
$user=wp_get_current_user(); $token=bin2hex(random_bytes(32));
|
|
update_user_meta($user->ID,'ab_email_token',$token);
|
|
update_user_meta($user->ID,'ab_email_token_expires',date('Y-m-d H:i:s',time()+86400));
|
|
$url=add_query_arg('token',$token,get_rest_url(null,'autobooking/v1/confirm-email'));
|
|
wp_mail($user->user_email,'Confirma tu email — AutoBooking',"Nuevo enlace (válido 24h):\n\n$url");
|
|
wp_redirect(home_url('/pending-driver/?ab_confirm=resent'));exit;
|
|
},
|
|
]);
|
|
|
|
register_rest_route('autobooking/v1','/driver/upload-doc',[
|
|
'methods'=>'POST','permission_callback'=>'is_user_logged_in',
|
|
'callback'=>function(WP_REST_Request $req){
|
|
global $wpdb; $user=wp_get_current_user();
|
|
$doc_type=sanitize_text_field($req->get_param('doc_type'));
|
|
if(!in_array($doc_type,['license','insurance','vehicle_photo'],true))
|
|
return new WP_Error('invalid_type','Tipo inválido.',['status'=>400]);
|
|
if(empty($_FILES['file'])) return new WP_Error('no_file','Sin archivo.',['status'=>400]);
|
|
$file=$_FILES['file'];
|
|
if(!in_array($file['type'],['image/jpeg','image/png','application/pdf'],true))
|
|
return new WP_Error('invalid_mime','Solo JPG, PNG o PDF.',['status'=>400]);
|
|
if($file['size']>5*1024*1024) return new WP_Error('file_too_large','Máx 5 MB.',['status'=>400]);
|
|
require_once ABSPATH.'wp-admin/includes/file.php';
|
|
$up=wp_upload_dir(); $dir=$up['basedir'].'/ab-driver-docs/'.$user->ID;
|
|
wp_mkdir_p($dir);
|
|
$ext=pathinfo($file['name'],PATHINFO_EXTENSION);
|
|
$fname=$doc_type.'_'.time().'.'.$ext;
|
|
if(!move_uploaded_file($file['tmp_name'],$dir.'/'.$fname))
|
|
return new WP_Error('upload_failed','Error al guardar.',['status'=>500]);
|
|
$url=$up['baseurl'].'/ab-driver-docs/'.$user->ID.'/'.$fname;
|
|
$data=['driver_id'=>$user->ID,'doc_type'=>$doc_type,'file_url'=>$url,'status'=>'pending','created_at'=>current_time('mysql')];
|
|
$exists=$wpdb->get_var($wpdb->prepare(
|
|
"SELECT id FROM {$wpdb->prefix}ab_driver_documents WHERE driver_id=%d AND doc_type=%s",$user->ID,$doc_type));
|
|
if($exists) $wpdb->update("{$wpdb->prefix}ab_driver_documents",$data,['driver_id'=>$user->ID,'doc_type'=>$doc_type]);
|
|
else $wpdb->insert("{$wpdb->prefix}ab_driver_documents",$data);
|
|
return rest_ensure_response(['ok'=>true,'file_url'=>$url]);
|
|
},
|
|
]);
|
|
|
|
register_rest_route('autobooking/v1','/driver/my-docs',[
|
|
'methods'=>'GET','permission_callback'=>'is_user_logged_in',
|
|
'callback'=>function(){
|
|
global $wpdb; $uid=get_current_user_id();
|
|
return rest_ensure_response(['docs'=>$wpdb->get_results($wpdb->prepare(
|
|
"SELECT doc_type,file_url,status,expiry_date FROM {$wpdb->prefix}ab_driver_documents WHERE driver_id=%d",$uid),ARRAY_A)?:[]]);
|
|
},
|
|
]);
|
|
|
|
register_rest_route('autobooking/v1','/admin/driver/(?P<id>\d+)/docs',[
|
|
'methods'=>'GET','permission_callback'=>function(){return current_user_can('manage_autobooking');},
|
|
'callback'=>function(WP_REST_Request $req){
|
|
global $wpdb; $id=absint($req['id']);
|
|
return rest_ensure_response(['docs'=>$wpdb->get_results($wpdb->prepare(
|
|
"SELECT * FROM {$wpdb->prefix}ab_driver_documents WHERE driver_id=%d",$id),ARRAY_A)?:[]]);
|
|
},
|
|
]);
|
|
|
|
register_rest_route('autobooking/v1','/admin/doc/(?P<id>\d+)/review',[
|
|
'methods'=>'POST','permission_callback'=>function(){return current_user_can('manage_autobooking');},
|
|
'callback'=>function(WP_REST_Request $req){
|
|
global $wpdb; $id=absint($req['id']);
|
|
$status=in_array($req->get_param('status'),['approved','rejected'],true)?$req->get_param('status'):'pending';
|
|
$wpdb->update("{$wpdb->prefix}ab_driver_documents",
|
|
['status'=>$status,'reviewed_by'=>get_current_user_id(),'reviewed_at'=>current_time('mysql')],['id'=>$id]);
|
|
return rest_ensure_response(['ok'=>true]);
|
|
},
|
|
]);
|
|
|
|
register_rest_route('autobooking/v1','/admin/blocked-ips',[
|
|
'methods'=>'GET','permission_callback'=>function(){return current_user_can('manage_autobooking');},
|
|
'callback'=>function(){
|
|
global $wpdb;
|
|
return rest_ensure_response([
|
|
'blocked'=>$wpdb->get_results("SELECT id,ip,reason,blocked_at,expires_at FROM {$wpdb->prefix}ab_blocked_ips WHERE expires_at>NOW() ORDER BY blocked_at DESC LIMIT 100",ARRAY_A)?:[],
|
|
'blocked_24h'=>(int)$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}ab_blocked_ips WHERE blocked_at>=DATE_SUB(NOW(),INTERVAL 24 HOUR)"),
|
|
]);
|
|
},
|
|
]);
|
|
|
|
register_rest_route('autobooking/v1','/admin/blocked-ips/(?P<id>\d+)/unblock',[
|
|
'methods'=>'POST','permission_callback'=>function(){return current_user_can('manage_autobooking');},
|
|
'callback'=>function(WP_REST_Request $req){
|
|
global $wpdb; $wpdb->delete("{$wpdb->prefix}ab_blocked_ips",['id'=>absint($req['id'])]);
|
|
return rest_ensure_response(['ok'=>true]);
|
|
},
|
|
]);
|
|
|
|
register_rest_route('autobooking/v1','/admin/security-settings/save',[
|
|
'methods'=>'POST','permission_callback'=>function(){return current_user_can('manage_autobooking');},
|
|
'callback'=>function(WP_REST_Request $req){
|
|
$wl = $req->get_param('whitelist');
|
|
$sk = $req->get_param('hcaptcha_site_key');
|
|
$sec = $req->get_param('hcaptcha_secret');
|
|
if($wl !== null) update_option('ab_sec_whitelist', sanitize_textarea_field($wl));
|
|
if($sk !== null) update_option('ab_hcaptcha_site_key', sanitize_text_field($sk));
|
|
if($sec !== null) update_option('ab_hcaptcha_secret', sanitize_text_field($sec));
|
|
return rest_ensure_response(['ok'=>true,'saved'=>array_filter(['whitelist'=>$wl,'sk'=>$sk,'sec'=>$sec])]);
|
|
},
|
|
]);
|
|
|
|
} );
|