PluginProbe
Search & Replace Everything by WPCode – Find and Replace Media, Text, Links, and More / 1.0.8
Search & Replace Everything by WPCode – Find and Replace Media, Text, Links, and More v1.0.8
trunk 1.0.0 1.0.1 1.0.10 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9
search-replace-wpcode / includes / class-wsrw-image-replace.php

class-wsrw-image-replace.php in Search & Replace Everything by WPCode – Find and Replace Media, Text, Links, and More 1.0.8, at includes/class-wsrw-image-replace.php

399 lines 11.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * This class handles the main logic for replacing images.
4 *
5 * @package Search_Replace_WPCode
6 */
7
8 // Exit if accessed directly.
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 /**
14 * Class WSRW_Image_Replace
15 */
16 class WSRW_Image_Replace {
17
18 /**
19 * The old file path.
20 *
21 * @var string
22 */
23 protected $old_file_path;
24
25 /**
26 * WSRW_Image_Replace constructor.
27 */
28 public function __construct() {
29 add_action( 'rest_api_init', array( $this, 'register_routes' ) );
30
31 add_filter( 'attachment_fields_to_edit', array( $this, 'add_button_to_edit_media_modal_fields_area' ), 10, 2 );
32 add_filter( 'media_row_actions', array( $this, 'add_button_to_media_row_actions' ), 10, 2 );
33
34 add_filter( 'wp_get_attachment_image_src', array( $this, 'get_attachment_url' ), 10, 4 );
35
36 add_action( 'add_meta_boxes_attachment', array( $this, 'add_meta_boxes' ) );
37 }
38
39 /**
40 * Filters the attachment image source result.
41 *
42 * @param array|false $image {
43 * Array of image data, or boolean false if no image is available.
44 *
45 * @type string $0 Image source URL.
46 * @type int $1 Image width in pixels.
47 * @type int $2 Image height in pixels.
48 * @type bool $3 Whether the image is a resized image.
49 * }
50 *
51 * @param int $attachment_id Image attachment ID.
52 * @param string|int[] $size Requested image size. Can be any registered image size name, or
53 * an array of width and height values in pixels (in that order).
54 * @param bool $icon Whether the image should be treated as an icon.
55 *
56 * @since 4.3.0
57 */
58 public function get_attachment_url( $image, $attachment_id, $size, $icon ) {
59 if ( ! is_admin() && ( ! defined( 'REST_REQUEST' ) || ! REST_REQUEST ) ) {
60 return $image;
61 }
62 // Let's check if the attachment has been replaced using the _wsrw_replaced meta.
63 $replaced = get_post_meta( $attachment_id, '_wsrw_replaced', true );
64 // If the replaced timestamp is past 24h let's just skip this.
65 if ( ! empty( $replaced ) && $replaced < strtotime( '-1 day' ) ) {
66 return $image;
67 }
68
69 if ( is_array( $image ) ) {
70 $image[0] = add_query_arg( 'wsr', $replaced, $image[0] );
71 }
72
73 return $image;
74 }
75
76 /**
77 * Handle the image upload.
78 *
79 * @param WP_REST_Request $request The request object.
80 *
81 * @return WP_REST_Response
82 */
83 public function handle_image_upload( $request ) {
84 $files = $request->get_file_params();
85
86 $nonce = $request->get_header( 'X-WP-Nonce' );
87 if ( empty( $nonce ) ) {
88 $nonce = isset( $_POST['_wpnonce'] ) ? sanitize_key( $_POST['_wpnonce'] ) : '';
89 }
90
91 if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
92 return new WP_REST_Response(
93 array(
94 'success' => false,
95 'message' => esc_html__( 'Invalid nonce', 'search-replace-wpcode' ),
96 ),
97 403
98 );
99 }
100
101 if ( empty( $files['file'] ) ) {
102 return new WP_REST_Response(
103 array(
104 'success' => false,
105 'message' => esc_html__( 'No file uploaded', 'search-replace-wpcode' ),
106 ),
107 400
108 );
109 }
110 $file = $files['file'];
111 $media_id = isset( $_POST['media_id'] ) ? absint( $_POST['media_id'] ) : 0;
112
113 // Let's grab the path of the current image using the media_id.
114 $attachment = get_post( $media_id );
115 $old_file_path = get_attached_file( $media_id, true );
116
117 // Validate file type.
118 $original_mime_type = get_post_mime_type( $media_id );
119 $file_mime_type = $file['type'];
120
121 // Get allowed mime types.
122 $allowed_mime_types = get_allowed_mime_types();
123
124 // Check if the uploaded file type is allowed.
125 $is_allowed = false;
126 foreach ( $allowed_mime_types as $ext => $mime ) {
127 if ( $mime === $file_mime_type ) {
128 $is_allowed = true;
129 break;
130 }
131 }
132
133 if ( ! $is_allowed ) {
134 return new WP_REST_Response(
135 array(
136 'success' => false,
137 'message' => esc_html__( 'File type not allowed. Please upload a file with a supported format.', 'search-replace-wpcode' ),
138 ),
139 400
140 );
141 }
142
143 // For images, check if we're replacing an image with a non-image.
144 if ( strpos( $original_mime_type, 'image/' ) === 0 && strpos( $file_mime_type, 'image/' ) !== 0 ) {
145 return new WP_REST_Response(
146 array(
147 'success' => false,
148 'message' => esc_html__( 'You cannot replace an image with a non-image file. Please upload an image file.', 'search-replace-wpcode' ),
149 ),
150 200
151 );
152 }
153
154 // Let's first delete all the thumbnails for the old image.
155 $metadata = wp_get_attachment_metadata( $media_id );
156 $backup_sizes = get_post_meta( $media_id, '_wp_attachment_backup_sizes', true );
157 wp_delete_attachment_files( $media_id, $metadata, $backup_sizes, $old_file_path );
158
159 // Let's upload the new file in the same directory as the old file with the same exact name.
160 if ( ! function_exists( 'wp_handle_upload' ) ) {
161 require_once ABSPATH . 'wp-admin/includes/file.php';
162 }
163 $this->old_file_path = $old_file_path;
164 // Let's grab the original upload time from the post publish date.
165 $time = strtotime( $attachment->post_date );
166 // Time should be formatted in yyyy/mm so that we use the same directory.
167 $time = gmdate( 'Y/m', $time );
168 // Let's move the file to the new location using wp_handle_upload.
169 $move_file = wp_handle_upload(
170 $file,
171 array(
172 'test_form' => false,
173 'unique_filename_callback' => array( $this, 'unique_filename_callback' ),
174 ),
175 $time
176 );
177
178 if ( ! $move_file || isset( $move_file['error'] ) ) {
179 return new WP_REST_Response(
180 array(
181 'success' => false,
182 'message' => $move_file['error'] ?? esc_html__( 'An error occurred while uploading the file', 'search-replace-wpcode' ),
183 ),
184 200
185 );
186 }
187
188 $new_file_path = $move_file['file'];
189
190 // Let's make sure the media file is included before calling generate attachment metada.
191 require_once ABSPATH . 'wp-admin/includes/image.php';
192
193 add_filter( 'big_image_size_threshold', '__return_false' );
194 // Let's update the attachment metadata.
195 $attachment_data = wp_generate_attachment_metadata( $media_id, $new_file_path );
196 wp_update_attachment_metadata( $media_id, $attachment_data );
197
198 // Add post meta to mark this has been replaced.
199 update_post_meta( $media_id, '_wsrw_replaced', time() );
200
201 $new_media = wp_get_attachment_image_src( $media_id, 'large' );
202
203 $message = '<p>' . esc_html__( 'File uploaded successfully', 'search-replace-wpcode' ) . '</p>';
204
205 $message .= '<p><strong style="color:red;">' . esc_html__( 'Please note that the source file has been replaced. If you see the old file, please clear your browser cache.', 'search-replace-wpcode' ) . '</strong></p>';
206
207 $response = array(
208 'success' => true,
209 'message' => $message,
210 );
211
212 if ( wp_attachment_is_image( $media_id ) ) {
213 $response['image_url'] = $new_media[0] ?? wp_get_attachment_url( $media_id );
214 }
215
216 return new WP_REST_Response(
217 $response,
218 200
219 );
220 }
221
222 /**
223 * Register the REST API routes.
224 */
225 public function register_routes() {
226 register_rest_route(
227 'wsrw/v1',
228 '/upload-image',
229 array(
230 'methods' => 'POST',
231 'callback' => array( $this, 'handle_image_upload' ),
232 'permission_callback' => function () {
233 return current_user_can( 'upload_files' );
234 },
235 )
236 );
237 }
238
239 /**
240 * Get the new filename for the uploaded file.
241 *
242 * @param string $old_file_path The path to the old file.
243 * @param string $new_filename The new filename.
244 *
245 * @return string
246 */
247 protected function get_new_filename( $old_file_path, $new_filename ) {
248 // By default, use the original filename.
249 return basename( $old_file_path );
250 }
251
252 /**
253 * Get the file extension to use.
254 *
255 * @param string $old_file_path The path to the old file.
256 * @param string $new_file_path The path to the new file.
257 *
258 * @return string
259 */
260 protected function get_file_extension( $old_file_path, $new_file_path ) {
261 // Always use the original extension in base class.
262 return pathinfo( $old_file_path, PATHINFO_EXTENSION );
263 }
264
265 /**
266 * Add a button to the edit media modal fields area.
267 *
268 * @param array $form_fields The form fields.
269 * @param WP_Post $post The post object.
270 *
271 * @return array
272 */
273 public function add_button_to_edit_media_modal_fields_area( $form_fields, $post ) {
274 if ( ! $this->can_user_replace_image( $post ) ) {
275 return $form_fields;
276 }
277
278 if ( ! wp_attachment_is_image( $post ) ) {
279 return $form_fields;
280 }
281
282 $form_fields['wsrw-replace-button'] = array(
283 'label' => '',
284 'input' => 'html',
285 'html' => '<a href="' . esc_url( self::get_replace_page_url( $post ) ) . '" class="button-secondary button-large" title="' . esc_attr__( 'Replace the source file for this image', 'search-replace-wpcode' ) . '">' . esc_html_x( 'Replace Source File', 'action for a single image', 'search-replace-wpcode' ) . '</a>',
286 'show_in_modal' => true,
287 'show_in_edit' => false,
288 'helps' => esc_html__( 'Directly replace the original file\'s source without creating a duplicate.', 'search-replace-wpcode' ),
289 );
290
291 return $form_fields;
292 }
293
294 /**
295 * Get the URL for the replace page.
296 *
297 * @param WP_Post $post The post object.
298 *
299 * @return string
300 */
301 public static function get_replace_page_url( $post ) {
302 return wp_nonce_url( admin_url( 'admin.php?page=wsrw-search-replace&view=replace_media&media_id=' . $post->ID ), 'wsrw_replace_media' );
303 }
304
305 /**
306 * Add a button to the media row actions.
307 *
308 * @param array $actions The actions array.
309 * @param WP_Post $post The post object.
310 *
311 * @return array
312 */
313 public function add_button_to_media_row_actions( $actions, $post ) {
314 if ( ! $this->can_user_replace_image( $post ) ) {
315 return $actions;
316 }
317
318 $actions['wsrw-replace'] = '<a href="' . esc_url( self::get_replace_page_url( $post ) ) . '" title="' . esc_attr__( 'Replace the source file.', 'search-replace-wpcode' ) . '">' . esc_html_x( 'Replace Source File', 'action in the list of attachments', 'search-replace-wpcode' ) . '</a>';
319
320 return $actions;
321 }
322
323 /**
324 * Permissions check for a specific attachment
325 *
326 * @param WP_Post $post The post object to check for.
327 *
328 * @return bool
329 */
330 public function can_user_replace_image( $post ) {
331 return current_user_can( 'upload_files' ) && current_user_can( 'edit_post', $post->ID );
332 }
333
334 /**
335 * Add meta boxes for the replace media page.
336 *
337 * @param WP_Post $post The post object.
338 *
339 * @return void
340 */
341 public function add_meta_boxes( $post ) {
342 if ( ! $this->can_user_replace_image( $post ) ) {
343 return;
344 }
345
346 add_meta_box(
347 'wsrw-replace',
348 __( 'Replace Source File', 'search-replace-wpcode' ),
349 array(
350 $this,
351 'replace_meta_box',
352 ),
353 'attachment',
354 'side',
355 'low'
356 );
357 }
358
359 /**
360 * Output the meta box for the replace media page.
361 *
362 * @param WP_Post $post The post object.
363 *
364 * @return void
365 */
366 public function replace_meta_box( $post ) {
367 ?>
368 <p>
369 <a href="<?php echo esc_url( self::get_replace_page_url( $post ) ); ?>" class="button-secondary button-large"><?php esc_html_e( 'Replace Source File', 'search-replace-wpcode' ); ?></a>
370 </p>
371 <p>
372 <?php esc_html_e( 'Use the button above to begin the source file replacement process.', 'search-replace-wpcode' ); ?>
373 </p>
374 <?php
375 }
376
377 /**
378 * Override the unique filename callback so we override the original file.
379 *
380 * @param string $dir The directory path.
381 * @param string $filename The filename.
382 * @param string $ext The file extension.
383 *
384 * @return string
385 */
386 public function unique_filename_callback( $dir, $filename, $ext = '' ) {
387 if ( isset( $this->old_file_path ) ) {
388 $new_filename = $this->get_new_filename( $this->old_file_path, $filename );
389 $filename_without_ext = pathinfo( $new_filename, PATHINFO_FILENAME );
390 $new_ext = $this->get_file_extension( $this->old_file_path, $filename . $ext );
391
392 return $filename_without_ext . '.' . $new_ext;
393 } else {
394 return $filename;
395 }
396 }
397
398 }
399