dvadf
File manager - Edit - /home/centroca/public_html/wp/wp-content/plugins/nextgen-gallery/src/REST/DataMappers/ImageREST.php
Back
<?php /** * REST API endpoints for NextGEN Gallery images. * * @package Imagely\NGG\REST\DataMappers */ namespace Imagely\NGG\REST\DataMappers; use WP_REST_Request; use WP_REST_Response; use WP_Error; use Imagely\NGG\DataMappers\Gallery as GalleryMapper; use Imagely\NGG\DataMappers\Image as ImageMapper; use Imagely\NGG\DataTypes\Image; use Imagely\NGG\Settings\GlobalSettings; use Imagely\NGG\Util\Security; use Imagely\NGG\Util\Transient; /** * Class ImageREST * Handles REST API endpoints for NextGEN Gallery images. * * @package Imagely\NGG\REST\DataMappers */ class ImageREST { /** * Register the REST API routes for images */ public static function register_routes() { register_rest_route( 'imagely/v1', '/images', [ 'methods' => 'GET', 'callback' => [ self::class, 'get_images' ], 'permission_callback' => [ self::class, 'check_read_permission' ], 'args' => [ 'gallery_id' => [ 'type' => 'integer', 'sanitize_callback' => 'absint', ], 'per_page' => [ 'type' => 'integer', 'default' => 100, 'sanitize_callback' => function ( $value ) { // Allow -1 for "all images", otherwise ensure minimum of 1 $int_value = (int) $value; if ( $int_value < 0 ) { return -1; // Normalize to -1 for "all" } return max( 1, $int_value ); }, ], 'page' => [ 'type' => 'integer', 'default' => 1, 'sanitize_callback' => 'absint', ], 'orderby' => [ 'type' => 'string', 'enum' => [ 'sortorder', 'pid', 'filename', 'imagedate' ], 'default' => 'sortorder', 'sanitize_callback' => 'sanitize_text_field', ], 'order' => [ 'type' => 'string', 'enum' => [ 'ASC', 'DESC' ], 'default' => 'ASC', 'sanitize_callback' => 'sanitize_text_field', ], ], ] ); // Get a single image. register_rest_route( 'imagely/v1', '/images/(?P<id>\d+)', [ 'methods' => 'GET', 'callback' => [ self::class, 'get_image' ], 'permission_callback' => [ self::class, 'check_read_permission' ], 'args' => [ 'id' => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint', ], ], ] ); // Update an image. register_rest_route( 'imagely/v1', '/images/(?P<id>\d+)', [ 'methods' => 'PUT', 'callback' => [ self::class, 'update_image' ], 'permission_callback' => [ self::class, 'check_image_edit_permission' ], 'args' => [ 'id' => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint', ], 'alttext' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ], 'description' => [ 'type' => 'string', 'sanitize_callback' => 'wp_kses_post', ], 'exclude' => [ 'type' => 'boolean', ], 'image_slug' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_title', ], 'sortorder' => [ 'type' => 'integer', 'sanitize_callback' => 'absint', ], 'tags' => [ 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ], 'pricelist_id' => [ 'type' => 'integer', 'sanitize_callback' => function ( $value ) { // Allow -1 for "not for sale", 0 for "use gallery pricelist", positive integers for actual pricelists $int_value = (int) $value; return ( $int_value === -1 || $int_value >= 0 ) ? $int_value : 0; }, 'description' => 'Pricelist ID for ecommerce functionality', ], ], ] ); // Delete an image. register_rest_route( 'imagely/v1', '/images/(?P<id>\d+)', [ 'methods' => 'DELETE', 'callback' => [ self::class, 'delete_image' ], 'permission_callback' => [ self::class, 'check_delete_permission' ], 'args' => [ 'id' => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint', ], ], ] ); // Move an image to a new sortorder slot (cross-page or in-page). register_rest_route( 'imagely/v1', '/images/(?P<id>\d+)/move', [ 'methods' => 'POST', 'callback' => [ self::class, 'move_image' ], 'permission_callback' => [ self::class, 'check_edit_permission' ], 'args' => [ 'id' => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint', ], 'gallery_id' => [ 'required' => true, 'type' => 'integer', 'sanitize_callback' => 'absint', ], 'after_pid' => [ 'type' => 'integer', 'sanitize_callback' => 'absint', ], 'before_pid' => [ 'type' => 'integer', 'sanitize_callback' => 'absint', ], 'target_page' => [ 'type' => 'integer', 'sanitize_callback' => 'absint', ], 'per_page' => [ 'type' => 'integer', 'sanitize_callback' => 'absint', ], ], ] ); // Bulk update images. register_rest_route( 'imagely/v1', '/images/bulk', [ 'methods' => 'PUT', 'callback' => [ self::class, 'bulk_update_images' ], 'permission_callback' => [ self::class, 'check_edit_permission' ], 'args' => [ 'images' => [ 'required' => true, 'type' => 'array', 'items' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'required' => true, 'type' => 'integer', ], 'alttext' => [ 'type' => 'string', ], 'description' => [ 'type' => 'string', ], 'exclude' => [ 'type' => 'boolean', ], 'image_slug' => [ 'type' => 'string', ], 'sortorder' => [ 'type' => 'integer', ], 'tags' => [ 'type' => 'string', ], 'pricelist_id' => [ 'type' => 'integer', 'description' => 'Pricelist ID for ecommerce functionality', ], ], ], ], ], ] ); // Import images from the media library. register_rest_route( 'imagely/v1', '/images/import-media-library', [ 'methods' => 'POST', 'callback' => [ self::class, 'import_media_library' ], 'permission_callback' => [ self::class, 'check_edit_permission' ], 'args' => [ 'gallery_id' => [ 'type' => 'integer', 'required' => false, 'sanitize_callback' => 'absint', ], 'gallery_name' => [ 'type' => 'string', 'required' => false, 'sanitize_callback' => 'sanitize_text_field', ], 'attachment_ids' => [ 'type' => 'array', 'items' => [ 'type' => 'integer' ], 'required' => true, ], ], ] ); // Upload an image to a gallery (single image or zip). register_rest_route( 'imagely/v1', '/images/upload', [ 'methods' => 'POST', 'callback' => [ self::class, 'upload_image' ], 'permission_callback' => [ self::class, 'check_edit_permission' ], 'args' => [ 'gallery_id' => [ 'type' => 'integer', 'required' => false, 'sanitize_callback' => 'absint', ], 'gallery_name' => [ 'type' => 'string', 'required' => false, 'sanitize_callback' => 'sanitize_text_field', ], ], ] ); // Browse a folder for import. register_rest_route( 'imagely/v1', '/folders/browse', [ 'methods' => 'GET', 'callback' => [ self::class, 'browse_folder' ], 'permission_callback' => [ self::class, 'check_folder_import_permission' ], 'args' => [ 'dir' => [ 'type' => 'string', 'required' => false, 'sanitize_callback' => 'sanitize_text_field', ], ], ] ); // Import a folder as a gallery. register_rest_route( 'imagely/v1', '/folders/import', [ 'methods' => 'POST', 'callback' => [ self::class, 'import_folder' ], 'permission_callback' => [ self::class, 'check_folder_import_permission' ], 'args' => [ 'folder' => [ 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field', ], 'keep_location' => [ 'type' => 'boolean', 'required' => false, ], 'gallery_title' => [ 'type' => 'string', 'required' => false, 'sanitize_callback' => 'sanitize_text_field', ], ], ] ); } /** * Check if user has permission to read images * * @return bool */ public static function check_read_permission() { return Security::is_allowed( 'NextGEN Gallery overview' ); } /** * Check if user has permission to edit images * * @param \WP_REST_Request|null $request Optional REST request — when supplied, per-gallery ownership is enforced. * @return bool */ public static function check_edit_permission( $request = null ) { if ( ! Security::is_allowed( 'NextGEN Manage gallery' ) ) { return false; } if ( null === $request ) { return true; } // Upload/import routes: verify the user owns (or can manage) the target gallery. // Authorization is gallery-level: ownership of the destination, not the images. $gallery_id = (int) $request->get_param( 'gallery_id' ); if ( $gallery_id > 0 ) { return self::current_user_can_manage_gallery( $gallery_id ); } // Bulk endpoints have no single id; per-row ownership is enforced inside the handler. return true; } /** * Check if user has permission to edit a specific image (single PUT). * Resolves image_id from URL params only and verifies gallery ownership. * * @param WP_REST_Request $request The REST request object. * @return bool|\WP_Error */ public static function check_image_edit_permission( WP_REST_Request $request ) { if ( ! Security::is_allowed( 'NextGEN Manage gallery' ) ) { return false; } $url_params = $request->get_url_params(); $image_id = isset( $url_params['id'] ) ? (int) $url_params['id'] : 0; if ( $image_id <= 0 ) { return false; } return self::current_user_can_manage_image( $image_id ); } /** * Folder browse/import: NextGEN Manage gallery, multisite wpmuImportFolder when applicable, * and legacy nggGallery::current_user_can( 'NextGEN Import image folder' ) (cap may be unregistered → allowed). * * @return bool */ public static function check_folder_import_permission() { if ( ! self::check_edit_permission() ) { return false; } if ( is_multisite() && ! GlobalSettings::get_instance()->get( 'wpmuImportFolder', false ) ) { return false; } if ( class_exists( 'nggGallery', false ) ) { return \nggGallery::current_user_can( 'NextGEN Import image folder' ); } // nggGallery should always be loaded; if it isn't, fall back to the // management-cap gate already passed above rather than denying on an // unregistered capability. return true; } /** * Check if user has permission to delete images * * @param WP_REST_Request $request The REST request object. * @return bool */ public static function check_delete_permission( WP_REST_Request $request ) { if ( ! Security::is_allowed( 'NextGEN Manage gallery' ) ) { return false; } $url_params = $request->get_url_params(); $image_id = isset( $url_params['id'] ) ? (int) $url_params['id'] : 0; if ( $image_id <= 0 ) { return false; } return self::current_user_can_manage_image( $image_id ); } /** * Check if the current user can manage the gallery that contains a given image. * Users can manage an image if they own its gallery or have "NextGEN Manage others gallery". * * @param int $image_id The image ID. * @return bool|\WP_Error */ public static function current_user_can_manage_image( int $image_id ) { $image = ImageMapper::get_instance()->find( $image_id ); if ( ! $image ) { return new \WP_Error( 'rest_image_not_found', __( 'Image not found.', 'nggallery' ), [ 'status' => 404 ] ); } $gallery = GalleryMapper::get_instance()->find( $image->galleryid ); if ( ! $gallery ) { // Orphaned image: deny unless the user has explicit cross-gallery capability. return Security::is_allowed( 'NextGEN Manage others gallery' ); } return ( (int) get_current_user_id() === (int) $gallery->author ) || Security::is_allowed( 'NextGEN Manage others gallery' ); } /** * Check if the current user can manage a specific gallery. * Used to authorize upload/import operations targeting an existing gallery. * * @param int $gallery_id The gallery ID. * @return bool */ public static function current_user_can_manage_gallery( int $gallery_id ) { $gallery = GalleryMapper::get_instance()->find( $gallery_id ); if ( ! $gallery ) { return false; } return ( (int) get_current_user_id() === (int) $gallery->author ) || Security::is_allowed( 'NextGEN Manage others gallery' ); } /** * Resolve an image id to its parent gallery's author and verify the current user may edit it. * * Returns true when the user owns the parent gallery or holds 'NextGEN Manage others gallery'. * Missing image returns true so the handler can emit a 404 instead of a 403. * * @param int $image_id NGG image id (pid). * @return bool */ protected static function current_user_owns_image( $image_id ) { $image = ImageMapper::get_instance()->find( $image_id ); if ( ! $image ) { return true; } $gallery_id = isset( $image->galleryid ) ? (int) $image->galleryid : 0; if ( $gallery_id <= 0 ) { return Security::is_allowed( 'NextGEN Manage others gallery' ); } $gallery = GalleryMapper::get_instance()->find( $gallery_id ); if ( ! $gallery ) { return Security::is_allowed( 'NextGEN Manage others gallery' ); } if ( get_current_user_id() === (int) $gallery->author ) { return true; } return Security::is_allowed( 'NextGEN Manage others gallery' ); } /** * Get all images with optional filtering and pagination. * * @param WP_REST_Request $request The request object containing gallery_id, per_page, page, orderby, order parameters. * @return WP_REST_Response */ public static function get_images( WP_REST_Request $request ) { global $wpdb; $gallery_id = $request->get_param( 'gallery_id' ); // Get pagination parameters $per_page_param = (int) $request->get_param( 'per_page' ); // Normalize all negative values to -1 (treated as "all") for consistency if ( $per_page_param < 0 ) { $per_page_param = -1; } // Handle -1 as "all" (WordPress standard for unlimited pagination) $per_page = ( -1 === $per_page_param ) ? PHP_INT_MAX : $per_page_param; $page = max( 1, (int) $request->get_param( 'page' ) ); $offset = ( $page - 1 ) * $per_page; // Get ordering parameters $orderby = $request->get_param( 'orderby' ) ?? 'sortorder'; $order = strtoupper( $request->get_param( 'order' ) ?? 'ASC' ); $mapper = ImageMapper::get_instance(); $query = $mapper->select(); if ( $gallery_id ) { $query->where( [ 'galleryid = %d', $gallery_id ] ); } // Calculate total items for pagination $count_query = $mapper->select(); if ( $gallery_id ) { $count_query->where( [ 'galleryid = %d', $gallery_id ] ); } $total_items = count( $count_query->run_query( false, false, true ) ); // Add ordering and pagination. Append a deterministic pid tiebreaker so the // displayed order matches the order ensure_sequential_sortorders() renumbers in // (sortorder ASC, pid ASC). Without it, healing tied sortorders could reshuffle // rows relative to what the user saw, landing a drag-drop on the wrong side. $query->order_by( $orderby, $order ); if ( 'pid' !== $orderby ) { $query->order_by( 'pid', 'ASC' ); } if ( -1 !== $per_page_param ) { $query->limit( $per_page, $offset ); } $images = $query->run_query(); $images_data = array_map( [ self::class, 'prepare_image_for_response' ], $images ); $response = new WP_REST_Response( $images_data ); // Add pagination headers if ( -1 !== $per_page_param ) { $total_pages = ceil( $total_items / $per_page ); $response->header( 'X-WP-Total', $total_items ); $response->header( 'X-WP-TotalPages', $total_pages ); } else { // When returning all items, set total pages to 1 $response->header( 'X-WP-Total', $total_items ); $response->header( 'X-WP-TotalPages', 1 ); } return $response; } /** * Get a single image by ID. * * @param WP_REST_Request $request The request object containing the image ID. * @return WP_REST_Response|WP_Error */ public static function get_image( WP_REST_Request $request ) { $image_id = $request->get_param( 'id' ); $mapper = ImageMapper::get_instance(); $image = $mapper->find( $image_id ); if ( ! $image ) { return new WP_Error( 'not_found', __( 'Image not found', 'nggallery' ), [ 'status' => 404 ] ); } return new WP_REST_Response( self::prepare_image_for_response( $image ) ); } /** * Update an image. * * @param WP_REST_Request $request The request object containing image data to update. * @return WP_REST_Response|WP_Error */ public static function update_image( WP_REST_Request $request ) { $image_id = $request->get_param( 'id' ); $mapper = ImageMapper::get_instance(); $image = $mapper->find( $image_id ); if ( ! $image ) { return new WP_Error( 'not_found', __( 'Image not found', 'nggallery' ), [ 'status' => 404 ] ); } $fields = [ 'alttext', 'description', 'exclude', 'image_slug', 'sortorder', 'pricelist_id' ]; foreach ( $fields as $field ) { if ( $request->has_param( $field ) ) { $value = $request->get_param( $field ); if ( 'exclude' === $field ) { $value = $value ? 1 : 0; } $image->$field = $value; } } try { $mapper->save( $image ); // Handle tags separately using WordPress taxonomy functions if ( $request->has_param( 'tags' ) ) { $tags = $request->get_param( 'tags' ); if ( ! is_string( $tags ) ) { return new WP_Error( 'invalid_tags', __( 'Tags must be a string', 'nggallery' ), [ 'status' => 400 ] ); } $tags = array_filter( array_map( 'trim', explode( ',', $tags ) ), function ( $tag ) { return ! empty( $tag ); } ); $tags = array_values( array_unique( $tags ) ); wp_set_object_terms( $image->pid, $tags, 'ngg_tag' ); } // Reload image from database to get fresh data $fresh_image = $mapper->find( $image_id ); return new WP_REST_Response( self::prepare_image_for_response( $fresh_image ) ); } catch ( \Exception $e ) { return new WP_Error( 'update_failed', $e->getMessage(), [ 'status' => 500 ] ); } } /** * Move an image to a new sortorder slot via a single range-shift. * * Accepts one of three target modes in the request body: * - after_pid: place the moved image immediately after another image. * - before_pid: place it immediately before another image. * - target_page (+ per_page): place it at the end of the given page, * clamped to the end of the gallery if the page is beyond the total. * * Executes at most two SQL statements: a range UPDATE that shifts every * affected row's sortorder by ±1, then a single UPDATE that positions the * moved row. Both rely on InnoDB row locks for concurrency safety. * * @param WP_REST_Request $request The request carrying the move parameters. * @return WP_REST_Response|WP_Error */ public static function move_image( WP_REST_Request $request ) { global $wpdb; $pid = (int) $request->get_param( 'id' ); $gallery_id = (int) $request->get_param( 'gallery_id' ); $mapper = ImageMapper::get_instance(); $image = $mapper->find( $pid ); if ( ! $image || (int) $image->galleryid !== $gallery_id ) { return new WP_Error( 'ngg_image_not_found', __( 'Image not found in this gallery', 'nggallery' ), [ 'status' => 404 ] ); } $table = $mapper->get_table_name(); // Heal galleries whose sortorders default to 0; only refetch when the heal actually ran. $healed = self::ensure_sequential_sortorders( $gallery_id, $table ); if ( is_wp_error( $healed ) ) { return $healed; } if ( $healed ) { $mapper->flush_query_cache(); Transient::flush( 'displayed_gallery_rendering' ); $image = $mapper->find( $pid ); } $current_sortorder = (int) $image->sortorder; $target_sortorder = self::resolve_target_sortorder( $request, $gallery_id, $current_sortorder ); if ( is_wp_error( $target_sortorder ) ) { return $target_sortorder; } if ( $target_sortorder === $current_sortorder ) { return new WP_REST_Response( self::prepare_image_for_response( $image ), 200 ); } // Atomic: roll back the shift if the final positional update fails. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ( false === $wpdb->query( 'START TRANSACTION' ) ) { return new WP_Error( 'ngg_move_failed', __( 'Failed to start transaction', 'nggallery' ), [ 'status' => 500 ] ); } if ( $current_sortorder < $target_sortorder ) { // Forward move: pull rows in (S, N] down by 1 to close the gap left // by the moved row, then drop the moved row into N. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $shift_result = $wpdb->query( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "UPDATE {$table} SET sortorder = sortorder - 1 WHERE galleryid = %d AND sortorder > %d AND sortorder <= %d", $gallery_id, $current_sortorder, $target_sortorder ) ); } else { // Backward move: push rows in [N, S) up by 1, then drop the moved row into N. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $shift_result = $wpdb->query( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "UPDATE {$table} SET sortorder = sortorder + 1 WHERE galleryid = %d AND sortorder >= %d AND sortorder < %d", $gallery_id, $target_sortorder, $current_sortorder ) ); } if ( false === $shift_result ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( 'ROLLBACK' ); return new WP_Error( 'ngg_move_failed', __( 'Failed to reorder images', 'nggallery' ), [ 'status' => 500 ] ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $update_result = $wpdb->update( $table, [ 'sortorder' => $target_sortorder ], [ 'pid' => $pid ] ); if ( false === $update_result ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( 'ROLLBACK' ); return new WP_Error( 'ngg_move_failed', __( 'Failed to reorder images', 'nggallery' ), [ 'status' => 500 ] ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching if ( false === $wpdb->query( 'COMMIT' ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( 'ROLLBACK' ); return new WP_Error( 'ngg_move_failed', __( 'Failed to commit transaction', 'nggallery' ), [ 'status' => 500 ] ); } $mapper->flush_query_cache(); Transient::flush( 'displayed_gallery_rendering' ); $fresh = $mapper->find( $pid ); return new WP_REST_Response( self::prepare_image_for_response( $fresh ), 200 ); } /** * Resolve the target sortorder for a move based on which mode the request used. * * @param WP_REST_Request $request The request carrying after_pid / before_pid / target_page. * @param int $gallery_id ID of the gallery the move is scoped to. * @param int $current_sortorder Sortorder of the image being moved. * @return int|WP_Error Target sortorder, or WP_Error on invalid input. */ private static function resolve_target_sortorder( WP_REST_Request $request, $gallery_id, $current_sortorder ) { global $wpdb; $mapper = ImageMapper::get_instance(); $table = $mapper->get_table_name(); $after = (int) $request->get_param( 'after_pid' ); $before = (int) $request->get_param( 'before_pid' ); $page = (int) $request->get_param( 'target_page' ); // Require exactly one target mode; reject ambiguous combinations. $mode_count = ( $after > 0 ? 1 : 0 ) + ( $before > 0 ? 1 : 0 ) + ( $page > 0 ? 1 : 0 ); if ( $mode_count > 1 ) { return new WP_Error( 'ngg_conflicting_target', __( 'Provide exactly one of after_pid, before_pid, or target_page', 'nggallery' ), [ 'status' => 400 ] ); } if ( $after > 0 || $before > 0 ) { $reference_pid = $after > 0 ? $after : $before; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $row = $wpdb->get_row( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT sortorder FROM {$table} WHERE pid = %d AND galleryid = %d", $reference_pid, $gallery_id ) ); if ( ! $row ) { return new WP_Error( 'ngg_reference_not_found', __( 'Reference image is not in this gallery', 'nggallery' ), [ 'status' => 400 ] ); } $neighbor = (int) $row->sortorder; $moving_forward = $current_sortorder < $neighbor; // The range-shift in move_image() vacates one neighboring slot, so the // target lands on the neighbor itself in the "shift-toward" direction // and on the next/prev slot in the "shift-away" direction. if ( $after > 0 ) { return $moving_forward ? $neighbor : $neighbor + 1; } return $moving_forward ? $neighbor - 1 : $neighbor; } if ( $page > 0 ) { $per_page = (int) $request->get_param( 'per_page' ); if ( $per_page < 1 ) { $per_page = 50; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $max_row = $wpdb->get_row( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT COALESCE(MAX(sortorder), 0) AS max_sort FROM {$table} WHERE galleryid = %d", $gallery_id ) ); $end_of_page = $page * $per_page; $end_of_gallery = (int) $max_row->max_sort; return min( $end_of_page, $end_of_gallery ); } return new WP_Error( 'ngg_missing_target', __( 'Must provide after_pid, before_pid, or target_page', 'nggallery' ), [ 'status' => 400 ] ); } /** * Renumber a gallery's sortorders to 1..N when they are not unique. * * The range-shift in move_image() shifts a contiguous band of sortorder values * by ±1, so its one hard requirement is that sortorders are UNIQUE — on ties the * shift collapses two rows onto the same slot. Gaps and a non-1 start are harmless * to the shift, so we deliberately do NOT renumber for those: doing so would rewrite * the entire gallery on the first move after any delete (which leaves a gap) for no * benefit. We only heal duplicates — e.g. freshly-uploaded images that all default * to sortorder 0 — renumbering in display order (sortorder, then pid as tie-breaker). * * @param int $gallery_id ID of the gallery to renumber. * @param string $table Fully-qualified images table name. * @return bool|WP_Error True if renumbered, false if already unique, WP_Error on write failure. */ private static function ensure_sequential_sortorders( $gallery_id, $table ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $stats = $wpdb->get_row( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT COUNT(*) AS cnt, COUNT(DISTINCT sortorder) AS distinct_cnt FROM {$table} WHERE galleryid = %d", $gallery_id ) ); if ( ! $stats ) { return false; } $cnt = (int) $stats->cnt; if ( $cnt === 0 ) { return false; } // Sortorders are already safe for the range-shift as long as they are unique. // Only a duplicate (distinct count below the row count) forces a renumber. if ( $cnt === (int) $stats->distinct_cnt ) { return false; } // Prefetch + per-row update: MySQL forbids ORDER BY on multi-table UPDATE, // and user-defined vars in UPDATE are deprecated in 8.0+. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $rows = $wpdb->get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared "SELECT pid FROM {$table} WHERE galleryid = %d ORDER BY sortorder ASC, pid ASC", $gallery_id ) ); if ( empty( $rows ) ) { return false; } // Wrap the per-row renumber in its own transaction: a mid-loop write failure // must not leave the gallery half-renumbered (a worse, possibly newly-duplicated // state). This is separate from the move's own transaction, which has not started // yet at the call site. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( 'START TRANSACTION' ); $sortorder = 1; foreach ( $rows as $row ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $update_result = $wpdb->update( $table, [ 'sortorder' => $sortorder ], [ 'pid' => (int) $row->pid ] ); if ( false === $update_result ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( 'ROLLBACK' ); return new WP_Error( 'ngg_renumber_failed', __( 'Failed to renumber gallery sortorders', 'nggallery' ), [ 'status' => 500 ] ); } ++$sortorder; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( 'COMMIT' ); return true; } /** * Delete an image. * * @param WP_REST_Request $request The request object containing the image ID to delete. * @return WP_REST_Response|WP_Error */ public static function delete_image( WP_REST_Request $request ) { global $wpdb; $image_id = $request->get_param( 'id' ); $mapper = ImageMapper::get_instance(); $image = $mapper->find( $image_id ); if ( ! $image ) { return new WP_Error( 'not_found', __( 'Image not found', 'nggallery' ), [ 'status' => 404 ] ); } // Get the gallery ID before deletion $gallery_id = $image->galleryid; try { // Fire the action hook for other plugins to respond to image deletion // This must be called BEFORE any deletion so that listening plugins can access the database record do_action( 'ngg_delete_picture', $image_id, $image ); // Check if we should delete image files from filesystem $settings = \Imagely\NGG\Settings\Settings::get_instance(); $storage = \Imagely\NGG\DataStorage\Manager::get_instance(); if ( $settings->get( 'deleteImg' ) ) { // Delete image files from filesystem and database $delete_success = $storage->delete_image( $image ); if ( ! $delete_success ) { return new WP_Error( 'delete_files_failed', __( 'Could not delete image file(s) from disk', 'nggallery' ), [ 'status' => 500 ] ); } } else { // Only remove from database, keep files on disk $mapper->destroy( $image ); } // Check if the deleted image was the gallery's preview image // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery $gallery_preview = $wpdb->get_var( $wpdb->prepare( "SELECT previewpic FROM {$wpdb->nggallery} WHERE gid = %d AND previewpic = %d", $gallery_id, $image_id ) ); // If this was the preview image, update the gallery to use the first available image if ( $gallery_preview ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery $first_image_id = $wpdb->get_var( $wpdb->prepare( "SELECT pid FROM {$wpdb->nggpictures} WHERE galleryid = %d ORDER BY sortorder ASC, pid ASC LIMIT 1", $gallery_id ) ); // Update the gallery's previewpic field // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->update( $wpdb->nggallery, [ 'previewpic' => $first_image_id ? (int) $first_image_id : 0 ], [ 'gid' => $gallery_id ], [ '%d' ], [ '%d' ] ); } Transient::flush( 'rest_galleries' ); return new WP_REST_Response( [ 'message' => __( 'Image deleted successfully', 'nggallery' ), 'deleted' => true, 'id' => $image_id, 'files_deleted' => (bool) $settings->get( 'deleteImg' ), ] ); } catch ( \Exception $e ) { return new WP_Error( 'delete_failed', $e->getMessage(), [ 'status' => 500 ] ); } } /** * Bulk update images. * * @param WP_REST_Request $request The request object containing an array of image data to update. * @return WP_REST_Response */ public static function bulk_update_images( WP_REST_Request $request ) { $images_data = $request->get_param( 'images' ); $mapper = ImageMapper::get_instance(); $updated = []; $errors = []; foreach ( $images_data as $image_data ) { $can_manage = self::current_user_can_manage_image( (int) $image_data['id'] ); if ( is_wp_error( $can_manage ) || ! $can_manage ) { // translators: %d is the image ID. $errors[] = sprintf( __( 'Permission denied for image ID %d', 'nggallery' ), $image_data['id'] ); continue; } $image = $mapper->find( $image_data['id'] ); if ( ! $image ) { // translators: %d is the image ID. $errors[] = sprintf( __( 'Image with ID %d not found', 'nggallery' ), $image_data['id'] ); continue; } $fields = [ 'alttext', 'description', 'exclude', 'image_slug', 'sortorder', 'pricelist_id' ]; foreach ( $fields as $field ) { if ( isset( $image_data[ $field ] ) ) { $value = $image_data[ $field ]; if ( 'exclude' === $field ) { $value = $value ? 1 : 0; } $image->$field = $value; } } try { $mapper->save( $image ); // Handle tags separately using WordPress taxonomy functions if ( isset( $image_data['tags'] ) ) { $tags = $image_data['tags']; if ( is_string( $tags ) ) { $tags = array_filter( array_map( 'trim', explode( ',', $tags ) ), function ( $tag ) { return ! empty( $tag ); } ); $tags = array_values( array_unique( $tags ) ); } wp_set_object_terms( $image->pid, $tags, 'ngg_tag' ); } $updated[] = self::prepare_image_for_response( $image ); } catch ( \Exception $e ) { $errors[] = sprintf( // translators: %1$d is the image ID, %2$s is the error message. __( 'Failed to update image %1$d: %2$s', 'nggallery' ), $image_data['id'], $e->getMessage() ); } } return new WP_REST_Response( [ 'updated' => $updated, 'errors' => $errors, 'message' => sprintf( // translators: %1$d is the number of updated images, %2$d is the number of errors. __( 'Updated %1$d images with %2$d errors', 'nggallery' ), count( $updated ), count( $errors ) ), ], count( $errors ) > 0 ? 207 : 200 ); } /** * Import images from the WordPress media library into a NextGEN gallery. * * @param WP_REST_Request $request The request object containing gallery_id, gallery_name, and attachment_ids parameters. * @return WP_REST_Response */ public static function import_media_library( WP_REST_Request $request ) { // Image imports can require significant memory for decoding/resizing. // In REST requests, WordPress may still be running at WP_MEMORY_LIMIT (often low). if ( function_exists( 'wp_raise_memory_limit' ) ) { \wp_raise_memory_limit( 'image' ); } $retval = []; $created_gallery = false; $gallery_id = (int) $request->get_param( 'gallery_id' ); $gallery_name = $request->get_param( 'gallery_name' ); $attachment_ids = $request->get_param( 'attachment_ids' ); $gallery_mapper = \Imagely\NGG\DataMappers\Gallery::get_instance(); $image_mapper = \Imagely\NGG\DataMappers\Image::get_instance(); if ( empty( $attachment_ids ) || ! is_array( $attachment_ids ) ) { return new WP_REST_Response( [ 'error' => __( 'An unexpected error occurred.', 'nggallery' ), ], 400 ); } if ( 0 === $gallery_id ) { if ( ! empty( $gallery_name ) ) { $gallery = $gallery_mapper->create( [ 'title' => $gallery_name ] ); if ( ! $gallery->save() ) { return new WP_REST_Response( [ 'error' => $gallery->validate(), ], 400 ); } else { $created_gallery = true; $gallery_id = $gallery->id(); } } else { return new WP_REST_Response( [ 'error' => __( 'No gallery name specified', 'nggallery' ), ], 400 ); } } Transient::flush( 'rest_galleries' ); $retval['gallery_id'] = $gallery_id; $storage = \Imagely\NGG\DataStorage\Manager::get_instance(); $retval['image_ids'] = []; foreach ( $attachment_ids as $id ) { try { $abspath = get_attached_file( $id ); $file_data = file_get_contents( $abspath ); if ( empty( $file_data ) ) { return new WP_REST_Response( [ 'error' => __( 'Image generation failed', 'nggallery' ), ], 500 ); } $file_name = \Imagely\NGG\Display\I18N::mb_basename( $abspath ); $attachment = get_post( $id ); $image = $storage->upload_image( $gallery_id, $file_name, $file_data ); if ( $image ) { // Potentially import metadata from WordPress. $image = $image_mapper->find( $image ); if ( ! empty( $attachment->post_excerpt ) ) { $image->alttext = $attachment->post_excerpt; } if ( ! empty( $attachment->post_content ) ) { $image->description = $attachment->post_content; } $image = apply_filters( 'ngg_medialibrary_imported_image', $image, $attachment ); $image_mapper->save( $image ); $retval['image_ids'][] = $image->{$image->id_field}; } else { return new WP_REST_Response( [ 'error' => __( 'Image generation failed', 'nggallery' ), ], 500 ); } } catch ( \RuntimeException $ex ) { if ( $created_gallery ) { $gallery_mapper->destroy( $gallery_id ); } return new WP_REST_Response( [ 'error' => $ex->getMessage(), ], 500 ); } catch ( \Exception $ex ) { return new WP_REST_Response( [ 'error' => __( 'An unexpected error occurred.', 'nggallery' ), 'error_details' => $ex->getMessage(), ], 500 ); } } // If images were imported successfully, set the first one as preview if gallery has no preview if ( ! empty( $retval['image_ids'] ) ) { self::set_gallery_preview_if_empty( $gallery_id, $retval['image_ids'][0] ); } $retval['gallery_name'] = esc_html( $gallery_name ); return new WP_REST_Response( $retval ); } /** * Upload an image or zip file to a gallery. * * @param WP_REST_Request $request The request object containing gallery_id, gallery_name, and file parameters. * @return WP_REST_Response */ public static function upload_image( WP_REST_Request $request ) { // Image uploads can require significant memory for decoding/resizing. // In REST requests, WordPress may still be running at WP_MEMORY_LIMIT (often low). if ( function_exists( 'wp_raise_memory_limit' ) ) { \wp_raise_memory_limit( 'image' ); } $created_gallery = false; $gallery_id = (int) $request->get_param( 'gallery_id' ); $gallery_name = $request->get_param( 'gallery_name' ); $gallery_mapper = \Imagely\NGG\DataMappers\Gallery::get_instance(); $retval = [ 'gallery_name' => esc_html( $gallery_name ) ]; if ( ! class_exists( 'DOMDocument' ) ) { $retval['error'] = __( 'Please ask your hosting provider or system administrator to enable the PHP XML module which is required for image uploads', 'nggallery' ); return new WP_REST_Response( $retval, 500 ); } if ( 0 === $gallery_id ) { if ( ! empty( $gallery_name ) ) { $gallery = $gallery_mapper->create( [ 'title' => $gallery_name ] ); if ( ! $gallery->save() ) { $retval['error'] = $gallery->validate(); return new WP_REST_Response( $retval, 400 ); } else { $created_gallery = true; $gallery_id = $gallery->id(); } } else { $retval['error'] = __( 'No gallery name specified', 'nggallery' ); return new WP_REST_Response( $retval, 400 ); } } $retval['gallery_id'] = $gallery_id; $settings = \Imagely\NGG\Settings\Settings::get_instance(); $storage = \Imagely\NGG\DataStorage\Manager::get_instance(); try { // Pass true to skip nonce check since REST API has its own authentication if ( $storage->is_zip( true ) ) { $results = $storage->upload_zip( $gallery_id, true ); if ( $results ) { $retval = $results; // If ZIP uploaded successfully and has images, set preview if empty if ( ! empty( $results['image_ids'] ) && is_array( $results['image_ids'] ) ) { self::set_gallery_preview_if_empty( $gallery_id, $results['image_ids'][0] ); } } else { $retval['error'] = __( 'Failed to extract images from ZIP', 'nggallery' ); } // phpcs:ignore WordPress.Security.NonceVerification.Missing } elseif ( isset( $_FILES['file'] ) ) { $image_id = $storage->upload_image( $gallery_id ); if ( $image_id ) { $retval['image_ids'] = [ $image_id ]; // check if image was resized correctly. if ( $settings->get( 'imgAutoResize' ) ) { $image_path = $storage->get_full_abspath( $image_id ); $image_thumb = new \Imagely\NGG\DataTypes\LegacyThumbnail( $image_path, true ); if ( $image_thumb->error ) { // translators: %s is the error message. $retval['error'] = sprintf( __( 'Automatic image resizing failed [%1$s].', 'nggallery' ), $image_thumb->errmsg ); } } // check if thumb was generated correctly. $thumb_path = $storage->get_image_abspath( $image_id, 'thumb' ); if ( ! file_exists( $thumb_path ) ) { $retval['error'] = __( 'Thumbnail generation failed.', 'nggallery' ); } // If gallery has no preview image, set this as the preview self::set_gallery_preview_if_empty( $gallery_id, $image_id ); } else { $retval['error'] = __( 'Image generation failed', 'nggallery' ); } } else { $retval['error'] = __( 'No file uploaded', 'nggallery' ); } } catch ( \RuntimeException $ex ) { $retval['error'] = $ex->getMessage(); if ( $created_gallery ) { $gallery_mapper->destroy( $gallery_id ); } } catch ( \Exception $ex ) { // translators: %s is the error message. $retval['error'] = sprintf( __( 'An unexpected error occurred: %s', 'nggallery' ), $ex->getMessage() ); } $status = ! empty( $retval['error'] ) ? 500 : 200; if ( $status === 200 ) { Transient::flush( 'rest_galleries' ); } return new WP_REST_Response( $retval, $status ); } /** * Returns the absolute root path allowed for folder browsing and importing. * * On single-site this is NGG_IMPORT_ROOT (wp-content by default). * On multisite subsites the path is validated against the current blog's * upload directory so that one subsite cannot reach another's files. * * @return string | \WP_Error */ private static function get_import_root() { $root = is_multisite() ? \Imagely\NGG\DataStorage\Manager::get_instance()->get_upload_abspath() : NGG_IMPORT_ROOT; $root = str_replace( '/', DIRECTORY_SEPARATOR, $root ); $root = untrailingslashit( $root ); if ( is_multisite() && ! is_main_site() ) { $upload_dir = wp_upload_dir(); $blog_upload_base = trailingslashit( wp_normalize_path( realpath( $upload_dir['basedir'] ) ?: $upload_dir['basedir'] ) ); $resolved_root = wp_normalize_path( realpath( $root ) ?: $root ); $normalized_root = trailingslashit( $resolved_root ); if ( strpos( $normalized_root, $blog_upload_base ) !== 0 ) { return new \WP_Error( 'invalid_gallery_path', __( 'Access denied. You can only browse your own upload directory.', 'nggallery' ), [ 'status' => 403 ] ); } } return $root; } /** * Browse a folder for import. * * @param WP_REST_Request $request The request object containing dir parameter. * @return WP_REST_Response */ public static function browse_folder( WP_REST_Request $request ) { $retval = []; $dir = $request->get_param( 'dir' ); $root = self::get_import_root(); if ( is_wp_error( $root ) ) { return new WP_REST_Response( [ 'error' => $root->get_error_message() ], 403 ); } $browse_path = $root; if ( ! empty( $dir ) ) { $browse_path = $root . DIRECTORY_SEPARATOR . ltrim( $dir, DIRECTORY_SEPARATOR ); } // Use WP_Filesystem for directory listing. global $wp_filesystem; if ( ! $wp_filesystem ) { require_once ABSPATH . '/wp-admin/includes/file.php'; WP_Filesystem(); } if ( ! $wp_filesystem ) { $retval['error'] = __( 'Could not initialize WordPress filesystem API.', 'nggallery' ); return new WP_REST_Response( $retval, 500 ); } if ( ! $wp_filesystem->exists( $browse_path ) ) { $retval['error'] = __( 'Directory does not exist.', 'nggallery' ); return new WP_REST_Response( $retval, 404 ); } // Security: ensure path is within root using true prefix match; reject when realpath() fails (non-existent or symlink outside root). $real_browse = realpath( $browse_path ); $real_root = realpath( $root ); if ( false === $real_browse || false === $real_root || 0 !== strpos( trailingslashit( wp_normalize_path( $real_browse ) ), trailingslashit( wp_normalize_path( $real_root ) ) ) ) { $retval['error'] = __( 'No permissions to browse folders. Try refreshing the page or ensuring that your user account has sufficient roles/privileges.', 'nggallery' ); return new WP_REST_Response( $retval, 403 ); } $dirlist = $wp_filesystem->dirlist( $browse_path ); if ( ! is_array( $dirlist ) ) { $retval['error'] = __( 'Could not list directory contents.', 'nggallery' ); return new WP_REST_Response( $retval, 500 ); } $is_wp_content_path = untrailingslashit( WP_CONTENT_DIR ) === untrailingslashit( $browse_path ); $is_wp_content_path = apply_filters( 'imagely_is_wp_content_path', $is_wp_content_path ); $directories = []; foreach ( $dirlist as $name => $info ) { if ( 'd' !== $info['type'] ) { continue; } if ( $is_wp_content_path && in_array( $info['name'], [ 'plugins', 'upgrade', 'themes' ], true ) ) { continue; } $rel_file_path = ltrim( str_replace( $root, '', $browse_path . DIRECTORY_SEPARATOR . $name ), DIRECTORY_SEPARATOR ); $directories[] = [ 'name' => $name, 'relative_path' => $rel_file_path . DIRECTORY_SEPARATOR, ]; } $retval['directories'] = $directories; return new WP_REST_Response( $retval, 200 ); } /** * Import a folder as a gallery. * * @param WP_REST_Request $request The request object containing folder, keep_location, and gallery_title parameters. * @return WP_REST_Response */ public static function import_folder( WP_REST_Request $request ) { $retval = []; $folder = stripslashes( $request->get_param( 'folder' ) ); $keep_location = $request->get_param( 'keep_location' ) === true || $request->get_param( 'keep_location' ) === 'on'; $gallery_title = $request->get_param( 'gallery_title', null ); if ( empty( $gallery_title ) ) { $gallery_title = null; } $root = self::get_import_root(); if ( is_wp_error( $root ) ) { return new WP_REST_Response( [ 'error' => $root->get_error_message() ], 403 ); } $import_path = str_replace( '//', DIRECTORY_SEPARATOR, path_join( $root, $folder ) ); // First check if the path exists and is accessible. if ( ! file_exists( $import_path ) ) { return new WP_REST_Response( [ 'error' => sprintf( // translators: %s is the folder path. __( 'The folder "%s" does not exist.', 'nggallery' ), $folder ), ], 404 ); } if ( ! is_dir( $import_path ) ) { return new WP_REST_Response( [ 'error' => sprintf( // translators: %s is the folder path. __( 'The path "%s" is not a directory.', 'nggallery' ), $folder ), ], 400 ); } if ( ! is_readable( $import_path ) ) { return new WP_REST_Response( [ 'error' => sprintf( // translators: %s is the folder path. __( 'The folder "%s" is not readable. Please check permissions.', 'nggallery' ), $folder ), ], 403 ); } // Check if the folder is within the allowed root path (true prefix match; reject when realpath() fails to prevent traversal). $real_import = realpath( $import_path ); $real_root = realpath( $root ); if ( false === $real_import || false === $real_root || 0 !== strpos( trailingslashit( wp_normalize_path( $real_import ) ), trailingslashit( wp_normalize_path( $real_root ) ) ) ) { return new WP_REST_Response( [ 'error' => sprintf( // translators: %s is the folder path. __( 'The folder "%s" is outside of the allowed import root path.', 'nggallery' ), $folder ), ], 403 ); } $storage = \Imagely\NGG\DataStorage\Manager::get_instance(); try { $retval = $storage->import_gallery_from_fs( $import_path, false, ! $keep_location, $gallery_title ); if ( ! $retval ) { return new WP_REST_Response( [ 'error' => sprintf( // translators: %s is the folder path. __( 'No images were found in the folder "%s". Please ensure the folder contains supported image files (JPEG, PNG, GIF).', 'nggallery' ), $folder ), ], 400 ); } // If folder imported successfully with images, set the first one as preview if gallery has no preview if ( ! empty( $retval['gallery_id'] ) && ! empty( $retval['image_ids'] ) ) { self::set_gallery_preview_if_empty( $retval['gallery_id'], $retval['image_ids'][0] ); } } catch ( \Exception $ex ) { return new WP_REST_Response( [ 'error' => sprintf( // translators: %1$s is the folder path, %2$s is the error message. __( 'Failed to import folder "%1$s": %2$s', 'nggallery' ), $folder, $ex->getMessage() ), ], 500 ); } Transient::flush( 'rest_galleries' ); return new WP_REST_Response( $retval, 200 ); } /** * Prepare image data for API response. * * @param Image $image The image object to prepare for response. * @return array */ private static function prepare_image_for_response( $image ) { $tags = wp_get_object_terms( $image->pid, 'ngg_tag', [ 'fields' => 'names' ] ); // Enhance meta_data with live EXIF data if missing critical fields like Orientation $meta_data = $image->meta_data; if ( ! is_array( $meta_data ) ) { $meta_data = []; } // If Orientation is missing from stored metadata, try to get it from live EXIF if ( ! isset( $meta_data['Orientation'] ) || empty( $meta_data['Orientation'] ) ) { require_once NGGALLERY_ABSPATH . '/lib/meta.php'; $meta = new \nggMeta( $image ); $orientation = $meta->get_EXIF( 'Orientation' ); if ( $orientation ) { $meta_data['Orientation'] = $orientation; } } // Get properly formatted image URLs using the storage manager $storage = \Imagely\NGG\DataStorage\Manager::get_instance(); $thumb_url = $storage->get_image_url( $image, 'thumb' ); $image_url = $storage->get_image_url( $image, 'full' ); return [ 'pid' => $image->pid, 'filename' => $image->filename, 'description' => $image->description, 'alttext' => $image->alttext, 'image_slug' => $image->image_slug, 'galleryid' => $image->galleryid, 'meta_data' => $meta_data, 'post_id' => $image->post_id, 'imagedate' => $image->imagedate, 'exclude' => (bool) $image->exclude, 'sortorder' => (int) $image->sortorder, 'tags' => is_array( $tags ) ? $tags : [], 'updated_at' => $image->updated_at, 'extras_post_id' => $image->extras_post_id, 'pricelist_id' => isset( $image->pricelist_id ) ? (int) $image->pricelist_id : 0, 'thumb_url' => $thumb_url ?? '', 'image_url' => $image_url ?? '', ]; } /** * Set the gallery's preview image if it doesn't have one. * * @param int $gallery_id The gallery ID. * @param int $image_id The image ID to set as preview. * @return void */ private static function set_gallery_preview_if_empty( $gallery_id, $image_id ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery $current_preview = $wpdb->get_var( $wpdb->prepare( "SELECT previewpic FROM {$wpdb->nggallery} WHERE gid = %d", $gallery_id ) ); // Only set if gallery has no preview image (0 or empty) if ( empty( $current_preview ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->update( $wpdb->nggallery, [ 'previewpic' => (int) $image_id ], [ 'gid' => $gallery_id ], [ '%d' ], [ '%d' ] ); } } }
dvadf
dvadf
| ver. 1.4 |
Github
|
.
| PHP 7.3.33 | Generation time: 0 |
proxy
|
phpinfo
|
Settings