, avatar= * * Ver issue gitea.feadulta.com/rafa/feadulta#175. */ if (!defined('ABSPATH')) exit; const FEA_AVATAR_MAX_BYTES = 5242880; // 5 MiB. const FEA_AVATAR_SIZE = 512; const FEA_AVATAR_MIN_SIZE = 512; const FEA_AVATAR_MAX_DIMENSION = 4096; const FEA_AVATAR_MAX_PIXELS = 16777216; // 16 megapíxeles. add_action('rest_api_init', function () { register_rest_route('fea/v1', '/subir-avatar', [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => 'fea_subir_avatar_handle', 'permission_callback' => 'fea_subir_avatar_can_call', // Se valida dentro del handler, después del permission_callback: una // llamada sin autenticar siempre recibe 401, incluso si omite user_id. 'args' => [ 'user_id' => [ 'sanitize_callback' => 'absint', ], ], ]); }); /** Editor o superior: mismo nivel que el endpoint /crear-autor. */ function fea_subir_avatar_can_call(WP_REST_Request $request) { if (!is_user_logged_in()) { return new WP_Error( 'fea_subir_avatar_not_authenticated', 'Debes autenticarte para asignar un avatar.', ['status' => 401] ); } if (!current_user_can('edit_others_posts')) { return new WP_Error( 'fea_subir_avatar_forbidden', 'No tienes permiso para asignar avatares.', ['status' => 403] ); } return true; } function fea_subir_avatar_handle(WP_REST_Request $request) { $user_id = absint($request->get_param('user_id')); if (!$user_id) { return new WP_Error( 'fea_subir_avatar_invalid_user', 'user_id es obligatorio.', ['status' => 400] ); } $user = get_userdata($user_id); if (!$user) { return new WP_Error( 'fea_subir_avatar_user_not_found', 'No existe el usuario indicado.', ['status' => 404] ); } $files = $request->get_file_params(); $file = $files['avatar'] ?? null; $validation = fea_subir_avatar_validate_file($file); if (is_wp_error($validation)) return $validation; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; $uploaded = wp_handle_upload($file, [ 'test_form' => false, 'mimes' => [ 'jpg|jpeg|jpe' => 'image/jpeg', 'png' => 'image/png', 'webp' => 'image/webp', ], ]); if (!empty($uploaded['error'])) { return new WP_Error( 'fea_subir_avatar_upload_failed', 'No se pudo guardar la imagen: ' . $uploaded['error'], ['status' => 400] ); } $normalized = fea_subir_avatar_normalize($uploaded['file']); if (is_wp_error($normalized)) { wp_delete_file($uploaded['file']); return $normalized; } $attachment_id = wp_insert_attachment([ 'post_mime_type' => $normalized['mime-type'], 'post_title' => 'Avatar — ' . $user->display_name, 'post_status' => 'inherit', ], $normalized['path'], 0, true); if (is_wp_error($attachment_id)) { wp_delete_file($normalized['path']); return new WP_Error( 'fea_subir_avatar_attachment_failed', 'No se pudo crear el attachment del avatar.', ['status' => 500] ); } $metadata = wp_generate_attachment_metadata($attachment_id, $normalized['path']); if (is_wp_error($metadata) || !is_array($metadata) || !$metadata) { wp_delete_attachment($attachment_id, true); return new WP_Error( 'fea_subir_avatar_metadata_failed', 'No se pudo generar la metadata del avatar.', ['status' => 500] ); } wp_update_attachment_metadata($attachment_id, $metadata); if (!wp_get_attachment_metadata($attachment_id)) { wp_delete_attachment($attachment_id, true); return new WP_Error( 'fea_subir_avatar_metadata_failed', 'No se pudo guardar la metadata del avatar.', ['status' => 500] ); } $previous_attachment_id = (int) get_user_meta($user_id, 'foto_perfil', true); if (!update_user_meta($user_id, 'foto_perfil', (string) $attachment_id)) { wp_delete_attachment($attachment_id, true); return new WP_Error( 'fea_subir_avatar_assignment_failed', 'No se pudo asignar el avatar al usuario.', ['status' => 500] ); } $size = wp_getimagesize($normalized['path']); return new WP_REST_Response([ 'user_id' => $user_id, 'attachment_id' => (int) $attachment_id, 'previous_attachment_id' => $previous_attachment_id ?: null, 'avatar_url' => wp_get_attachment_image_url($attachment_id, 'full'), 'width' => (int) ($size[0] ?? 0), 'height' => (int) ($size[1] ?? 0), ], 201); } function fea_subir_avatar_validate_file($file) { if (!is_array($file) || empty($file['tmp_name']) || !isset($file['error'])) { return new WP_Error( 'fea_subir_avatar_missing_file', 'avatar es obligatorio.', ['status' => 400] ); } if ((int) $file['error'] !== UPLOAD_ERR_OK) { return new WP_Error( 'fea_subir_avatar_file_error', 'La subida de avatar falló.', ['status' => 400] ); } if ((int) $file['size'] > FEA_AVATAR_MAX_BYTES) { return new WP_Error( 'fea_subir_avatar_file_too_large', 'La imagen no puede superar 5 MB.', ['status' => 400] ); } $image = wp_getimagesize($file['tmp_name']); if (!$image) { return new WP_Error( 'fea_subir_avatar_invalid_image', 'avatar debe ser una imagen válida.', ['status' => 400] ); } $width = (int) $image[0]; $height = (int) $image[1]; if ($width < FEA_AVATAR_MIN_SIZE || $height < FEA_AVATAR_MIN_SIZE) { return new WP_Error( 'fea_subir_avatar_invalid_image', 'avatar debe ser una imagen de al menos 512×512 píxeles.', ['status' => 400] ); } if ($width > FEA_AVATAR_MAX_DIMENSION || $height > FEA_AVATAR_MAX_DIMENSION || ($width * $height) > FEA_AVATAR_MAX_PIXELS) { return new WP_Error( 'fea_subir_avatar_image_too_large', 'avatar no puede superar 4096 píxeles por lado ni 16 megapíxeles.', ['status' => 400] ); } $allowed_mimes = ['image/jpeg', 'image/png', 'image/webp']; if (!in_array($image['mime'], $allowed_mimes, true)) { return new WP_Error( 'fea_subir_avatar_unsupported_type', 'Solo se admiten imágenes JPEG, PNG o WebP.', ['status' => 400] ); } return true; } /** Recorta al centro y normaliza la imagen al tamaño estándar del avatar. */ function fea_subir_avatar_normalize(string $path) { $editor = wp_get_image_editor($path); if (is_wp_error($editor)) { return new WP_Error( 'fea_subir_avatar_editor_unavailable', 'No se pudo procesar la imagen subida.', ['status' => 400] ); } $resized = $editor->resize(FEA_AVATAR_SIZE, FEA_AVATAR_SIZE, true); if (is_wp_error($resized)) { return new WP_Error( 'fea_subir_avatar_resize_failed', 'No se pudo normalizar el avatar.', ['status' => 400] ); } $saved = $editor->save($path); if (is_wp_error($saved) || empty($saved['path'])) { return new WP_Error( 'fea_subir_avatar_save_failed', 'No se pudo guardar el avatar normalizado.', ['status' => 500] ); } return $saved; }