aicc_conf_apikpixabay(), 'q' => rawurlencode( $keywords ), 'lang' => substr( $idioma, 0, 2 ), 'orientation' => 'horizontal', 'min_width' => '1024', 'min_height' => '512', 'safesearch' => 'true', 'order' => 'popular', 'pretty' => 'false', // Temporalmente true para desarrollo. ); $pixabay_url = add_query_arg( $query_args, 'https://pixabay.com/api/' ); return $pixabay_url; } /** Reformatea las keywords de la maetaetiqueta a algo buscable separado por símbolos "|". * * @param string $metakeywords Lista obtenida de las meta Keywords, que es separada por comas. * @return string con la cadena buscable. */ function aicc_forma_keywords_busqueda_pixabay( $metakeywords ) { // Elimina los espacios en blanco al principio y al final del string. $metakeywords = trim( $metakeywords ); // Divide el string en un array de palabras clave utilizando comas como separador. $keywords_array = explode( ',', $metakeywords ); // Itera sobre el array de palabras clave y elimina los espacios sobrantes. $keywords_array = array_map( 'trim', $keywords_array ); // Une las palabras clave con el símbolo "|" como concatenador. $formatted_keywords = implode( '|', $keywords_array ); return $formatted_keywords; } /** Gestiona la búsqueda de imagen para un artículo y claves dadas. * Requerirá también $consulta, $idioma para localizar en la caché la imagen * * @param string $id Artículo. * @param string $keywords Búsqueda. */ function aicc_seleccionar_imagen( $id, $keywords ) { $idioma = aicc_conf_idioma(); $cantidad = aicc_conf_pixabay_cantidad(); echo '

' . esc_html( __( 'Featured image selection', 'ai-content-creator' ) ) . "

\n"; echo '

'; esc_html_e( 'Use the search engine to view images and select one or select', 'ai-content-creator' ); echo ' '; esc_html_e( 'Without image', 'ai-content-creator' ); echo ''; echo ".

\n"; echo '
' . "\n"; echo ' ' . "\n"; echo ' ' . "\n"; echo ' ' . "\n"; // Si no se han recibido keywords, se toman por el objeto. if ( ! $keywords ) { $aicc_generacion = new AIccGeneracion(); if ( $aicc_generacion->cargar( $id ) ) { $keywords = $aicc_generacion->meta_keywords(); if ( ! $keywords ) { $keywords = aicc_improvisa_meta_keywords( $aicc_generacion->titulo(), $aicc_generacion->idioma() ); } } else { aicc_mostrar_notificacion( __( 'Unable to load the specified article.', 'ai-content-creator' ), 'error' ); } } echo '
' . "\n"; echo ' ' . "\n"; echo "
\n"; $aicc_generacion = new AIccGeneracion(); echo wp_kses_post( $aicc_generacion->boton_formulario_operacion_html( __( 'Search for images', 'ai-content-creator' ), 'buscar', 'dashicons-format-image' ) ); echo "
\n"; echo "
\n"; echo "
\n"; echo "
\n"; // Muestra la selección de imágenes. if ( $keywords ) { aicc_mostrar_miniaturas_pixabay( $id, $keywords, $idioma, $cantidad ); } } /** Muestra las miniaturas de imágenes de Pixabay. * * @param string $id Artículo. * @param string $consulta Texto de la consulta enviada. * @param string $idioma Idioma en que hacer la consulta. * @param integer $cantidad Máximo de imágenes obtenidas por cada consulta. * @return array con las respuestas de Pixabay o un error. */ function aicc_mostrar_miniaturas_pixabay( $id, $consulta, $idioma, $cantidad = 25 ) { // Obtiene los resultados de la búsqueda en Pixabay. $resultados = aicc_busqueda_pixabay( $consulta, $idioma ); // Si hay un error, muestra un mensaje y termina la función. if ( isset( $resultados['error'] ) ) { echo '

