@@ -683,3 +683,189 @@ function webp_uploads_update_rest_attachment( WP_REST_Response $response, WP_Pos
683683 return rest_ensure_response ( $ data );
684684}
685685add_filter ( 'rest_prepare_attachment ' , 'webp_uploads_update_rest_attachment ' , 10 , 3 );
686+
687+ /**
688+ * Inspect if the current call to `wp_update_attachment_metadata()` was done from within the context
689+ * of an edit to an attachment either restore or other type of edit, in that case we perform operations
690+ * to save the sources properties, specifically for the `full` size image due this is a virtual image size.
691+ *
692+ * @since n.e.x.t
693+ *
694+ * @see wp_update_attachment_metadata()
695+ *
696+ * @param array $data The current metadata of the attachment.
697+ * @param int $attachment_id The ID of the current attachment.
698+ * @return array The updated metadata for the attachment to be stored in the meta table.
699+ */
700+ function webp_uploads_update_attachment_metadata ( $ data , $ attachment_id ) {
701+ $ trace = debug_backtrace ( DEBUG_BACKTRACE_IGNORE_ARGS , 10 );
702+
703+ foreach ( $ trace as $ element ) {
704+ if ( ! isset ( $ element ['function ' ] ) ) {
705+ continue ;
706+ }
707+
708+ switch ( $ element ['function ' ] ) {
709+ case 'wp_save_image ' :
710+ // Right after an image has been edited.
711+ return webp_uploads_backup_sources ( $ attachment_id , $ data );
712+ case 'wp_restore_image ' :
713+ // When an image has been restored.
714+ return webp_uploads_restore_image ( $ attachment_id , $ data );
715+ }
716+ }
717+
718+ return $ data ;
719+ }
720+ add_filter ( 'wp_update_attachment_metadata ' , 'webp_uploads_update_attachment_metadata ' , 10 , 2 );
721+
722+ /**
723+ * Before saving the metadata of the image store a backup values for the sources and file property
724+ * those files would be used and deleted by the backup mechanism, right after the metadata has
725+ * been updated. It removes the current sources property due once this function is executed
726+ * right after an edit has taken place and the current sources are no longer accurate.
727+ *
728+ * @since n.e.x.t
729+ *
730+ * @param int $attachment_id The ID representing the attachment.
731+ * @param array $data The current metadata of the attachment.
732+ * @return array The updated metadata for the attachment.
733+ */
734+ function webp_uploads_backup_sources ( $ attachment_id , $ data ) {
735+ $ target = isset ( $ _REQUEST ['target ' ] ) ? $ _REQUEST ['target ' ] : 'all ' ;
736+
737+ // When an edit to an image is only applied to a thumbnail there's nothing we need to back up.
738+ if ( 'thumbnail ' === $ target ) {
739+ return $ data ;
740+ }
741+
742+ $ metadata = wp_get_attachment_metadata ( $ attachment_id );
743+ // Nothing to back up.
744+ if ( ! isset ( $ metadata ['sources ' ] ) ) {
745+ return $ data ;
746+ }
747+
748+ $ sources = $ metadata ['sources ' ];
749+ // Prevent execution of the callbacks more than once if the callback was already executed.
750+ $ has_been_processed = false ;
751+
752+ $ hook = function ( $ meta_id , $ post_id , $ meta_name ) use ( $ attachment_id , $ sources , &$ has_been_processed ) {
753+ // Make sure this hook is only executed in the same context for the provided $attachment_id.
754+ if ( $ post_id !== $ attachment_id ) {
755+ return ;
756+ }
757+
758+ // This logic should work only if we are looking at the meta key: `_wp_attachment_backup_sizes`.
759+ if ( '_wp_attachment_backup_sizes ' !== $ meta_name ) {
760+ return ;
761+ }
762+
763+ if ( $ has_been_processed ) {
764+ return ;
765+ }
766+
767+ $ has_been_processed = true ;
768+ webp_uploads_backup_full_image_sources ( $ post_id , $ sources );
769+ };
770+
771+ add_action ( 'added_post_meta ' , $ hook , 10 , 3 );
772+ add_action ( 'updated_post_meta ' , $ hook , 10 , 3 );
773+
774+ // Remove the current sources as at this point the current values are no longer accurate.
775+ // TODO: Requires to be updated from https://github.com/WordPress/performance/issues/158.
776+ unset( $ data ['sources ' ] );
777+
778+ return $ data ;
779+ }
780+
781+ /**
782+ * Stores the provided sources for the attachment ID in the `_wp_attachment_backup_sources` with
783+ * the next available target if target is `null` no source would be stored.
784+ *
785+ * @since n.e.x.t
786+ *
787+ * @param int $attachment_id The ID of the attachment.
788+ * @param array $sources An array with the full sources to be stored on the next available key.
789+ */
790+ function webp_uploads_backup_full_image_sources ( $ attachment_id , $ sources ) {
791+ if ( empty ( $ sources ) ) {
792+ return ;
793+ }
794+
795+ $ target = webp_uploads_get_next_full_size_key_from_backup ( $ attachment_id );
796+ if ( null === $ target ) {
797+ return ;
798+ }
799+
800+ $ backup_sources = get_post_meta ( $ attachment_id , '_wp_attachment_backup_sources ' , true );
801+ $ backup_sources = is_array ( $ backup_sources ) ? $ backup_sources : array ();
802+ $ backup_sources [ $ target ] = $ sources ;
803+ // Store the `sources` property into the full size if present.
804+ update_post_meta ( $ attachment_id , '_wp_attachment_backup_sources ' , $ backup_sources );
805+ }
806+
807+ /**
808+ * It finds the next available `full-{orig or hash}` key on the images if the name
809+ * has not been used as part of the backup sources it would be used if no size is
810+ * found or backup exists `null` would be returned instead.
811+ *
812+ * @since n.e.x.t
813+ *
814+ * @param int $attachment_id The ID of the attachment.
815+ * @return null|string The next available full size name.
816+ */
817+ function webp_uploads_get_next_full_size_key_from_backup ( $ attachment_id ) {
818+ $ backup_sizes = get_post_meta ( $ attachment_id , '_wp_attachment_backup_sizes ' , true );
819+ $ backup_sizes = is_array ( $ backup_sizes ) ? $ backup_sizes : array ();
820+
821+ if ( empty ( $ backup_sizes ) ) {
822+ return null ;
823+ }
824+
825+ $ backup_sources = get_post_meta ( $ attachment_id , '_wp_attachment_backup_sources ' , true );
826+ $ backup_sources = is_array ( $ backup_sources ) ? $ backup_sources : array ();
827+ foreach ( array_keys ( $ backup_sizes ) as $ size_name ) {
828+ // If the target already has the sources attributes find the next one.
829+ if ( isset ( $ backup_sources [ $ size_name ] ) ) {
830+ continue ;
831+ }
832+
833+ // We are only interested in the `full-` sizes.
834+ if ( strpos ( $ size_name , 'full- ' ) === false ) {
835+ continue ;
836+ }
837+
838+ return $ size_name ;
839+ }
840+
841+ return null ;
842+ }
843+
844+ /**
845+ * Restore an image from the backup sizes, the current hook moves the `sources` from the `full-orig` key into
846+ * the top level `sources` into the metadata, in order to ensure the restore process has a reference to the right
847+ * images.
848+ *
849+ * @since n.e.x.t
850+ *
851+ * @param int $attachment_id The ID of the attachment.
852+ * @param array $data The current metadata to be stored in the attachment.
853+ * @return array The updated metadata of the attachment.
854+ */
855+ function webp_uploads_restore_image ( $ attachment_id , $ data ) {
856+ $ backup_sources = get_post_meta ( $ attachment_id , '_wp_attachment_backup_sources ' , true );
857+
858+ if ( ! is_array ( $ backup_sources ) ) {
859+ return $ data ;
860+ }
861+
862+ if ( ! isset ( $ backup_sources ['full-orig ' ] ) || ! is_array ( $ backup_sources ['full-orig ' ] ) ) {
863+ return $ data ;
864+ }
865+
866+ // TODO: Handle the case If `IMAGE_EDIT_OVERWRITE` is defined and is truthy remove any edited images if present before replacing the metadata.
867+ // See: https://github.com/WordPress/performance/issues/158.
868+ $ data ['sources ' ] = $ backup_sources ['full-orig ' ];
869+
870+ return $ data ;
871+ }
0 commit comments