*/ public static function image_ids( array $attachment_ids ): array { $ids = array_values( array_unique( array_filter( array_map( 'intval', $attachment_ids ) ) ) ); if ( array() === $ids ) { return array(); } // Мету всех вложений — одним запросом, а не по запросу на вложение. update_meta_cache( 'post', $ids ); $result = array(); foreach ( $ids as $id ) { $image_id = (string) get_post_meta( $id, Meta::IMAGE_ID, true ); if ( '' !== $image_id ) { $result[ $id ] = $image_id; } } return $result; } /** * Кладёт картинку в медиатеку или находит уже положенную. * * @param string $image_id Id картинки aiModelka. * @param bool $watermark Водяной знак. * @param bool $ai_label Видимая пометка ИИ. * @return int Id вложения. * @throws Action_Failed Файл не получен, не скачан или не принят медиатекой. */ public function import( string $image_id, bool $watermark, bool $ai_label ): int { $variant = self::variant( $this->file_format, $watermark, $ai_label ); $context = array( 'product_id' => $this->product_id, 'photo_session_id' => $this->photo_session_id, 'image_id' => $image_id, 'variant' => $variant, ); $existing = $this->find_existing( $image_id, $variant ); if ( $existing > 0 ) { Logger::info( 'Photo is already in the product, the existing file is reused', array_merge( $context, array( 'attachment_id' => $existing ) ) ); return $existing; } try { $file = $this->client->create_image_file( $this->photo_session_id, $image_id, $this->file_format, $watermark, $ai_label ); } catch ( Api_Exception $exception ) { throw Action_Failed::from_api( $exception ); } $url = is_string( $file['url'] ?? null ) ? $file['url'] : ''; $file_name = is_string( $file['fileName'] ?? null ) && '' !== $file['fileName'] ? $file['fileName'] : 'aimodelka-' . $image_id . '.' . $this->file_format; if ( '' === $url ) { Logger::error( 'aiModelka answered without a file address', $context ); throw new Action_Failed( __( 'aiModelka could not complete the request. Try again in a few minutes.', 'aimodelka' ), true ); } $tmp = $this->download( $url, $file_name, $context ); $attachment_id = $this->add_to_media_library( $tmp, $file_name, $context ); update_post_meta( $attachment_id, '_wp_attachment_image_alt', $this->product_name ); update_post_meta( $attachment_id, Meta::IMAGE_ID, $image_id ); update_post_meta( $attachment_id, Meta::IMAGE_VARIANT, $variant ); // Только новое вложение: уже лежащее в медиатеке (повтор) человек мог разложить сам, // и переносить его в другую папку нельзя. Media_Folders::put( $attachment_id, $this->folder_id ); Logger::info( 'Photo added to the Media Library', array_merge( $context, array( 'attachment_id' => $attachment_id, 'file_name' => $file_name, 'folder_id' => $this->folder_id, ) ) ); return $attachment_id; } /** * Вложение этого товара с той же картинкой aiModelka и тем же видом файла. * * @param string $image_id Id картинки aiModelka. * @param string $variant Вид файла. * @return int Id вложения или 0. */ private function find_existing( string $image_id, string $variant ): int { $found = get_posts( array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'post_parent' => $this->product_id, 'fields' => 'ids', 'numberposts' => 1, 'no_found_rows' => true, // Фильтры других плагинов (языковых и т. п.) get_posts отключает сам — так он устроен // по умолчанию: поиск дубля не зависит от того, что они прячут. Задавать это вручную // не нужно, Plugin Check считает такую строку ошибкой. // Ищется среди вложений одного товара — их единицы, запрос по мете тут дешёвый. 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query array( 'key' => Meta::IMAGE_ID, 'value' => $image_id, ), array( 'key' => Meta::IMAGE_VARIANT, 'value' => $variant, ), ), ) ); return array() === $found ? 0 : (int) $found[0]; } /** * Скачивает готовый файл во временный файл на сервере магазина. * * Файл пишется на диск по мере скачивания, а не в память: PNG в 2K весит мегабайты, * а память PHP на хостинге бывает тесной. * * @param string $url Подписанная ссылка на файл. * @param string $file_name Имя файла — от него берётся расширение временного файла. * @param array $context Подробности для журнала. * @return string Путь к временному файлу. * @throws Action_Failed Скачать не удалось — временная неудача, повтор позже может помочь. */ private function download( string $url, string $file_name, array $context ): string { require_once ABSPATH . 'wp-admin/includes/file.php'; $tmp = wp_tempnam( $file_name ); if ( ! $tmp ) { Logger::error( 'Could not create a temporary file', $context ); throw new Action_Failed( __( 'The shop could not create a temporary file for the photo. Ask your hosting provider to check free disk space.', 'aimodelka' ) ); } $response = wp_safe_remote_get( $url, array( 'timeout' => self::DOWNLOAD_TIMEOUT, 'stream' => true, 'filename' => $tmp, ) ); $status = is_wp_error( $response ) ? 0 : (int) wp_remote_retrieve_response_code( $response ); if ( 200 !== $status ) { wp_delete_file( $tmp ); Logger::error( 'Photo download failed', array_merge( $context, array( 'http_status' => $status, 'error' => is_wp_error( $response ) ? $response->get_error_message() : '', ) ) ); throw new Action_Failed( __( 'The shop could not download a photo from aiModelka. Try again in a few minutes.', 'aimodelka' ), true ); } return $tmp; } /** * Кладёт скачанный файл в медиатеку штатным механизмом WordPress. * * @param string $tmp Временный файл. * @param string $file_name Имя файла в медиатеке (`fileName` из ответа aiModelka). * @param array $context Подробности для журнала. * @return int Id вложения. * @throws Action_Failed Медиатека не приняла файл. */ private function add_to_media_library( string $tmp, string $file_name, array $context ): int { require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; $attachment_id = media_handle_sideload( array( 'name' => $file_name, 'tmp_name' => $tmp, ), $this->product_id, $this->product_name ); if ( is_wp_error( $attachment_id ) ) { if ( file_exists( $tmp ) ) { wp_delete_file( $tmp ); } Logger::error( 'Media Library refused the photo', array_merge( $context, array( 'error' => $attachment_id->get_error_message() ) ) ); throw new Action_Failed( sprintf( /* translators: %s: error message from WordPress. */ __( 'WordPress could not add a photo to the Media Library: %s', 'aimodelka' ), $attachment_id->get_error_message() ) ); } return (int) $attachment_id; } }