' . esc_html( $resultados['message'] ) . '

'; return; } // Muestra las miniaturas de las imágenes. echo '
' . "\n"; // Agrega un elemento "Sin imagen" al comienzo de la lista. aicc_mostrar_miniatura_pixabay( $id ); $contador = 0; // La cantidad de imágenes máxima se puede obtener de count( $resultados['hits'] ). foreach ( $resultados['hits'] as $imagen ) { // Si se alcanza la cantidad predeterminada de miniaturas, detiene el bucle. if ( $contador >= $cantidad ) { break; } // Muestra la miniatura y el formulario para seleccionar la imagen. aicc_mostrar_miniatura_pixabay( $id, $imagen['tags'], $imagen['previewURL'], $imagen['largeImageURL'] ); $contador++; } echo "
\n"; // Incluir mención a que las imágenes provienen de Pixabay. echo '

' . esc_html( __( 'Images provided by', 'ai-content-creator' ) ) . ' Pixabay.

'; } /** * Muestra una miniatura de la imagen de Pixabay junto con un botón de selección. * * La función genera la estructura HTML necesaria para mostrar la miniatura de la imagen de Pixabay, * así como un botón de selección para elegir la imagen como imagen destacada en un artículo. * Si no se proporcionan las URL de la imagen y la miniatura, se mostrará un mensaje de error o un mensaje * que indica que no hay imagen. * * @param string $id ID de la imagen de Pixabay. * @param string $tags Etiquetas asociadas a la imagen, se utilizarán como atributo 'alt' y 'title'. * @param string $url_miniatura URL de la miniatura de la imagen. * @param string $url_imagen URL de la imagen en tamaño completo. */ function aicc_mostrar_miniatura_pixabay( $id, $tags = '', $url_miniatura = '', $url_imagen = '' ) { echo '
' . "\n"; // Si falta solo una de las dos URL, es que algo anda mal con la imagen. if ( ( '' === $url_miniatura ) ^ ( '' === $url_imagen ) ) { echo '
' . "\n"; echo '

' . esc_html( __( 'Image failed', 'ai-content-creator' ) ) . "

