PluginProbe ʕ •ᴥ•ʔ
Pods – Custom Content Types and Fields / 2.8.23.4
Pods – Custom Content Types and Fields v2.8.23.4
2.7.31.4 2.8.23.5 2.9.19.5 3.0.10.5 3.1.4.3 3.2.8.4 3.3.9.2 2.8.23.4 2.9.19.4 3.0.10.4 3.1.4.2 3.2.8.3 3.3.9.1 trunk 1.14.8 2.7.31.3 2.8.23.3 2.9.19.3 3.0.10.3 3.1.4.1 3.2.0 3.2.1 3.2.1.1 3.2.2 3.2.4 3.2.5 3.2.6 3.2.7 3.2.7.1 3.2.8 3.2.8.1 3.2.8.2 3.3.0 3.3.1 3.3.2 3.3.3 3.3.4 3.3.5 3.3.6 3.3.7 3.3.8 3.3.9
pods / tribe-common / src / Tribe / Image / Uploader.php
pods / tribe-common / src / Tribe / Image Last commit date
Uploader.php 2 weeks ago
Uploader.php
320 lines
1 <?php
2
3
4 class Tribe__Image__Uploader {
5 /**
6 * @var bool|array
7 */
8 protected static $original_urls_cache = false;
9 /**
10 * @var bool|array
11 */
12 protected static $attachment_guids_cache = false;
13 /**
14 * @var string|int Either an absolute URL to an image file or a media attachment post ID.
15 */
16 protected $featured_image;
17
18 /**
19 * Tribe__Events__Importer__Featured_Image_Uploader constructor.
20 *
21 * @var string A single importing file row.
22 */
23 public function __construct( $featured_image = null ) {
24 $this->featured_image = $featured_image;
25 }
26
27 /**
28 * Resets the static "cache" of the class.
29 */
30 public static function reset_cache() {
31 self::$attachment_guids_cache = false;
32 self::$original_urls_cache = false;
33 }
34
35 /**
36 * Uploads a file and creates the media attachment or simply returns the attachment ID if existing.
37 *
38 * @return int|bool The attachment post ID if the uploading and attachment is successful or the ID refers to an
39 * attachment;
40 * `false` otherwise.
41 */
42 public function upload_and_get_attachment_id() {
43 if ( empty( $this->featured_image ) ) {
44 return false;
45 }
46
47 $existing = false;
48
49 if ( is_string( $this->featured_image ) && ! is_numeric( $this->featured_image ) ) {
50 // Assume image exists in the local file system.
51 $id = $this->get_attachment_ID_from_url( $this->featured_image );
52 if ( ! $id ) {
53 $id = $this->upload_file( $this->featured_image );
54 $id = $this->maybe_retry_upload( $id );
55 }
56 $existing = (bool) $id;
57 } elseif ( $post = get_post( $this->featured_image ) ) {
58 $id = $post && 'attachment' === $post->post_type ? $this->featured_image : false;
59 } else {
60 $id = false;
61 }
62
63 do_action(
64 'tribe_log',
65 'debug',
66 __CLASS__,
67 [
68 'featured_image' => $this->featured_image,
69 'exists' => $existing,
70 'id' => $id,
71 ]
72 );
73
74 return $id;
75 }
76
77 /**
78 * Retry to upload an image after it failed as was provided, try to decode the URL as in some cases the
79 * original URL might be encoded HTML components such as: "&" and some CDNs does not handle well different URLs
80 * as they were provided so we try to recreate the original URL where it might be required.
81 *
82 * @since 4.11.5
83 *
84 * @param int|bool $id The id of the attachment if was uploaded correctly, false otherwise.
85 *
86 * @return int The ID of the attachment after the upload retry.
87 */
88 protected function maybe_retry_upload( $id ) {
89 if ( $id ) {
90 do_action( 'tribe_log', 'debug', __CLASS__, [ 'message' => "ID: {$id} is already a valid one." ] );
91
92 return $id;
93 }
94
95 $decoded = esc_url_raw( html_entity_decode( $this->featured_image ) );
96
97 do_action( 'tribe_log', 'debug', __CLASS__, [
98 'message' => 'Retry upload decoding the URL of the image',
99 'url' => $this->featured_image,
100 'decoded' => $decoded,
101 ] );
102
103 // Maybe the URL was encoded and we need to convert it to a valid URL.
104 return $this->upload_file( $decoded );
105 }
106
107 /**
108 * @param string $file_url
109 *
110 * @return int
111 */
112 protected function upload_file( $file_url ) {
113 /**
114 * Allow plugins to enable local URL uploads, mainly used for testing.
115 *
116 * @since 4.9.5
117 *
118 * @param bool $allow_local_urls Whether to allow local URLs.
119 * @param string $file_url File URL.
120 */
121 $allow_local_urls = apply_filters( 'tribe_image_uploader_local_urls', false, $file_url );
122
123 if ( ! $allow_local_urls && ! filter_var( $file_url, FILTER_VALIDATE_URL ) ) {
124 return false;
125 }
126
127 // These files need to be included as dependencies
128 require_once( ABSPATH . 'wp-admin/includes/image.php' );
129 require_once( ABSPATH . 'wp-admin/includes/file.php' );
130 require_once( ABSPATH . 'wp-admin/includes/media.php' );
131
132 $is_local = false;
133 // This is a local file no need to fetch it from the wire.
134 if ( $allow_local_urls && file_exists( $file_url ) ) {
135 $file = $file_url;
136 $is_local = true;
137 } else {
138 /**
139 * Some CDN services will append query arguments to the image URL; removing
140 * them now has the potential of blocking the image fetching completely so we
141 * let them be here.
142 */
143 $file = download_url( $file_url );
144 if ( is_wp_error( $file ) ) {
145 do_action( 'tribe_log', 'error', __CLASS__, [
146 'message' => $file->get_error_message(),
147 'url' => $file_url,
148 'error' => $file,
149 ] );
150
151 return false;
152 }
153 }
154
155 // Upload file into WP and leave WP handle the resize and such.
156 $attachment_id = media_handle_sideload(
157 [
158 'name' => $this->create_file_name( $file ),
159 'tmp_name' => $file,
160 'post_mime_type' => 'image',
161 ],
162 0
163 );
164
165 // Remove the temporary file as is no longer required at this point.
166 if ( ! $is_local && file_exists( $file ) ) {
167 @unlink( $file );
168 }
169
170 if ( is_wp_error( $attachment_id ) ) {
171 do_action( 'tribe_log', 'error', __CLASS__, [
172 'message' => $attachment_id->get_error_message(),
173 'url' => $file_url,
174 'error' => $attachment_id,
175 ] );
176
177 return false;
178 }
179
180 update_post_meta( $attachment_id, '_tribe_importer_original_url', $file_url );
181
182 $this->maybe_init_attachment_guids_cache();
183 $this->maybe_init_attachment_original_urls_cache();
184
185 $attachment_post = get_post( $attachment_id );
186 // Only update the cache if is a valid attachment.
187 if ( $attachment_post instanceof WP_Post ) {
188 self::$attachment_guids_cache[ $attachment_post->guid ] = $attachment_id;
189 self::$original_urls_cache[ $file_url ] = $attachment_id;
190 }
191
192 return $attachment_id;
193 }
194
195 /**
196 * WordPress requires to have an extension in all all files as uses `wp_check_filetype` which uses the extension
197 * of the file to define if a file is valid or not, in this case the extension might not be present in some URLs of
198 * attachments or media files, in those cases we try to guess the right extension using the mime of the file as
199 * an alternative, if the $filename is a path we can verify the mime type using native WP functions.
200 *
201 * @since 4.11.5
202 *
203 * @param string $filename The name of the file or URL.
204 *
205 * @return string Returned a file name with an extension if is not already part of the file name.
206 */
207 protected function create_file_name( $filename ) {
208 /**
209 * We use the path basename only here to provided WordPress with a good filename
210 * that will allow it to correctly detect and validate the extension.
211 */
212 $path = wp_parse_url( $filename, PHP_URL_PATH );
213
214 $name = basename( $path );
215 $properties = wp_check_filetype( $name );
216
217 // Type can be defined from the name use that one instead.
218 if ( ! empty( $properties['type'] ) ) {
219 return $name;
220 }
221
222 // This is not a file that exists on the system, use the name instead.
223 if ( ! file_exists( $filename ) ) {
224 return $name;
225 }
226
227 $mime = wp_get_image_mime( $filename );
228
229 // There's no mime defined for the file use the plain name instead.
230 if ( $mime === '' ) {
231 return $name;
232 }
233
234 // create an array with the mimes as the keys and extensions as values.
235 $mime_to_extensions = array_flip( wp_get_mime_types() );
236
237 // No mime was found for the file on the array of allowed mime types, fallback to the name.
238 if ( ! isset( $mime_to_extensions[ $mime ] ) ) {
239 return $name;
240 }
241
242 // If there are more than one extension just ose one.
243 $parts = explode( '|', $mime_to_extensions[ $mime ] );
244
245 // Create a new name with extension.
246 return implode( '.', [ $name, reset( $parts ) ] );
247 }
248
249 protected function get_attachment_ID_from_url( $featured_image ) {
250 $this->maybe_init_attachment_guids_cache();
251 $this->maybe_init_attachment_original_urls_cache();
252
253 $guids_cache = self::$attachment_guids_cache;
254 $original_urls_cache = self::$original_urls_cache;
255 if ( isset( $guids_cache[ $featured_image ] ) ) {
256 return $guids_cache[ $featured_image ];
257 }
258
259 if ( isset( $original_urls_cache[ $featured_image ] ) ) {
260 return $original_urls_cache[ $featured_image ];
261 }
262
263 return false;
264 }
265
266 protected function maybe_init_attachment_guids_cache() {
267 if ( false === self::$attachment_guids_cache ) {
268 /** @var \wpdb $wpdb */
269 global $wpdb;
270 $guids = $wpdb->get_results( "SELECT ID, guid FROM $wpdb->posts where post_type = 'attachment'" );
271
272 if ( $guids ) {
273 $keys = wp_list_pluck( $guids, 'guid' );
274 $values = wp_list_pluck( $guids, 'ID' );
275 self::$attachment_guids_cache = array_combine( $keys, $values );
276 } else {
277 self::$attachment_guids_cache = [];
278 }
279 }
280 }
281
282 protected function maybe_init_attachment_original_urls_cache() {
283 if ( false === self::$original_urls_cache ) {
284 /** @var \wpdb $wpdb */
285 global $wpdb;
286 $original_urls = $wpdb->get_results( "
287 SELECT p.ID, pm.meta_value FROM $wpdb->posts p
288 JOIN $wpdb->postmeta pm
289 ON p.ID = pm.post_id
290 WHERE p.post_type = 'attachment'
291 AND pm.meta_key = '_tribe_importer_original_url'
292 " );
293
294 if ( $original_urls ) {
295 $keys = wp_list_pluck( $original_urls, 'meta_value' );
296 $values = wp_list_pluck( $original_urls, 'ID' );
297 self::$original_urls_cache = array_combine( $keys, $values );
298 } else {
299 self::$original_urls_cache = [];
300 }
301 }
302 }
303
304 /**
305 * Handles errors generated during the use of `file_get_contents` to
306 * make them run-time exceptions.
307 *
308 * @since 4.7.22
309 *
310 * @param string $unused_error_code The error numeric code.
311 * @param string $message The error message.
312 *
313 * @throws RuntimeException To pass the error as an exception to
314 * the handler.
315 */
316 public function handle_error( $unused_error_code, $message ) {
317 throw new RuntimeException( $message );
318 }
319 }
320