\n"; echo "
\n"; } else { // Muestra la imagen o un cartel de "Sin imagen" en función de si se ha indcado URL o no. if ( $url_miniatura ) { echo ' ' . esc_attr( $tags ) . '\n"; } else { echo '
' . "\n"; echo '

' . esc_html( __( 'Without image', 'ai-content-creator' ) ) . "

\n"; echo "
\n"; } echo '
' . "\n"; echo ' ' . "\n"; echo ' ' . "\n"; // Ausencia del prámetro para el caso de seleccionar que no tenga imagen. Si se envíase el parámetro, ha de ser con un valor válido. if ( $url_imagen ) { echo ' ' . "\n"; } echo ' ' . "\n"; echo ' \n"; echo "
\n"; } echo "
\n"; } /** Realiza la búsqueda de imágenes en Pixabay, haciendo caché de 24 horas. * * @param string $consulta Texto de la consulta enviada. * @param string $idioma idioma en que hacer la consulta. * @return array con las respuestas de Pixabay o un error. */ function aicc_busqueda_pixabay( $consulta, $idioma ) { global $wpdb; $tabla = aicc_tabla_pixabay_cache(); // Elimina las consultas antiguas (mayores a 24 horas). $result = aicc_borra_viejos_registros_cache( $tabla ); // Busca la consulta en la tabla caché, por requisito de Pixabay de cachear las consultas 24 horas. // A su vez, cachea esa consulta media hora. // Clave de caché basada en la consulta y el idioma. $cache_key = 'aicc_cache_' . md5( $consulta . $idioma ); // Intenta obtener el resultado desde la caché de WP. $cache_result = wp_cache_get( $cache_key ); // Si no hay un resultado en la caché de WP, realiza la consulta a la base de datos y almacena el resultado en la caché de WP. if ( false === $cache_result ) { // Inevitable por ser tablas ajenas a WP la advertencia: // WARNING | [ ] Usage of a direct database call is discouraged. // ERROR | Use placeholders and $wpdb->prepare(); found interpolated variable $tabla... // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $cache_result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$tabla} WHERE consulta = %s AND idioma = %s", $consulta, $idioma ) ); // Almacena el resultado en la caché con una expiración de media hora (1800 segundos). wp_cache_set( $cache_key, $cache_result, '', 1800 ); } // Si la consulta está en caché, devuelve la respuesta almacenada. if ( $cache_result ) { return json_decode( $cache_result->respuesta, true ); } // Si no está en caché, realiza la consulta a la API de Pixabay. $url = aicc_url_busqueda_pixabay( $consulta, $idioma ); $response = wp_remote_get( $url ); // Si la consulta a la API es exitosa, almacena la respuesta en la caché y la devuelve. if ( wp_remote_retrieve_response_code( $response ) === 200 ) { $respuesta_json = wp_remote_retrieve_body( $response ); // Inserta la respuesta en la tabla de caché. // Inevitable por ser tablas ajenas a WP la advertencia: // WARNING | [ ] Usage of a direct database call is discouraged. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery $wpdb->insert( $tabla, array( 'consulta' => $consulta, 'idioma' => $idioma, 'respuesta' => $respuesta_json, 'creado_en' => current_time( 'mysql', true ), ), array( '%s', '%s', '%s', '%s' ) ); return json_decode( $respuesta_json, true ); } // Si la consulta a la API no tuvo éxito, devuelve un error. return array( 'error' => true, 'message' => __( 'Pixabay API query failed.' ), ); } /** Borra registros de la tabla de caché con más de 24 horas de antigüedad. * * @param string $tabla Nombre de la tabla personalizada sin el prefijo de WordPress. */ function aicc_borra_viejos_registros_cache( $tabla ) { global $wpdb; // Calcular la fecha y hora de hace 24 horas en UTC. $fecha_limite = gmdate( 'Y-m-d H:i:s', strtotime( '-24 hours' ) ); // Borrar los registros con una fecha anterior a $fecha_limite. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( $wpdb->prepare( "DELETE FROM {$tabla} WHERE creado_en < %s", $fecha_limite ) ); } /** Sube la imagen a la biblioteca de medios y guarda su id en el objeto del artículo * para que llegue a la BBDD y se pueda incorporar el post al crear el borrador * * @param string $articulo_id Artículo. * @param string $imagen_url Dirección de la imagen que hay que subir a la biblioteca de medios. * @return int|false ID de la imagen subida a la biblioteca de medios, cero si no la hay, false si algo falló. */ function aicc_procesar_seleccion_imagen( $articulo_id, $imagen_url ) { $imagen_id = ''; // Graba en la BBDD el id de la imagen seleccionada. $aicc_generacion = new AIccGeneracion(); if ( $aicc_generacion->cargar( $articulo_id ) ) { // Sube la imagensi procede. if ( '' === $imagen_url ) { // No hay imagen que subir, queda con id 0. $imagen_id = 0; } else { // Sube la imagen y obtiene el ID. $imagen_id = aicc_upload_image_from_url( $imagen_url, $aicc_generacion->titulo(), $aicc_generacion->idioma() ); } // Graba el id de la imagen tanto si subió como si se decidió no usar ninguna. if ( false !== $imagen_id ) { // Si el artículo ya tiene borrador o está publicado, es tarde para cambiar la imagen. if ( ! $aicc_generacion->publicada_post_id() ) { // Si tiene una imagen previa, la elimina. aicc_eliminar_imagen_biblioteca( $aicc_generacion->imagen_id() ); // Asigna la nueva imagen. $aicc_generacion->imagen_id( $imagen_id ); if ( ! $aicc_generacion->grabar() ) { aicc_mostrar_notificacion( __( 'Unable to save the specified article image data.', 'ai-content-creator' ), 'error' ); return false; } } else { aicc_mostrar_notificacion( __( 'Unable to change the image of a publised article.', 'ai-content-creator' ), 'error' ); } } else { // Fallo al grabar la imagen en la biblioteca de medios. aicc_mostrar_notificacion( __( 'Unable to save the image for the specified article.', 'ai-content-creator' ), 'error' ); return false; } } else { // Fallo al cargar el artículo. aicc_mostrar_notificacion( __( 'Unable to load the specified article.', 'ai-content-creator' ), 'error' ); return false; } return $imagen_id; } /** Sube una imagen desde una URL a la biblioteca de medios de WordPress. * * Esta función descarga una imagen de una URL dada, la sube a la biblioteca de medios de * WordPress y devuelve su ID de adjunto. Si ocurre algún error durante el proceso de * descarga o subida, la función devuelve false. * * @param string $imagen_url La URL de la imagen que se desea subir. * @param string $titulo_articulo Título del artículo para aplicar en los metadatos de la imagen en la biblioteca de medios a nivel SEO. * @param string $idioma Idioma en que está el título del artículo. * @return int|false El ID de adjunto de la imagen subida, o false en caso de error. */ function aicc_upload_image_from_url( $imagen_url, $titulo_articulo, $idioma ) { if ( ! function_exists( 'wp_upload_bits' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } // Obtiene la imagen como cadena binaria. $response = wp_remote_get( $imagen_url ); if ( is_wp_error( $response ) ) { return false; } $image_data = wp_remote_retrieve_body( $response ); // Obtiene el nombre del archivo y su extensión. preg_match( '/[^\?]+\.(jpe?g|jpe|gif|png)\b/i', $imagen_url, $matches ); $file_extension = pathinfo( $matches[0], PATHINFO_EXTENSION ); // Crea el nombre de archivo SEO amigable. $seo_friendly_filename = aicc_seo_nombre_archivo_amigable( $titulo_articulo, $idioma ); $file_name = $seo_friendly_filename . '.' . $file_extension; // Sube la imagen y guarda la información en la base de datos. $upload_result = wp_upload_bits( $file_name, null, $image_data ); if ( ! empty( $upload_result['error'] ) ) { return false; } // Registra el archivo en la base de datos como adjunto. $file_path = $upload_result['file']; $file_url = $upload_result['url']; $file_type = wp_check_filetype( $file_path ); $attachment = array( 'guid' => $file_url, 'post_mime_type' => $file_type['type'], 'post_title' => $titulo_articulo, 'post_content' => $titulo_articulo, 'post_status' => 'inherit', ); $imagen_id = wp_insert_attachment( $attachment, $file_path ); if ( ! is_wp_error( $imagen_id ) ) { require_once ABSPATH . 'wp-admin/includes/image.php'; $metadata = wp_generate_attachment_metadata( $imagen_id, $file_path ); wp_update_attachment_metadata( $imagen_id, $metadata ); // Incorpora el texto alternativo a la imagen. update_post_meta( $imagen_id, '_wp_attachment_image_alt', $titulo_articulo ); } else { return false; } return $imagen_id; } /** Elimina una imagen de la biblioteca de medios de WordPress dado su ID. * * @param int $imagen_id El ID de la imagen que se va a eliminar. * @return bool Devuelve true si la eliminación fue exitosa, false si falló. */ function aicc_eliminar_imagen_biblioteca( $imagen_id ) { // Verifica si el ID de la imagen es válido y si corresponde a un archivo adjunto. if ( ! empty( $imagen_id ) && get_post_type( $imagen_id ) === 'attachment' ) { // Solo la intenta eliminar si no está en uso. if ( ! aicc_imagen_biblioteca_en_uso( $imagen_id ) ) { // Intenta eliminar la imagen de la biblioteca de medios. $eliminado = wp_delete_attachment( $imagen_id, true ); } else { aicc_mostrar_notificacion( __( 'Old image usage detected, it will not be deleted.', 'ai-content-creator' ), 'warning' ); $eliminado = false; } // Si se eliminó correctamente, devuelve true, de lo contrario, devuelve false. return false !== $eliminado; } // Si el ID no es válido o no corresponde a un archivo adjunto, devuelve false. return false; } /** * Comprueba si una imagen de la biblioteca de medios está en uso como imagen destacada o en otro contexto. * * @param int $imagen_id El ID de la imagen en la biblioteca de medios. * @return bool Devuelve true si la imagen está en uso, false en caso contrario. * * @phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query */ function aicc_imagen_biblioteca_en_uso( $imagen_id ) { // Comprobar si la imagen se utiliza como imagen destacada en algún post. $args = array( 'post_type' => 'any', 'posts_per_page' => 1, 'meta_query' => array( array( 'key' => '_thumbnail_id', 'value' => $imagen_id, ), ), ); $query = new WP_Query( $args ); if ( $query->found_posts > 0 ) { return true; } // Comprobar si la imagen se utiliza en el contenido de algún post. $image_url = wp_get_attachment_url( $imagen_id ); $args = array( 'post_type' => 'any', 'posts_per_page' => 1, 's' => $image_url, ); $query = new WP_Query( $args ); if ( $query->found_posts > 0 ) { return true; } // Si no se encuentra la imagen en uso, devuelve false. return false; } /** * Genera un nombre de archivo SEO amigable a partir de un título. * * Esta función convierte el título dado a un nombre de archivo SEO amigable, * reemplazando espacios y caracteres no alfanuméricos por guiones, eliminando * guiones múltiples y recortando guiones al principio y al final. Además, * limita la longitud del nombre del archivo al valor especificado por * $max_longitud sin truncar palabras. * * @param string $titulo Título del cual se generará el nombre de archivo. * @param string $idioma Idioma en que está el título del artículo. * @param int $max_longitud Longitud máxima del nombre de archivo (opcional, por defecto 100). * @return string Nombre de archivo SEO amigable generado a partir del título. */ function aicc_seo_nombre_archivo_amigable( $titulo, $idioma, $max_longitud = 100 ) { $nombre_archivo = strtolower( $titulo ); $nombre_archivo = preg_replace( '/[^a-z0-9]+/', '-', $nombre_archivo ); $nombre_archivo = preg_replace( '/-+/', '-', $nombre_archivo ); $nombre_archivo = trim( $nombre_archivo, '-' ); // Retira las stopwords. Para ello, ha de convertir la cadena en una matriz de palabras. // Divide el nombre de archivo en un array de palabras utilizando los guiones como separador. $palabras = explode( '-', $nombre_archivo ); // Limpia las palabras utilizando la función aicc_elimina_stopwords. $palabras_relevantes = aicc_elimina_stopwords( $palabras, $idioma ); // Une las palabras relevantes con guiones como concatenador para formar el nombre de archivo nuevamente. $nombre_archivo = implode( '-', $palabras_relevantes ); // Comprueba si el nombre de archivo supera la longitud máxima. if ( strlen( $nombre_archivo ) > $max_longitud ) { // Encuentra la posición del último guión antes de la longitud máxima. $ultima_posicion_guion = strrpos( $nombre_archivo, '-', $max_longitud - strlen( $nombre_archivo ) - 1 ); // Si se encuentra un guión, recorta el nombre de archivo en esa posición. if ( false !== $ultima_posicion_guion ) { $nombre_archivo = substr( $nombre_archivo, 0, $ultima_posicion_guion ); } else { // Si no se encuentra un guión, recorta el nombre de archivo a la longitud máxima. $nombre_archivo = substr( $nombre_archivo, 0, $max_longitud ); } } return $nombre_archivo; }