├── .gitignore
├── .travis.yml
├── README.md
├── composer.json
├── inc
└── class-tachyon.php
├── phpunit.xml.dist
├── tachyon.php
└── tests
├── bootstrap.php
├── data
├── tachyon-large.jpg
└── tachyon.jpg
├── install-tests.sh
└── tests
├── class-tests-linking.php
├── class-tests-resizing.php
└── class-tests-srcset.php
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # Travis CI Configuration File
2 |
3 | services:
4 | - docker
5 |
6 | cache:
7 | timeout: 1000
8 | directories:
9 | - vendor
10 |
11 | notifications:
12 | email: false
13 |
14 | before_script:
15 | - docker run --rm -v $PWD:/code --entrypoint='' humanmade/plugin-tester composer install
16 |
17 | script:
18 | - docker run --rm -v $PWD:/code humanmade/plugin-tester
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Tachyon
5 | Faster than light image processing. Inspired / forked from Photon.
6 | |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | |
15 |
16 |
17 |
18 | A Human Made project. Maintained by @joehoyle.
19 | |
20 |
21 |
22 | |
23 |
24 |
25 |
26 | [Tachyon](https://github.com/humanmade/tachyon) is an image resizing service built to be used with Amazon S3 as the image backend, AWS Lambda (or any node.js server) to process images using [sharp](http://sharp.pixelplumbing.com/en/stable/), and sits behind a CDN such as CloudFront or CloudFlare.
27 |
28 | This plugin handles modifying WordPress image URLs to use a Tachyon service instance.
29 |
30 | ## Installation
31 |
32 | 1. Upload and enable this plugin.
33 | 2. Add `define( 'TACHYON_URL', 'https://your.tachyon.url/path/to/uploads' )` to your `wp-config.php` file.
34 |
35 | ## Usage
36 |
37 | Typically the above steps are all you need to do however you can use the following public facing functions and filters.
38 |
39 | ### Functions
40 |
41 | #### `tachyon_url( string $image_url, array $args = [] )`
42 |
43 | This function returns the Tachyon URL for a given image hosted on Amazon S3.
44 |
45 | ```php
46 | $image_url = 'https://my-bucket.s3.us-east-1.amazonaws.com/path/to/image.jpg';
47 | $args = [
48 | 'resize' => '300,300',
49 | 'quality' => 90
50 | ];
51 |
52 | $url = tachyon_url( $image_url, $args );
53 | ```
54 |
55 | ### Filters
56 |
57 | The following filters allow you to modify the output and behaviour of the plugin.
58 |
59 | #### `tachyon_disable_in_admin`
60 |
61 | Defaults to `true`. You can override this by adding the following code to a plugin or your theme's `functions.php`:
62 |
63 | ```php
64 | add_filter( 'tachyon_disable_in_admin', '__return_false' );
65 | ```
66 |
67 | #### `tachyon_override_image_downsize`
68 |
69 | Defaults to `false`. Provides a way of preventing Tachyon from being applied to images retrieved from WordPress Core at the lowest level, you might use this if you wanted to use `tachyon_url()` manually in specific cases.
70 |
71 | ```php
72 | add_filter( 'tachyon_override_image_downsize', '__return_true' );
73 | ```
74 |
75 | #### `tachyon_skip_for_url`
76 |
77 | Allows skipping the Tachyon URL for a given image URL. Defaults to `false`.
78 |
79 | ```php
80 | add_filter( 'tachyon_skip_for_url', function ( $skip, $image_url, $args ) {
81 | if ( strpos( $image_url, 'original' ) !== false ) {
82 | return true;
83 | }
84 |
85 | return $skip;
86 | }, 10, 3 );
87 | ```
88 |
89 | #### `tachyon_pre_image_url`
90 |
91 | Filters the Tachyon image URL excluding the query string arguments. You might use this to shard Tachyon requests across multiple instances of the service for example.
92 |
93 | ```php
94 | add_filter( 'tachyon_pre_image_url', function ( $image_url, $args ) {
95 | if ( rand( 1, 2 ) === 2 ) {
96 | $image_url = str_replace( TACHYON_URL, TACHYON_URL_2, $image_url );
97 | }
98 |
99 | return $image_url;
100 | }, 10, 2 );
101 | ```
102 |
103 | #### `tachyon_pre_args`
104 |
105 | Filters the query string parameters appended to the tachyon image URL.
106 |
107 | ```php
108 | add_filter( 'tachyon_pre_args', function ( $args ) {
109 | if ( isset( $args['resize'] ) ) {
110 | $args['crop_strategy'] = 'smart';
111 | }
112 |
113 | return $args;
114 | } );
115 | ```
116 |
117 | #### `tachyon_remove_size_attributes`
118 |
119 | Defaults to `true`. `width` & `height` attributes on image tags are removed by default to prevent aspect ratio distortion that can happen in some unusual cases where `srcset` sizes have different aspect ratios.
120 |
121 |
122 | ```php
123 | add_filter( 'tachyon_remove_size_attributes', '__return_true' );
124 | ```
125 |
126 | ## Credits
127 | Created by Human Made for high volume and large-scale sites, such as [Happytables](http://happytables.com/). We run Tachyon on sites with millions of monthly page views, and thousands of sites.
128 |
129 | Written and maintained by [Joe Hoyle](https://github.com/joehoyle).
130 |
131 | Tachyon is forked from Photon by Automattic Inc. As Tachyon is not an all-purpose image resizer, rather it uses a media library in Amazon S3, it has a different use case to Photon.
132 |
133 | Interested in joining in on the fun? [Join us, and become human!](https://hmn.md/is/hiring/)
134 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "humanmade/tachyon-plugin",
3 | "description": "Rewrites WordPress image URLs to use Tachyon",
4 | "type": "wordpress-plugin",
5 | "license": "GPL-3.0",
6 | "authors": [
7 | {
8 | "name": "Joe Hoyle",
9 | "email": "joe@humanmade.com"
10 | }
11 | ],
12 | "require": {},
13 | "require-dev": {
14 | "phpunit/phpunit": "^7"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/inc/class-tachyon.php:
--------------------------------------------------------------------------------
1 | setup();
53 | }
54 |
55 | return self::$__instance;
56 | }
57 |
58 | /**
59 | * Silence is golden.
60 | */
61 | private function __construct() {}
62 |
63 | /**
64 | * Register actions and filters, but only if basic Tachyon functions are available.
65 | * The basic functions are found in ./functions.tachyon.php.
66 | *
67 | * @uses add_action, add_filter
68 | * @return null
69 | */
70 | private function setup() {
71 |
72 | if ( ! function_exists( 'tachyon_url' ) ) {
73 | return;
74 | }
75 |
76 | // Images in post content and galleries.
77 | add_filter( 'the_content', [ __CLASS__, 'filter_the_content' ], 999999 );
78 | add_filter( 'get_post_galleries', [ __CLASS__, 'filter_the_galleries' ], 999999 );
79 |
80 | // Core image retrieval.
81 | add_filter( 'image_downsize', [ $this, 'filter_image_downsize' ], 10, 3 );
82 | add_filter( 'rest_request_before_callbacks', [ $this, 'should_rest_image_downsize' ], 10, 3 );
83 | add_filter( 'rest_request_after_callbacks', [ $this, 'cleanup_rest_image_downsize' ] );
84 |
85 | // Responsive image srcset substitution.
86 | add_filter( 'wp_calculate_image_srcset', [ $this, 'filter_srcset_array' ], 10, 5 );
87 | }
88 |
89 | /**
90 | * * IN-CONTENT IMAGE MANIPULATION FUNCTIONS
91 | **/
92 |
93 | /**
94 | * Match all images and any relevant tags in a block of HTML.
95 | *
96 | * @param string $content Some HTML.
97 | * @return array An array of $images matches, where $images[0] is
98 | * an array of full matches, and the link_url, img_tag,
99 | * and img_url keys are arrays of those matches.
100 | */
101 | public static function parse_images_from_html( $content ) {
102 | $images = [];
103 |
104 | if ( preg_match_all( '#(?:]+?href=["|\'](?P[^\s]+?)["|\'][^>]*?>\s*)?(?P
]+?src=["|\'](?P[^\s]+?)["|\'].*?>){1}(?:\s*)?#is', $content, $images ) ) {
105 | foreach ( $images as $key => $unused ) {
106 | // Simplify the output as much as possible, mostly for confirming test results.
107 | if ( is_numeric( $key ) && $key > 0 ) {
108 | unset( $images[ $key ] );
109 | }
110 | }
111 |
112 | return $images;
113 | }
114 |
115 | return [];
116 | }
117 |
118 | /**
119 | * Try to determine height and width from strings WP appends to resized image filenames.
120 | *
121 | * @param string $src The image URL.
122 | * @return array An array consisting of width and height.
123 | */
124 | public static function parse_dimensions_from_filename( $src ) {
125 | $width_height_string = [];
126 |
127 | if ( preg_match( '#-(\d+)x(\d+)\.(?:' . implode( '|', self::$extensions ) . '){1}$#i', $src, $width_height_string ) ) {
128 | $width = (int) $width_height_string[1];
129 | $height = (int) $width_height_string[2];
130 |
131 | if ( $width && $height ) {
132 | return [ $width, $height ];
133 | }
134 | }
135 |
136 | return [ false, false ];
137 | }
138 |
139 | /**
140 | * Identify images in post content, and if images are local (uploaded to the current site), pass through Tachyon.
141 | *
142 | * @param string $content Post content.
143 | * @uses self::validate_image_url, apply_filters, tachyon_url, esc_url
144 | * @filter the_content
145 | * @return string
146 | */
147 | public static function filter_the_content( $content ) {
148 | $images = static::parse_images_from_html( $content );
149 |
150 | if ( ! empty( $images ) ) {
151 | $content_width = isset( $GLOBALS['content_width'] ) ? $GLOBALS['content_width'] : false;
152 |
153 | $image_sizes = self::image_sizes();
154 | $upload_dir = wp_upload_dir();
155 | $attachment_ids = [];
156 |
157 | foreach ( $images[0] as $tag ) {
158 | if ( preg_match( '/wp-image-([0-9]+)/i', $tag, $class_id ) && absint( $class_id[1] ) ) {
159 | // Overwrite the ID when the same image is included more than once.
160 | $attachment_ids[ $class_id[1] ] = true;
161 | }
162 | }
163 |
164 | if ( count( $attachment_ids ) > 1 ) {
165 | /*
166 | * Warm the object cache with post and meta information for all found
167 | * images to avoid making individual database calls.
168 | */
169 | _prime_post_caches( array_keys( $attachment_ids ), false, true );
170 | }
171 |
172 | foreach ( $images[0] as $index => $tag ) {
173 | // Default to resize, though fit may be used in certain cases where a dimension cannot be ascertained.
174 | $transform = 'resize';
175 |
176 | // Start with a clean size and attachment ID each time.
177 | $attachment_id = false;
178 | unset( $size );
179 |
180 | // Flag if we need to munge a fullsize URL.
181 | $fullsize_url = false;
182 |
183 | // Identify image source.
184 | $src = $src_orig = $images['img_url'][ $index ];
185 |
186 | /**
187 | * Allow specific images to be skipped by Tachyon.
188 | *
189 | * @since 2.0.3
190 | *
191 | * @param bool false Should Tachyon ignore this image. Default to false.
192 | * @param string $src Image URL.
193 | * @param string $tag Image Tag (Image HTML output).
194 | */
195 | if ( apply_filters( 'tachyon_skip_image', false, $src, $tag ) ) {
196 | continue;
197 | }
198 |
199 | // Support Automattic's Lazy Load plugin.
200 | // Can't modify $tag yet as we need unadulterated version later.
201 | if ( preg_match( '#data-lazy-src=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
202 | $placeholder_src = $placeholder_src_orig = $src;
203 | $src = $src_orig = $lazy_load_src[1];
204 | } elseif ( preg_match( '#data-lazy-original=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
205 | $placeholder_src = $placeholder_src_orig = $src;
206 | $src = $src_orig = $lazy_load_src[1];
207 | }
208 |
209 | // Check if image URL should be used with Tachyon.
210 | if ( self::validate_image_url( $src ) ) {
211 | // Find the width and height attributes.
212 | $width = $height = false;
213 |
214 | // First, check the image tag.
215 | if ( preg_match( '#width=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $width_string ) ) {
216 | $width = $width_string[1];
217 | }
218 |
219 | if ( preg_match( '#height=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $height_string ) ) {
220 | $height = $height_string[1];
221 | }
222 |
223 | // If image tag lacks width or height arguments, try to determine from strings WP appends to resized image filenames.
224 | if ( ! $width || ! $height ) {
225 | $size_from_file = static::parse_dimensions_from_filename( $src );
226 | $width = $width ?: $size_from_file[0];
227 | $height = $height ?: $size_from_file[1];
228 | }
229 |
230 | // Can't pass both a relative width and height, so unset the height in favor of not breaking the horizontal layout.
231 | if ( false !== strpos( $width, '%' ) && false !== strpos( $height, '%' ) ) {
232 | $width = $height = false;
233 | }
234 |
235 | // Detect WP registered image size from HTML class.
236 | if ( preg_match( '#class=["|\']?[^"\']*size-([^"\'\s]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $matches ) ) {
237 | $size = array_pop( $matches );
238 |
239 | if ( false === $width && false === $height && isset( $size ) && array_key_exists( $size, $image_sizes ) ) {
240 | $size_from_wp = wp_get_attachment_image_src( $attachment_id, $size );
241 | $width = $size_from_wp[1];
242 | $height = $size_from_wp[2];
243 | $transform = $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
244 | }
245 | }
246 |
247 | // WP Attachment ID, if uploaded to this site.
248 | if (
249 | preg_match( '#class=["|\']?[^"\']*wp-image-([\d]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $class_attachment_id ) &&
250 | (
251 | 0 === strpos( $src, $upload_dir['baseurl'] ) ||
252 | /**
253 | * Filter whether an image using an attachment ID in its class has to be uploaded to the local site to go through Tachyon.
254 | *
255 | * @since 2.0.3
256 | *
257 | * @param bool false Was the image uploaded to the local site. Default to false.
258 | * @param array $args {
259 | * Array of image details.
260 | *
261 | * @type $src Image URL.
262 | * @type tag Image tag (Image HTML output).
263 | * @type $images Array of information about the image.
264 | * @type $index Image index.
265 | * }
266 | */
267 | apply_filters( 'tachyon_image_is_local', false, compact( 'src', 'tag', 'images', 'index' ) )
268 | )
269 | ) {
270 | $class_attachment_id = intval( array_pop( $class_attachment_id ) );
271 |
272 | if ( $class_attachment_id ) {
273 | $attachment = get_post( $class_attachment_id );
274 | // Basic check on returned post object.
275 | if ( is_object( $attachment ) && ! is_wp_error( $attachment ) && 'attachment' === $attachment->post_type ) {
276 | $attachment_id = $attachment->ID;
277 |
278 | // If we still don't have a size for the image, use the attachment_id
279 | // to lookup the size for the image in the URL.
280 | if ( ! isset( $size ) ) {
281 | $meta = wp_get_attachment_metadata( $attachment_id );
282 | if ( $meta && $meta['sizes'] ) {
283 | $sizes = wp_list_filter( $meta['sizes'], [ 'file' => basename( $src ) ] );
284 | if ( $sizes ) {
285 | $size_names = array_keys( $sizes );
286 | $size = array_pop( $size_names );
287 | }
288 | }
289 | }
290 |
291 | // If we still don't have a size for the image but know the dimensions,
292 | // use the attachment sources to determine the size. Tachyon modifies
293 | // wp_get_attachment_image_src() to account for sizes created after upload.
294 | if ( ! isset( $size ) && $width && $height ) {
295 | $sizes = array_keys( $image_sizes );
296 | foreach ( $sizes as $size ) {
297 | $size_per_wp = wp_get_attachment_image_src( $attachment_id, $size );
298 | if ( $width === $size_per_wp[1] && $height === $size_per_wp[2] ) {
299 | $transform = $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
300 | break;
301 | }
302 | unset( $size ); // Prevent loop from polluting $size if it's incorrect.
303 | }
304 | }
305 |
306 | if ( isset( $size ) && false === $width && false === $height && array_key_exists( $size, $image_sizes ) ) {
307 | $width = (int) $image_sizes[ $size ]['width'];
308 | $height = (int) $image_sizes[ $size ]['height'];
309 | $transform = $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
310 | }
311 |
312 | /*
313 | * If size is still not set the dimensions were not provided by either
314 | * a class or by parsing the URL. Only the full sized image should return
315 | * no dimensions when returning the URL so it's safe to assume the $size is full.
316 | */
317 | $size = isset( $size ) ? $size : 'full';
318 |
319 | $src_per_wp = wp_get_attachment_image_src( $attachment_id, $size );
320 |
321 | if ( self::validate_image_url( $src_per_wp[0] ) ) {
322 | $src = $src_per_wp[0];
323 | $fullsize_url = true;
324 |
325 | // Prevent image distortion if a detected dimension exceeds the image's natural dimensions.
326 | if ( ( false !== $width && $width > $src_per_wp[1] ) || ( false !== $height && $height > $src_per_wp[2] ) ) {
327 | $width = false === $width ? false : min( $width, $src_per_wp[1] );
328 | $height = false === $height ? false : min( $height, $src_per_wp[2] );
329 | }
330 |
331 | // If no width and height are found, max out at source image's natural dimensions.
332 | // Otherwise, respect registered image sizes' cropping setting.
333 | if ( false === $width && false === $height ) {
334 | $width = $src_per_wp[1];
335 | $height = $src_per_wp[2];
336 | $transform = 'fit';
337 | } elseif ( isset( $size ) && array_key_exists( $size, $image_sizes ) && isset( $image_sizes[ $size ]['crop'] ) ) {
338 | $transform = (bool) $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
339 | }
340 | }
341 | } else {
342 | unset( $attachment );
343 | }
344 | }
345 | }
346 |
347 | // If width is available, constrain to $content_width.
348 | if ( false !== $width && false === strpos( $width, '%' ) && is_numeric( $content_width ) ) {
349 | if ( $width > $content_width && false !== $height && false === strpos( $height, '%' ) ) {
350 | $height = round( ( $content_width * $height ) / $width );
351 | $width = $content_width;
352 | } elseif ( $width > $content_width ) {
353 | $width = $content_width;
354 | }
355 | }
356 |
357 | // Set a width if none is found and $content_width is available.
358 | // If width is set in this manner and height is available, use `fit` instead of `resize` to prevent skewing.
359 | if ( false === $width && is_numeric( $content_width ) ) {
360 | $width = (int) $content_width;
361 |
362 | if ( false !== $height ) {
363 | $transform = 'fit';
364 | }
365 | }
366 |
367 | // Detect if image source is for a custom-cropped thumbnail and prevent further URL manipulation.
368 | if ( ! $fullsize_url && preg_match_all( '#-e[a-z0-9]+(-\d+x\d+)?\.(' . implode( '|', self::$extensions ) . '){1}$#i', basename( $src ), $filename ) ) {
369 | $fullsize_url = true;
370 | }
371 |
372 | // Build URL, first maybe removing WP's resized string so we pass the original image to Tachyon.
373 | if ( ! $fullsize_url ) {
374 | $src = self::strip_image_dimensions_maybe( $src );
375 | }
376 |
377 | // Build array of Tachyon args and expose to filter before passing to Tachyon URL function.
378 | $args = [];
379 |
380 | if ( false !== $width && false !== $height && false === strpos( $width, '%' ) && false === strpos( $height, '%' ) ) {
381 | if ( ! isset( $size ) || $size !== 'full' ) {
382 | $args[ $transform ] = $width . ',' . $height;
383 | }
384 |
385 | // Set the gravity from the registered image size.
386 | // Crop weight array values are in x, y order but the value `westsouth` will
387 | // cause Sharp to error and Tachyon to return a 404, it needs to be `southwest`
388 | // so we reverse the crop array to y, x order.
389 | if ( 'resize' === $transform && isset( $size ) && $size !== 'full' && array_key_exists( $size, $image_sizes ) && is_array( $image_sizes[ $size ]['crop'] ) ) {
390 | $args['gravity'] = implode( '', array_map( function ( $v ) {
391 | $map = [
392 | 'top' => 'north',
393 | 'center' => '',
394 | 'bottom' => 'south',
395 | 'left' => 'west',
396 | 'right' => 'east',
397 | ];
398 | return $map[ $v ];
399 | }, array_reverse( $image_sizes[ $size ]['crop'] ) ) );
400 | }
401 | } elseif ( false !== $width ) {
402 | $args['w'] = $width;
403 | } elseif ( false !== $height ) {
404 | $args['h'] = $height;
405 | }
406 |
407 | // Final logic check to determine the size for an unknown attachment ID.
408 | if ( ! isset( $size ) ) {
409 | if ( $width ) {
410 | $filter['width'] = $width;
411 | }
412 | if ( $height ) {
413 | $filter['height'] = $height;
414 | }
415 |
416 | if ( ! empty( $filter ) ) {
417 | $sizes = wp_list_filter( $image_sizes, $filter );
418 | if ( empty( $sizes ) ) {
419 | $sizes = wp_list_filter( $image_sizes, $filter, 'OR' );
420 | }
421 | if ( ! empty( $sizes ) ) {
422 | $size = reset( $sizes );
423 | }
424 | }
425 | }
426 |
427 | if ( ! isset( $size ) ) {
428 | // Custom size, send an array.
429 | $size = [ $width, $height ];
430 | }
431 |
432 | /**
433 | * Filter the array of Tachyon arguments added to an image when it goes through Tachyon.
434 | * By default, only includes width and height values.
435 | *
436 | * @see https://developer.wordpress.com/docs/photon/api/
437 | *
438 | * @param array $args Array of Tachyon Arguments.
439 | * @param array $args {
440 | * Array of image details.
441 | *
442 | * @type $tag Image tag (Image HTML output).
443 | * @type $src Image URL.
444 | * @type $src_orig Original Image URL.
445 | * @type $width Image width.
446 | * @type $height Image height.
447 | * @type $attachment_id Attachment ID.
448 | * }
449 | */
450 | $args = apply_filters( 'tachyon_post_image_args', $args, compact( 'tag', 'src', 'src_orig', 'width', 'height', 'attachment_id', 'size' ) );
451 |
452 | $tachyon_url = tachyon_url( $src, $args );
453 |
454 | // Modify image tag if Tachyon function provides a URL
455 | // Ensure changes are only applied to the current image by copying and modifying the matched tag, then replacing the entire tag with our modified version.
456 | if ( $src !== $tachyon_url ) {
457 | $new_tag = $tag;
458 |
459 | // If present, replace the link href with a Tachyoned URL for the full-size image.
460 | if ( ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
461 | $new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . tachyon_url( $images['link_url'][ $index ] ) . '\2', $new_tag, 1 );
462 | }
463 |
464 | // Supplant the original source value with our Tachyon URL.
465 | $tachyon_url = esc_url( $tachyon_url );
466 | $new_tag = str_replace( $src_orig, $tachyon_url, $new_tag );
467 |
468 | // If Lazy Load is in use, pass placeholder image through Tachyon.
469 | if ( isset( $placeholder_src ) && self::validate_image_url( $placeholder_src ) ) {
470 | $placeholder_src = tachyon_url( $placeholder_src );
471 |
472 | if ( $placeholder_src !== $placeholder_src_orig ) {
473 | $new_tag = str_replace( $placeholder_src_orig, esc_url( $placeholder_src ), $new_tag );
474 | }
475 |
476 | unset( $placeholder_src );
477 | }
478 |
479 | // Remove the width and height arguments from the tag to prevent distortion.
480 | if ( apply_filters( 'tachyon_remove_size_attributes', true ) ) {
481 | $new_tag = preg_replace( '#(?<=\s)(width|height)=["|\']?[\d%]+["|\']?\s?#i', '', $new_tag );
482 | }
483 |
484 | // Tag an image for dimension checking.
485 | $new_tag = preg_replace( '#(\s?/)?>(\s*)?$#i', ' data-recalc-dims="1"\1>\2', $new_tag );
486 |
487 | // Replace original tag with modified version.
488 | $content = str_replace( $tag, $new_tag, $content );
489 | }
490 | } elseif ( preg_match( '#^http(s)?://i[\d]{1}.wp.com#', $src ) && ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
491 | $new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . tachyon_url( $images['link_url'][ $index ] ) . '\2', $tag, 1 );
492 |
493 | $content = str_replace( $tag, $new_tag, $content );
494 | }
495 | }
496 | }
497 |
498 | return $content;
499 | }
500 |
501 | /**
502 | * Ensure galleries use Tachyon.
503 | *
504 | * @param array $galleries The post's galleries.
505 | * @return array
506 | */
507 | public static function filter_the_galleries( $galleries ) {
508 | if ( empty( $galleries ) || ! is_array( $galleries ) ) {
509 | return $galleries;
510 | }
511 |
512 | // Pass by reference, so we can modify them in place.
513 | foreach ( $galleries as &$this_gallery ) {
514 | if ( is_string( $this_gallery ) ) {
515 | $this_gallery = self::filter_the_content( $this_gallery );
516 | }
517 | }
518 | unset( $this_gallery ); // break the reference.
519 |
520 | return $galleries;
521 | }
522 |
523 | /**
524 | * * CORE IMAGE RETRIEVAL
525 | **/
526 |
527 | /**
528 | * Filter post thumbnail image retrieval, passing images through Tachyon.
529 | *
530 | * @param string|bool $image Image array.
531 | * @param int $attachment_id The attachment ID.
532 | * @param string|array $size The target image size.
533 | * @uses is_admin, apply_filters, wp_get_attachment_url, self::validate_image_url, this::image_sizes, tachyon_url
534 | * @filter image_downsize
535 | * @return string|bool
536 | */
537 | public function filter_image_downsize( $image, $attachment_id, $size ) {
538 | /**
539 | * Provide plugins a way of enable use of Tachyon in the admin context.
540 | *
541 | * @since 0.9.2
542 | *
543 | * @param bool true Disable the use of Tachyon in the admin.
544 | * @param array $args {
545 | * Array of image details.
546 | *
547 | * @type $image Image URL.
548 | * @type $attachment_id Attachment ID of the image.
549 | * @type $size Image size. Can be a string (name of the image size, e.g. full), integer or an array e.g. [ width, height ].
550 | * }
551 | */
552 | $disable_in_admin = is_admin() && apply_filters( 'tachyon_disable_in_admin', true, compact( 'image', 'attachment_id', 'size' ) );
553 |
554 | /**
555 | * Provide plugins a way of preventing Tachyon from being applied to images retrieved from WordPress Core.
556 | *
557 | * @since 0.9.2
558 | *
559 | * @param bool false Stop Tachyon from being applied to the image. Default to false.
560 | * @param array $args {
561 | * Array of image details.
562 | *
563 | * @type $image Image URL.
564 | * @type $attachment_id Attachment ID of the image.
565 | * @type $size Image size. Can be a string (name of the image size, e.g. full), integer or an array e.g. [ width, height ].
566 | * }
567 | */
568 | $override_image_downsize = apply_filters( 'tachyon_override_image_downsize', false, compact( 'image', 'attachment_id', 'size' ) );
569 |
570 | if ( $disable_in_admin || $override_image_downsize ) {
571 | return $image;
572 | }
573 |
574 | // Get the image URL and proceed with Tachyon-ification if successful.
575 | $image_url = wp_get_attachment_url( $attachment_id );
576 | $full_size_meta = wp_get_attachment_metadata( $attachment_id );
577 | $is_intermediate = false;
578 |
579 | if ( $image_url ) {
580 | // Check if image URL should be used with Tachyon.
581 | if ( ! self::validate_image_url( $image_url ) ) {
582 | return $image;
583 | }
584 |
585 | // If an image is requested with a size known to WordPress, use that size's settings with Tachyon.
586 | if ( ! empty( $full_size_meta ) && ( is_string( $size ) || is_int( $size ) ) && array_key_exists( $size, self::image_sizes() ) ) {
587 | $image_args = self::image_sizes();
588 | $image_args = $image_args[ $size ];
589 |
590 | $tachyon_args = [];
591 |
592 | $image_meta = image_get_intermediate_size( $attachment_id, $size );
593 |
594 | // 'full' is a special case: We need consistent data regardless of the requested size.
595 | if ( 'full' === $size ) {
596 | $image_meta = $full_size_meta;
597 | } elseif ( ! $image_meta ) {
598 | // If we still don't have any image meta at this point, it's probably from a custom thumbnail size
599 | // for an image that was uploaded before the custom image was added to the theme. Try to determine the size manually.
600 | $image_meta = $full_size_meta;
601 | if ( isset( $image_meta['width'] ) && isset( $image_meta['height'] ) ) {
602 | $image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $image_args['width'], $image_args['height'], $image_args['crop'] );
603 | if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
604 | $image_meta['width'] = $image_resized[6];
605 | $image_meta['height'] = $image_resized[7];
606 | $is_intermediate = true;
607 | }
608 | }
609 | } else {
610 | $is_intermediate = true;
611 | }
612 |
613 | // Expose determined arguments to a filter before passing to Tachyon.
614 | $transform = $image_args['crop'] ? 'resize' : 'fit';
615 |
616 | // If we can't get the width from the image size args, use the width of the
617 | // image metadata. We only do this is image_args['width'] is not set, because
618 | // we don't want to lose this data. $image_args is used as the Tachyon URL param
619 | // args, so we want to keep the original image sizes args. For example, if the image
620 | // size is 300x300px, non-cropped, we want to pass `fit=300,300` to Tachyon, instead
621 | // of say `resize=300,225`, because semantically, the image size is registered as
622 | // 300x300 un-cropped, not 300x225 cropped.
623 | if ( empty( $image_args['width'] ) && $transform !== 'resize' ) {
624 | $image_args['width'] = isset( $image_meta['width'] ) ? $image_meta['width'] : 0;
625 | }
626 |
627 | if ( empty( $image_args['height'] ) && $transform !== 'resize' ) {
628 | $image_args['height'] = isset( $image_meta['height'] ) ? $image_meta['height'] : 0;
629 | }
630 |
631 | // Prevent upscaling.
632 | $image_args['width'] = min( (int) $image_args['width'], (int) $full_size_meta['width'] );
633 | $image_args['height'] = min( (int) $image_args['height'], (int) $full_size_meta['height'] );
634 |
635 | // Respect $content_width settings.
636 | list( $width, $height ) = image_constrain_size_for_editor( $image_meta['width'], $image_meta['height'], $size, 'display' );
637 |
638 | // Check specified image dimensions and account for possible zero values; tachyon fails to resize if a dimension is zero.
639 | if ( ( 0 === $image_args['width'] || 0 === $image_args['height'] ) && $transform !== 'fit' ) {
640 | if ( 0 === $image_args['width'] && 0 < $image_args['height'] ) {
641 | $tachyon_args['h'] = $image_args['height'];
642 | } elseif ( 0 === $image_args['height'] && 0 < $image_args['width'] ) {
643 | $tachyon_args['w'] = $image_args['width'];
644 | }
645 | } else {
646 | // Fit accepts a zero value for either dimension so we allow that.
647 | // If resizing:
648 | // Both width & height are required, image args should be exact dimensions.
649 | if ( $transform === 'resize' ) {
650 | $image_args['width'] = $image_args['width'] ?: $width;
651 | $image_args['height'] = $image_args['height'] ?: $height;
652 | }
653 |
654 | $is_intermediate = ( $image_args['width'] < $full_size_meta['width'] || $image_args['height'] < $full_size_meta['height'] );
655 |
656 | // Add transform args if size is intermediate.
657 | if ( $is_intermediate ) {
658 | $tachyon_args[ $transform ] = $image_args['width'] . ',' . $image_args['height'];
659 | }
660 |
661 | if ( $is_intermediate && 'resize' === $transform && is_array( $image_args['crop'] ) ) {
662 | $tachyon_args['gravity'] = implode( '', array_map( function ( $v ) {
663 | $map = [
664 | 'top' => 'north',
665 | 'center' => '',
666 | 'bottom' => 'south',
667 | 'left' => 'west',
668 | 'right' => 'east',
669 | ];
670 | return $map[ $v ];
671 | }, array_reverse( $image_args['crop'] ) ) );
672 | }
673 | }
674 |
675 | /**
676 | * Filter the Tachyon Arguments added to an image when going through Tachyon, when that image size is a string.
677 | * Image size will be a string (e.g. "full", "medium") when it is known to WordPress.
678 | *
679 | * @param array $tachyon_args Array of Tachyon arguments.
680 | * @param array $args {
681 | * Array of image details.
682 | *
683 | * @type $image_args Array of Image arguments (width, height, crop).
684 | * @type $image_url Image URL.
685 | * @type $attachment_id Attachment ID of the image.
686 | * @type $size Image size. Can be a string (name of the image size, e.g. full) or an integer.
687 | * @type $transform Value can be resize or fit.
688 | * @see https://developer.wordpress.com/docs/photon/api
689 | * }
690 | */
691 | $tachyon_args = apply_filters( 'tachyon_image_downsize_string', $tachyon_args, compact( 'image_args', 'image_url', 'attachment_id', 'size', 'transform' ) );
692 |
693 | // Generate Tachyon URL.
694 | $image = [
695 | tachyon_url( $image_url, $tachyon_args ),
696 | $width,
697 | $height,
698 | $is_intermediate,
699 | ];
700 | } elseif ( is_array( $size ) ) {
701 | // Pull width and height values from the provided array, if possible.
702 | $width = isset( $size[0] ) ? (int) $size[0] : false;
703 | $height = isset( $size[1] ) ? (int) $size[1] : false;
704 |
705 | // Don't bother if necessary parameters aren't passed.
706 | if ( ! $width || ! $height ) {
707 | return $image;
708 | }
709 |
710 | $image_meta = wp_get_attachment_metadata( $attachment_id );
711 | if ( isset( $image_meta['width'] ) && isset( $image_meta['height'] ) ) {
712 | $image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $width, $height );
713 | if ( $image_resized ) {
714 | // Use the resized image dimensions.
715 | $width = $image_resized[6];
716 | $height = $image_resized[7];
717 | $is_intermediate = true;
718 | } else {
719 | // Resized image would be larger than original.
720 | $width = $image_meta['width'];
721 | $height = $image_meta['height'];
722 | }
723 | }
724 |
725 | list( $width, $height ) = image_constrain_size_for_editor( $width, $height, $size );
726 |
727 | $tachyon_args = [];
728 |
729 | // Expose arguments to a filter before passing to Tachyon.
730 | if ( $is_intermediate ) {
731 | $tachyon_args['fit'] = $width . ',' . $height;
732 | }
733 |
734 | /**
735 | * Filter the Tachyon Arguments added to an image when going through Tachyon,
736 | * when the image size is an array of height and width values.
737 | *
738 | * @param array $tachyon_args Array of Tachyon arguments.
739 | * @param array $args {
740 | * Array of image details.
741 | *
742 | * @type $width Image width.
743 | * @type height Image height.
744 | * @type $image_url Image URL.
745 | * @type $attachment_id Attachment ID of the image.
746 | * }
747 | */
748 | $tachyon_args = apply_filters( 'tachyon_image_downsize_array', $tachyon_args, compact( 'width', 'height', 'image_url', 'attachment_id' ) );
749 |
750 | // Generate Tachyon URL.
751 | $image = [
752 | tachyon_url( $image_url, $tachyon_args ),
753 | $width,
754 | $height,
755 | $is_intermediate,
756 | ];
757 | }
758 | }
759 |
760 | return $image;
761 | }
762 |
763 | /**
764 | * Filters an array of image `srcset` values, replacing each URL with its Tachyon equivalent.
765 | *
766 | * @since 3.8.0
767 | * @param array $sources An array of image urls and widths.
768 | * @param array $size_array List of sizes for the srcset.
769 | * @param string $image_src Current image URL.
770 | * @param array $image_meta Image meta data.
771 | * @param int $attachment_id The attachment ID.
772 | * @uses self::validate_image_url, tachyon_url
773 | * @return array An array of Tachyon image urls and widths.
774 | */
775 | public function filter_srcset_array( $sources, $size_array, $image_src, $image_meta, $attachment_id ) {
776 | $upload_dir = wp_upload_dir();
777 |
778 | foreach ( $sources as $i => $source ) {
779 | if ( ! self::validate_image_url( $source['url'] ) ) {
780 | continue;
781 | }
782 |
783 | $url = $source['url'];
784 | list( $width, $height ) = static::parse_dimensions_from_filename( $url );
785 |
786 | // It's quicker to get the full size with the data we have already, if available.
787 | if ( isset( $image_meta['file'] ) ) {
788 | $url = trailingslashit( $upload_dir['baseurl'] ) . $image_meta['file'];
789 | } else {
790 | $url = static::strip_image_dimensions_maybe( $url );
791 | }
792 |
793 | $args = [];
794 | if ( 'w' === $source['descriptor'] ) {
795 | if ( $height && ( intval( $source['value'] ) === intval( $width ) ) ) {
796 | $args['resize'] = $width . ',' . $height;
797 | } else {
798 | $args['w'] = $source['value'];
799 | }
800 | }
801 |
802 | // If the image_src is a tahcyon url, add it's params
803 | // to the srcset images too.
804 | if ( strpos( $image_src, TACHYON_URL ) === 0 ) {
805 | parse_str( parse_url( $image_src, PHP_URL_QUERY ) ?? '', $image_src_args );
806 | $args = array_merge( $args, array_intersect_key( $image_src_args, [ 'gravity' => true ] ) );
807 | }
808 |
809 | /**
810 | * Filter the array of Tachyon arguments added to an image when it goes through Tachyon.
811 | * By default, contains only resize or width params
812 | *
813 | * @param array $args Array of Tachyon Arguments.
814 | * @param array $args {
815 | * Array of image details.
816 | *
817 | * @type $source Array containing URL and target dimensions.
818 | * @type $image_meta Array containing attachment metadata.
819 | * @type $width Image width.
820 | * @type $height Image height.
821 | * @type $attachment_id Image ID.
822 | * }
823 | */
824 | $args = apply_filters( 'tachyon_srcset_image_args', $args, compact( 'source', 'image_meta', 'width', 'height', 'attachment_id' ) );
825 |
826 | $sources[ $i ]['url'] = tachyon_url( $url, $args );
827 | }
828 |
829 | return $sources;
830 | }
831 |
832 | /**
833 | * * GENERAL FUNCTIONS
834 | **/
835 |
836 | /**
837 | * Ensure image URL is valid for Tachyon.
838 | *
839 | * Though Tachyon functions address some of the URL issues, we should avoid unnecessary processing if we know early on that the image isn't supported.
840 | *
841 | * @param string $url An image URL.
842 | * @uses wp_parse_args
843 | * @return bool
844 | */
845 | public static function validate_image_url( $url ) {
846 | $parsed_url = @parse_url( $url );
847 |
848 | if ( ! $parsed_url ) {
849 | return false;
850 | }
851 |
852 | // only replace urls with supported file extensions.
853 | if ( ! in_array( strtolower( pathinfo( $parsed_url['path'], PATHINFO_EXTENSION ) ), static::$extensions, true ) ) {
854 | return false;
855 | }
856 |
857 | $upload_dir = wp_upload_dir();
858 | $upload_baseurl = $upload_dir['baseurl'];
859 |
860 | if ( is_multisite() ) {
861 | $upload_baseurl = preg_replace( '#/sites/[\d]+#', '', $upload_baseurl );
862 | }
863 |
864 | if ( strpos( $url, $upload_baseurl ) !== 0 ) {
865 | return false;
866 | }
867 |
868 | return apply_filters( 'tachyon_validate_image_url', true, $url, $parsed_url );
869 | }
870 |
871 | /**
872 | * Checks if the file exists before it passes the file to tachyon
873 | *
874 | * @param string $src The image URL.
875 | * @return string
876 | **/
877 | protected static function strip_image_dimensions_maybe( $src ) {
878 | // Build URL, first removing WP's resized string so we pass the original image to Tachyon.
879 | if ( preg_match( '#(-\d+x\d+)\.(' . implode( '|', self::$extensions ) . '){1}$#i', $src, $src_parts ) ) {
880 | $src = str_replace( $src_parts[1], '', $src );
881 | }
882 |
883 | return $src;
884 | }
885 |
886 | /**
887 | * Provide an array of available image sizes and corresponding dimensions.
888 | * Similar to get_intermediate_image_sizes() except that it includes image sizes' dimensions, not just their names.
889 | *
890 | * @global $wp_additional_image_sizes
891 | * @uses get_option
892 | * @return array
893 | */
894 | protected static function image_sizes() {
895 | if ( null === self::$image_sizes ) {
896 | global $_wp_additional_image_sizes;
897 |
898 | // Populate an array matching the data structure of $_wp_additional_image_sizes so we have a consistent structure for image sizes.
899 | $images = [
900 | 'thumb' => [
901 | 'width' => intval( get_option( 'thumbnail_size_w' ) ),
902 | 'height' => intval( get_option( 'thumbnail_size_h' ) ),
903 | 'crop' => (bool) get_option( 'thumbnail_crop' ),
904 | ],
905 | 'medium' => [
906 | 'width' => intval( get_option( 'medium_size_w' ) ),
907 | 'height' => intval( get_option( 'medium_size_h' ) ),
908 | 'crop' => false,
909 | ],
910 | 'medium_large' => [
911 | 'width' => intval( get_option( 'medium_large_size_w' ) ),
912 | 'height' => intval( get_option( 'medium_large_size_h' ) ),
913 | 'crop' => false,
914 | ],
915 | 'large' => [
916 | 'width' => intval( get_option( 'large_size_w' ) ),
917 | 'height' => intval( get_option( 'large_size_h' ) ),
918 | 'crop' => false,
919 | ],
920 | 'full' => [
921 | 'width' => null,
922 | 'height' => null,
923 | 'crop' => false,
924 | ],
925 | ];
926 |
927 | // Compatibility mapping as found in wp-includes/media.php.
928 | $images['thumbnail'] = $images['thumb'];
929 |
930 | // Update class variable, merging in $_wp_additional_image_sizes if any are set.
931 | if ( is_array( $_wp_additional_image_sizes ) && ! empty( $_wp_additional_image_sizes ) ) {
932 | self::$image_sizes = array_merge( $images, $_wp_additional_image_sizes );
933 | } else {
934 | self::$image_sizes = $images;
935 | }
936 | }
937 |
938 | return is_array( self::$image_sizes ) ? self::$image_sizes : [];
939 | }
940 |
941 | /**
942 | * Determine if image_downsize should utilize Tachyon via REST API.
943 | *
944 | * The WordPress Block Editor (Gutenberg) and other REST API consumers using the wp/v2/media endpoint, especially in the "edit"
945 | * context is more akin to the is_admin usage of Tachyon (see filter_image_downsize). Since consumers are trying to edit content in posts,
946 | * Tachyon should not fire as it will fire later on display. By aborting an attempt to change an image here, we
947 | * prevents issues like https://github.com/Automattic/jetpack/issues/10580
948 | *
949 | * To determine if we're using the wp/v2/media endpoint, we hook onto the `rest_request_before_callbacks` filter and
950 | * if determined we are using it in the edit context, we'll false out the `tachyon_override_image_downsize` filter.
951 | *
952 | * @author JetPack Photo / Automattic
953 | * @param null|WP_Error $response Result to send to the client. Usually a WP_REST_Response or WP_Error.
954 | * @param array $endpoint_data Route handler used for the request.
955 | * @param WP_REST_Request $request Request used to generate the response.
956 | *
957 | * @return null|WP_Error The original response object without modification.
958 | */
959 | public function should_rest_image_downsize( $response, $endpoint_data, $request ) {
960 | if ( ! is_a( $request, 'WP_REST_Request' ) ) {
961 | return $response; // Something odd is happening. Do nothing and return the response.
962 | }
963 |
964 | if ( is_wp_error( $response ) ) {
965 | // If we're going to return an error, we don't need to do anything with Photon.
966 | return $response;
967 | }
968 |
969 | $route = $request->get_route();
970 |
971 | if ( false !== strpos( $route, 'wp/v2/media' ) && 'edit' === $request['context'] ) {
972 | // Don't use `__return_true()`: Use something unique. See ::_override_image_downsize_in_rest_edit_context()
973 | // Late execution to avoid conflict with other plugins as we really don't want to run in this situation.
974 | add_filter( 'tachyon_override_image_downsize', [ $this, '_override_image_downsize_in_rest_edit_context' ], 999999 );
975 | }
976 |
977 | return $response;
978 | }
979 |
980 | /**
981 | * Remove the override we may have added in ::should_rest_image_downsize()
982 | * Since ::_override_image_downsize_in_rest_edit_context() is only
983 | * every used here, we can always remove it without ever worrying
984 | * about breaking any other configuration.
985 | *
986 | * @param mixed $response The result to send to the client.
987 | * @return mixed Unchanged $response
988 | */
989 | public function cleanup_rest_image_downsize( $response ) {
990 | remove_filter( 'tachyon_override_image_downsize', [ $this, '_override_image_downsize_in_rest_edit_context' ], 999999 );
991 | return $response;
992 | }
993 |
994 | /**
995 | * Used internally by ::should_rest_image_downsize() to not tachyonize
996 | * image URLs in ?context=edit REST requests.
997 | * MUST NOT be used anywhere else.
998 | * We use a unique function instead of __return_true so that we can clean up
999 | * after ourselves without breaking anyone else's filters.
1000 | *
1001 | * @internal
1002 | * @return true
1003 | */
1004 | public function _override_image_downsize_in_rest_edit_context() {
1005 | return true;
1006 | }
1007 | }
1008 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 | tests
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tachyon.php:
--------------------------------------------------------------------------------
1 | '300', 'resize' => array( 123, 456 ) ), or in string form (w=123&h=456).
32 | * @param string|null $scheme One of http or https.
33 | * @return string The raw final URL. You should run this through esc_url() before displaying it.
34 | */
35 | function tachyon_url( $image_url, $args = [], $scheme = null ) {
36 |
37 | $upload_dir = wp_upload_dir();
38 | $upload_baseurl = $upload_dir['baseurl'];
39 |
40 | if ( is_multisite() ) {
41 | $upload_baseurl = preg_replace( '#/sites/[\d]+#', '', $upload_baseurl );
42 | }
43 |
44 | $image_url = trim( $image_url );
45 |
46 | $image_file = basename( parse_url( $image_url, PHP_URL_PATH ) );
47 | $image_url = str_replace( $image_file, urlencode( $image_file ), $image_url );
48 |
49 | if ( strpos( $image_url, $upload_baseurl ) !== 0 ) {
50 | return $image_url;
51 | }
52 |
53 | if ( false !== apply_filters( 'tachyon_skip_for_url', false, $image_url, $args, $scheme ) ) {
54 | return $image_url;
55 | }
56 |
57 | $image_url = apply_filters( 'tachyon_pre_image_url', $image_url, $args, $scheme );
58 |
59 | // If the image URL has X-Amz params for signed requests, we need to add them to the Tachyon URL
60 | // under a `presign` param. However, only do this if we're on a version of TFA that supports it.
61 | if ( tachyon_server_version() && version_compare( tachyon_server_version(), '3.0.0', '>=' ) && stripos( $image_url, 'X-Amz-' ) !== false ) {
62 | $params = [];
63 | $presign = [];
64 | $query = parse_url( $image_url, PHP_URL_QUERY );
65 | parse_str( $query, $params );
66 | foreach ( $params as $key => $value ) {
67 | if ( stripos( $key, 'X-Amz-' ) === 0 ) {
68 | $presign[ $key ] = $value;
69 | $image_url = remove_query_arg( $key, $image_url );
70 | }
71 | }
72 | $image_url = add_query_arg( 'presign', urlencode( http_build_query( $presign ) ), $image_url );
73 | }
74 |
75 | $args = apply_filters( 'tachyon_pre_args', $args, $image_url, $scheme );
76 |
77 | $tachyon_url = str_replace( $upload_baseurl, TACHYON_URL, $image_url );
78 | if ( $args ) {
79 | if ( is_array( $args ) ) {
80 | // URL encode all param values, as this is not handled by add_query_arg.
81 | $tachyon_url = add_query_arg( array_map( 'rawurlencode', $args ), $tachyon_url );
82 | } else {
83 | // You can pass a query string for complicated requests but where you still want CDN subdomain help, etc.
84 | $tachyon_url .= '?' . $args;
85 | }
86 | }
87 |
88 | /**
89 | * Allows a final modification of the generated tachyon URL.
90 | *
91 | * @param string $tachyon_url The final tachyon image URL including query args.
92 | * @param string $image_url The image URL without query args.
93 | * @param array $args A key value array of the query args appended to $image_url.
94 | */
95 | return apply_filters( 'tachyon_url', $tachyon_url, $image_url, $args );
96 | }
97 |
98 | /**
99 | * Get the version of the Tachyon Server
100 | *
101 | * This is not always known, so it may return null.
102 | *
103 | * @return string|null
104 | */
105 | function tachyon_server_version() {
106 | $version = null;
107 | if ( defined( 'TACHYON_SERVER_VERSION' ) ) {
108 | $version = TACHYON_SERVER_VERSION;
109 | }
110 |
111 | return apply_filters( 'tachyon_server_version', $version );
112 | }
113 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | [db-host] [wp-version] [skip-database-creation]"
5 | exit 1
6 | fi
7 |
8 | DB_NAME=$1
9 | DB_USER=$2
10 | DB_PASS=$3
11 | DB_HOST=${4-localhost}
12 | WP_VERSION=${5-latest}
13 | SKIP_DB_CREATE=${6-false}
14 |
15 | WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib}
16 | WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/}
17 |
18 | download() {
19 | if [ `which curl` ]; then
20 | curl -s "$1" > "$2";
21 | elif [ `which wget` ]; then
22 | wget -nv -O "$2" "$1"
23 | fi
24 | }
25 |
26 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then
27 | WP_TESTS_TAG="tags/$WP_VERSION"
28 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
29 | WP_TESTS_TAG="trunk"
30 | else
31 | # http serves a single offer, whereas https serves multiple. we only want one
32 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json
33 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json
34 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//')
35 | if [[ -z "$LATEST_VERSION" ]]; then
36 | echo "Latest WordPress version could not be found"
37 | exit 1
38 | fi
39 | WP_TESTS_TAG="tags/$LATEST_VERSION"
40 | fi
41 |
42 | set -ex
43 |
44 | install_wp() {
45 |
46 | if [ -d $WP_CORE_DIR ]; then
47 | return;
48 | fi
49 |
50 | mkdir -p $WP_CORE_DIR
51 |
52 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
53 | mkdir -p /tmp/wordpress-nightly
54 | download https://wordpress.org/nightly-builds/wordpress-latest.zip /tmp/wordpress-nightly/wordpress-nightly.zip
55 | unzip -q /tmp/wordpress-nightly/wordpress-nightly.zip -d /tmp/wordpress-nightly/
56 | mv /tmp/wordpress-nightly/wordpress/* $WP_CORE_DIR
57 | else
58 | if [ $WP_VERSION == 'latest' ]; then
59 | local ARCHIVE_NAME='latest'
60 | else
61 | local ARCHIVE_NAME="wordpress-$WP_VERSION"
62 | fi
63 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz
64 | tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR
65 | fi
66 |
67 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
68 | }
69 |
70 | install_test_suite() {
71 | # portable in-place argument for both GNU sed and Mac OSX sed
72 | if [[ $(uname -s) == 'Darwin' ]]; then
73 | local ioption='-i .bak'
74 | else
75 | local ioption='-i'
76 | fi
77 |
78 | # set up testing suite if it doesn't yet exist
79 | if [ ! -d $WP_TESTS_DIR ]; then
80 | # set up testing suite
81 | mkdir -p $WP_TESTS_DIR
82 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
83 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data
84 | fi
85 |
86 | if [ ! -f wp-tests-config.php ]; then
87 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
88 | # remove all forward slashes in the end
89 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::")
90 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
91 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
92 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
93 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
94 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
95 | fi
96 |
97 | }
98 |
99 | install_db() {
100 |
101 | if [ ${SKIP_DB_CREATE} = "true" ]; then
102 | return 0
103 | fi
104 |
105 | # parse DB_HOST for port or socket references
106 | local PARTS=(${DB_HOST//\:/ })
107 | local DB_HOSTNAME=${PARTS[0]};
108 | local DB_SOCK_OR_PORT=${PARTS[1]};
109 | local EXTRA=""
110 |
111 | if ! [ -z $DB_HOSTNAME ] ; then
112 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then
113 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp"
114 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then
115 | EXTRA=" --socket=$DB_SOCK_OR_PORT"
116 | elif ! [ -z $DB_HOSTNAME ] ; then
117 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp"
118 | fi
119 | fi
120 |
121 | # create database
122 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA
123 | }
124 |
125 | install_wp
126 | install_test_suite
127 | install_db
128 |
--------------------------------------------------------------------------------
/tests/tests/class-tests-linking.php:
--------------------------------------------------------------------------------
1 | attachment->create_upload_object(
50 | realpath( __DIR__ . '/../data/tachyon.jpg' )
51 | );
52 | }
53 |
54 | /**
55 | * Runs the routine after all tests have been run.
56 | *
57 | * This deletes the files from the uploads directory
58 | * to account for the test suite returning the posts
59 | * table to the original state.
60 | */
61 | public static function wpTearDownAfterClass() {
62 | global $_wp_additional_image_sizes;
63 | $_wp_additional_image_sizes = self::$wp_additional_image_sizes;
64 |
65 | $singleton = Tachyon::instance(); // Get Tachyon instance.
66 | $reflection = new ReflectionClass( $singleton );
67 | $instance = $reflection->getProperty( 'image_sizes' );
68 | $instance->setAccessible( true ); // Allow modification of image sizes.
69 | $instance->setValue( null, null ); // Reset image sizes for next tests.
70 | $instance->setAccessible( false ); // clean up.
71 |
72 | $uploads_dir = wp_upload_dir()['basedir'];
73 |
74 | $files = glob( $uploads_dir . '/*' );
75 | array_walk( $files, function ( $file ) {
76 | if ( is_file( $file ) ) {
77 | unlink( $file );
78 | }
79 | } );
80 | rmdir( $uploads_dir );
81 | }
82 |
83 | /**
84 | * Extract the first src attribute from the given HTML.
85 | *
86 | * There should only ever be one image in the content so the regex
87 | * can be simplified to search for the source attribute only.
88 | *
89 | * @param string $html HTML containing an image tag.
90 | * @return string The first `src` attribute within the first image tag.
91 | */
92 | function get_src_from_html( $html ) {
93 | preg_match_all( '/src\s*=\s*[\'"]([^\'"]+)[\'"]/i', $html, $matches, PREG_SET_ORDER );
94 | if ( empty( $matches[0][1] ) ) {
95 | return false;
96 | }
97 |
98 | return $matches[0][1];
99 | }
100 |
101 | /**
102 | * Extract the first href attribute from the given HTML.
103 | *
104 | * There should only ever be one link in the content so the regex
105 | * can be simplified to search for the href attribute only.
106 | *
107 | * @param string $html HTML containing an image tag.
108 | * @return string The first `src` attribute within the first image tag.
109 | */
110 | function get_href_from_html( $html ) {
111 | preg_match_all( '/href\s*=\s*[\'"]([^\'"]+)[\'"]/i', $html, $matches, PREG_SET_ORDER );
112 | if ( empty( $matches[0][1] ) ) {
113 | return false;
114 | }
115 |
116 | return $matches[0][1];
117 | }
118 |
119 | /**
120 | * Test image tags passed as part of the content.
121 | *
122 | * @dataProvider data_content_filtering
123 | *
124 | * @param string $file Image file path.
125 | * @param string $content Post content.
126 | * @param array $valid_link_urls Valid outputs.
127 | * @param array $valid_src_urls Valid image sources.
128 | * @return void
129 | */
130 | function test_content_filtering( $file, $content, $valid_link_urls, $valid_src_urls ) {
131 | $valid_link_urls = (array) $valid_link_urls;
132 | $valid_src_urls = (array) $valid_src_urls;
133 | $attachment_id = self::$attachment_ids[ $file ];
134 | $content = str_replace( '%%ID%%', $attachment_id, $content );
135 | $content = str_replace( '%%BASE_URL%%', wp_upload_dir()['baseurl'], $content );
136 |
137 | $the_content = Tachyon::filter_the_content( $content );
138 | $actual_src = $this->get_src_from_html( $the_content );
139 | $actual_href = $this->get_href_from_html( $the_content );
140 |
141 | $this->assertContains( $actual_src, $valid_src_urls, 'The resized image is expected to be ' . implode( ' or ', $valid_src_urls ) );
142 | $this->assertContains( $actual_href, $valid_link_urls, 'The link is expected to be ' . implode( ' or ', $valid_link_urls ) );
143 | }
144 |
145 | /**
146 | * Data provider for test_content_filtering.
147 | *
148 | * Output:
149 | * return array[] {
150 | * $file string The basename of the uploaded file to tests against.
151 | * $content string The content being filtered.
152 | * `%%ID%%` is replaced with the attachment ID during the test.
153 | * `%%BASE_URL%%` is replaced with base uploads directory during the test.
154 | * $valid_urls array Valid Tachyon URLs for resizing.
155 | * }
156 | */
157 | function data_content_filtering() {
158 | return [
159 | // Classic editor linked thumbnail.
160 | [
161 | 'tachyon',
162 | '
',
163 | [
164 | 'http://tachy.on/u/tachyon.jpg',
165 | 'http://tachy.on/u/tachyon.jpg?resize=1280%2C719',
166 | ],
167 | [
168 | 'http://tachy.on/u/tachyon.jpg?resize=150%2C150',
169 | ],
170 | ],
171 | // Block editor linked thumbnail.
172 | [
173 | 'tachyon',
174 | '
',
175 | [
176 | 'http://tachy.on/u/tachyon.jpg',
177 | 'http://tachy.on/u/tachyon.jpg?resize=1280%2C719',
178 | ],
179 | [
180 | 'http://tachy.on/u/tachyon.jpg?resize=150%2C150',
181 | ],
182 | ],
183 | // Block editor gallery.
184 | [
185 | 'tachyon',
186 | '',
187 | [
188 | 'http://tachy.on/u/tachyon.jpg',
189 | 'http://tachy.on/u/tachyon.jpg?resize=1280%2C719',
190 | ],
191 | [
192 | 'http://tachy.on/u/tachyon.jpg?resize=1024%2C575',
193 | ],
194 | ],
195 | ];
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/tests/tests/class-tests-resizing.php:
--------------------------------------------------------------------------------
1 | attachment->create_upload_object(
52 | realpath( __DIR__ . '/../data/tachyon.jpg' )
53 | );
54 |
55 | self::$attachment_ids['tachyon-large'] = $factory->attachment->create_upload_object(
56 | realpath( __DIR__ . '/../data/tachyon-large.jpg' )
57 | );
58 | }
59 |
60 | /**
61 | * Runs the routine after all tests have been run.
62 | *
63 | * This deletes the files from the uploads directory
64 | * to account for the test suite returning the posts
65 | * table to the original state.
66 | */
67 | public static function wpTearDownAfterClass() {
68 | global $_wp_additional_image_sizes;
69 | $_wp_additional_image_sizes = self::$wp_additional_image_sizes;
70 |
71 | $singleton = Tachyon::instance(); // Get Tachyon instance.
72 | $reflection = new ReflectionClass( $singleton );
73 | $instance = $reflection->getProperty( 'image_sizes' );
74 | $instance->setAccessible( true ); // Allow modification of image sizes.
75 | $instance->setValue( null, null ); // Reset image sizes for next tests.
76 | $instance->setAccessible( false ); // clean up.
77 |
78 | $uploads_dir = wp_upload_dir()['basedir'];
79 |
80 | $files = glob( $uploads_dir . '/*' );
81 | array_walk( $files, function ( $file ) {
82 | if ( is_file( $file ) ) {
83 | unlink( $file );
84 | }
85 | } );
86 | rmdir( $uploads_dir );
87 | }
88 |
89 | /**
90 | * Set up tests.
91 | *
92 | * @return void
93 | */
94 | function setUp() {
95 | parent::setUp();
96 | self::setup_custom_sizes();
97 | }
98 |
99 | /**
100 | * Set up custom image sizes.
101 | *
102 | * These are done in both the class and per test set up as the sizes are
103 | * reset in the WP test suite's tearDown.
104 | *
105 | * Oversize, too tall/wide refer are references against the smaller image's
106 | * size, the larger image is always larger than the dimensions listed.
107 | */
108 | static function setup_custom_sizes() {
109 | add_image_size(
110 | 'oversize2d-early',
111 | 2500,
112 | 1500
113 | );
114 |
115 | add_image_size(
116 | 'too-wide-shorter-crop',
117 | 1500,
118 | 500,
119 | true
120 | );
121 |
122 | add_image_size(
123 | 'too-tall-narrower-crop',
124 | 1000,
125 | 1000,
126 | true
127 | );
128 |
129 | if ( ! empty( self::$attachment_ids ) ) {
130 | return;
131 | }
132 |
133 | add_image_size(
134 | 'oversize2d-late',
135 | 2000,
136 | 1000
137 | );
138 | }
139 |
140 | /**
141 | * Test URLs are parsed correctly.
142 | *
143 | * @dataProvider data_filtered_url
144 | *
145 | * @param string $file The image file path.
146 | * @param string|array $size Size name or array.
147 | * @param array $valid_urls Valid outputs.
148 | * @param array $expected_size Expected width and height.
149 | */
150 | function test_filtered_url( $file, $size, $valid_urls, $expected_size ) {
151 | $valid_urls = (array) $valid_urls;
152 | $actual_src = wp_get_attachment_image_src( self::$attachment_ids[ $file ], $size );
153 | $actual_url = $actual_src[0];
154 |
155 | $this->assertContains( $actual_url, $valid_urls, "The resized image is expected to be {$actual_src[1]}x{$actual_src[2]}" );
156 | $this->assertSame( $expected_size[0], $actual_src[1] );
157 | $this->assertSame( $expected_size[1], $actual_src[2] );
158 | }
159 |
160 | /**
161 | * Data provider for `test_filtered_url()`.
162 | *
163 | * Only the filename and querystring are stored as the
164 | *
165 | * return array[] {
166 | * $file string The basename of the uploaded file to tests against.
167 | * $size string The image size requested.
168 | * $valid_urls array Valid Tachyon URLs for resizing.
169 | * }
170 | */
171 | function data_filtered_url() {
172 | return [
173 | [
174 | 'tachyon',
175 | 'thumb',
176 | [
177 | 'http://tachy.on/u/tachyon.jpg?resize=150%2C150',
178 | ],
179 | [ 150, 150 ],
180 | ],
181 | [
182 | 'tachyon',
183 | 'thumbnail',
184 | [
185 | 'http://tachy.on/u/tachyon.jpg?resize=150%2C150',
186 | ],
187 | [ 150, 150 ],
188 | ],
189 | [
190 | 'tachyon',
191 | 'medium',
192 | [
193 | 'http://tachy.on/u/tachyon.jpg?fit=300%2C169',
194 | 'http://tachy.on/u/tachyon.jpg?resize=300%2C169',
195 | 'http://tachy.on/u/tachyon.jpg?fit=300%2C300',
196 | ],
197 | [ 300, 169 ],
198 | ],
199 | [
200 | 'tachyon',
201 | 'medium_large',
202 | [
203 | 'http://tachy.on/u/tachyon.jpg?fit=768%2C431',
204 | 'http://tachy.on/u/tachyon.jpg?resize=768%2C431',
205 | 'http://tachy.on/u/tachyon.jpg?w=768',
206 | 'http://tachy.on/u/tachyon.jpg?w=768&h=431',
207 | ],
208 | [ 768, 431 ],
209 | ],
210 | [
211 | 'tachyon',
212 | 'large',
213 | [
214 | 'http://tachy.on/u/tachyon.jpg?fit=1024%2C575',
215 | 'http://tachy.on/u/tachyon.jpg?fit=1024%2C719',
216 | 'http://tachy.on/u/tachyon.jpg?resize=1024%2C575',
217 | 'http://tachy.on/u/tachyon.jpg?fit=1024%2C1024',
218 | 'http://tachy.on/u/tachyon.jpg?w=1024&h=575',
219 | ],
220 | [ 1024, 575 ],
221 | ],
222 | [
223 | 'tachyon',
224 | 'full',
225 | [
226 | 'http://tachy.on/u/tachyon.jpg?fit=1280%2C719',
227 | 'http://tachy.on/u/tachyon.jpg?resize=1280%2C719',
228 | 'http://tachy.on/u/tachyon.jpg?w=1280',
229 | 'http://tachy.on/u/tachyon.jpg?w=1280&h=719',
230 | 'http://tachy.on/u/tachyon.jpg',
231 | ],
232 | [ 1280, 719 ],
233 | ],
234 | [
235 | 'tachyon',
236 | 'oversize2d-early',
237 | [
238 | 'http://tachy.on/u/tachyon.jpg?fit=1280%2C719',
239 | 'http://tachy.on/u/tachyon.jpg?resize=1280%2C719',
240 | 'http://tachy.on/u/tachyon.jpg?w=1280',
241 | 'http://tachy.on/u/tachyon.jpg?w=1280&h=719',
242 | 'http://tachy.on/u/tachyon.jpg',
243 | ],
244 | [ 1280, 719 ],
245 | ],
246 | [
247 | 'tachyon',
248 | 'oversize2d-late',
249 | [
250 | 'http://tachy.on/u/tachyon.jpg?fit=1280%2C719',
251 | 'http://tachy.on/u/tachyon.jpg?resize=1280%2C719',
252 | 'http://tachy.on/u/tachyon.jpg?w=1280',
253 | 'http://tachy.on/u/tachyon.jpg?w=1280&h=719',
254 | 'http://tachy.on/u/tachyon.jpg',
255 | ],
256 | [ 1280, 719 ],
257 | ],
258 | [
259 | 'tachyon',
260 | 'too-wide-shorter-crop',
261 | [
262 | 'http://tachy.on/u/tachyon.jpg?resize=1280%2C500',
263 | ],
264 | [ 1280, 500 ],
265 | ],
266 | [
267 | 'tachyon',
268 | 'too-tall-narrower-crop',
269 | [
270 | 'http://tachy.on/u/tachyon.jpg?resize=1000%2C719',
271 | ],
272 | [ 1000, 719 ],
273 | ],
274 | [
275 | 'tachyon',
276 | [ 1024, 1024 ], // Manual size, matches existing crop.
277 | [
278 | 'http://tachy.on/u/tachyon.jpg?fit=1024%2C575',
279 | 'http://tachy.on/u/tachyon.jpg?resize=1024%2C575',
280 | 'http://tachy.on/u/tachyon.jpg?fit=1024%2C1024',
281 | 'http://tachy.on/u/tachyon.jpg?w=1024&h=575',
282 | ],
283 | [ 1024, 575 ],
284 | ],
285 | [
286 | 'tachyon',
287 | [ 500, 300 ], // Manual size, new size, smaller than image, width limited.
288 | [
289 | 'http://tachy.on/u/tachyon.jpg?fit=500%2C281',
290 | 'http://tachy.on/u/tachyon.jpg?resize=500%2C281',
291 | 'http://tachy.on/u/tachyon.jpg?fit=500%2C300',
292 | 'http://tachy.on/u/tachyon.jpg?w=500&h=281',
293 | 'http://tachy.on/u/tachyon.jpg?w=500&h=300',
294 | ],
295 | [ 500, 281 ],
296 | ],
297 | [
298 | 'tachyon',
299 | [ 500, 30 ], // Manual size, new size, smaller than image, height limited.
300 | [
301 | 'http://tachy.on/u/tachyon.jpg?fit=53%2C30',
302 | 'http://tachy.on/u/tachyon.jpg?resize=53%2C30',
303 | 'http://tachy.on/u/tachyon.jpg?fit=500%2C30',
304 | 'http://tachy.on/u/tachyon.jpg?w=500&h=30',
305 | 'http://tachy.on/u/tachyon.jpg?w=53&h=30',
306 | ],
307 | [ 53, 30 ],
308 | ],
309 | [
310 | 'tachyon',
311 | [ 5000, 3000 ], // Manual size, new size, large than image, would be width limited.
312 | [
313 | 'http://tachy.on/u/tachyon.jpg?fit=1280%2C719',
314 | 'http://tachy.on/u/tachyon.jpg?resize=1280%2C719',
315 | 'http://tachy.on/u/tachyon.jpg?w=1280',
316 | 'http://tachy.on/u/tachyon.jpg?w=1280&h=719',
317 | 'http://tachy.on/u/tachyon.jpg',
318 | ],
319 | [ 1280, 719 ],
320 | ],
321 | [
322 | 'tachyon',
323 | [ 4000, 2000 ], // Manual size, new size, large than image, would be height limited.
324 | [
325 | 'http://tachy.on/u/tachyon.jpg?fit=1280%2C719',
326 | 'http://tachy.on/u/tachyon.jpg?resize=1280%2C719',
327 | 'http://tachy.on/u/tachyon.jpg?w=1280',
328 | 'http://tachy.on/u/tachyon.jpg?w=1280&h=719',
329 | 'http://tachy.on/u/tachyon.jpg',
330 | ],
331 | [ 1280, 719 ],
332 | ],
333 | [
334 | 'tachyon-large',
335 | 'thumb',
336 | [
337 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=150%2C150',
338 | ],
339 | [ 150, 150 ],
340 | ],
341 | [
342 | 'tachyon-large',
343 | 'thumbnail',
344 | [
345 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=150%2C150',
346 | ],
347 | [ 150, 150 ],
348 | ],
349 | [
350 | 'tachyon-large',
351 | 'medium',
352 | [
353 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=300%2C169',
354 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=300%2C169',
355 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=300%2C300',
356 | ],
357 | [ 300, 169 ],
358 | ],
359 | [
360 | 'tachyon-large',
361 | 'medium_large',
362 | [
363 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=768%2C432',
364 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=768%2C432',
365 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=768',
366 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=768&h=432',
367 | ],
368 | [ 768, 432 ],
369 | ],
370 | [
371 | 'tachyon-large',
372 | 'large',
373 | [
374 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=1024%2C576',
375 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=1024%2C576',
376 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=1024%2C1024',
377 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=1024&h=576',
378 | ],
379 | [ 1024, 576 ],
380 | ],
381 | [
382 | 'tachyon-large',
383 | 'full',
384 | [
385 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=2560%2C1440',
386 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=2560,1440',
387 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=2560',
388 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=2560&h=1440',
389 | 'http://tachy.on/u/tachyon-large-scaled.jpg',
390 | ],
391 | [ 2560, 1440 ],
392 | ],
393 | [
394 | 'tachyon-large',
395 | 'oversize2d-early',
396 | [
397 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=2500%2C1440',
398 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=2500%2C1406',
399 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=2500%2C1406',
400 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=2500%2C1500',
401 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=2500',
402 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=2500&h=1500',
403 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=2500&h=1406',
404 | ],
405 | [ 2500, 1406 ],
406 | ],
407 | [
408 | 'tachyon-large',
409 | 'oversize2d-late',
410 | [
411 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=1778%2C1000',
412 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=1778%2C1000',
413 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=2000%2C1000',
414 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=1778',
415 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=1778&h=1000',
416 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=2000&h=1000',
417 | ],
418 | [ 1778, 1000 ],
419 | ],
420 | [
421 | 'tachyon-large',
422 | 'too-wide-shorter-crop',
423 | [
424 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=1500%2C500',
425 | ],
426 | [ 1500, 500 ],
427 | ],
428 | [
429 | 'tachyon-large',
430 | 'too-tall-narrower-crop',
431 | [
432 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=1000%2C1000',
433 | ],
434 | [ 1000, 1000 ],
435 | ],
436 | ];
437 | }
438 |
439 | /**
440 | * Extract the first src attribute from the given HTML.
441 | *
442 | * There should only ever be one image in the content so the regex
443 | * can be simplified to search for the source attribute only.
444 | *
445 | * @param string $html HTML containing an image tag.
446 | * @return string The first `src` attribute within the first image tag.
447 | */
448 | function get_src_from_html( $html ) {
449 | preg_match_all( '/src\s*=\s*[\'"]([^\'"]+)[\'"]/i', $html, $matches, PREG_SET_ORDER );
450 | if ( empty( $matches[0][1] ) ) {
451 | return false;
452 | }
453 |
454 | return $matches[0][1];
455 | }
456 |
457 | /**
458 | * Test image tags passed as part of the content.
459 | *
460 | * @dataProvider data_content_filtering
461 | *
462 | * @param string $file Image file path.
463 | * @param string $content Post content.
464 | * @param array $valid_urls Valid outputs.
465 | * @return void
466 | */
467 | function test_content_filtering( $file, $content, $valid_urls ) {
468 | $valid_urls = (array) $valid_urls;
469 | $attachment_id = self::$attachment_ids[ $file ];
470 | $content = str_replace( '%%ID%%', $attachment_id, $content );
471 | $content = str_replace( '%%BASE_URL%%', wp_upload_dir()['baseurl'], $content );
472 |
473 | $the_content = Tachyon::filter_the_content( $content );
474 | $actual_src = $this->get_src_from_html( $the_content );
475 |
476 | $this->assertContains( $actual_src, $valid_urls, 'The resized image is expected to be ' . implode( ' or ', $valid_urls ) );
477 | }
478 |
479 | /**
480 | * Data provider for test_content_filtering.
481 | *
482 | * Output:
483 | * return array[] {
484 | * $file string The basename of the uploaded file to tests against.
485 | * $content string The content being filtered.
486 | * `%%ID%%` is replaced with the attachment ID during the test.
487 | * `%%BASE_URL%%` is replaced with base uploads directory during the test.
488 | * $valid_urls array Valid Tachyon URLs for resizing.
489 | * }
490 | */
491 | function data_content_filtering() {
492 | return [
493 | // Classic editor formatted image tags.
494 | [
495 | 'tachyon',
496 | '
',
497 | [
498 | 'http://tachy.on/u/tachyon.jpg?resize=150%2C150',
499 | ],
500 | ],
501 | [
502 | 'tachyon',
503 | '
',
504 | [
505 | 'http://tachy.on/u/tachyon.jpg?fit=300%2C169',
506 | 'http://tachy.on/u/tachyon.jpg?resize=300%2C169',
507 | 'http://tachy.on/u/tachyon.jpg?fit=300%2C300',
508 | ],
509 | ],
510 | [
511 | 'tachyon',
512 | '
',
513 | [
514 | 'http://tachy.on/u/tachyon.jpg?fit=1024%2C575',
515 | 'http://tachy.on/u/tachyon.jpg?resize=1024%2C575',
516 | 'http://tachy.on/u/tachyon.jpg?fit=1024%2C1024',
517 | 'http://tachy.on/u/tachyon.jpg?w=1024&h=1024',
518 | ],
519 | ],
520 | [
521 | 'tachyon',
522 | '
',
523 | [
524 | 'http://tachy.on/u/tachyon.jpg?fit=1280%2C719',
525 | 'http://tachy.on/u/tachyon.jpg?resize=1280%2C719',
526 | 'http://tachy.on/u/tachyon.jpg?w=1280',
527 | 'http://tachy.on/u/tachyon.jpg?w=1280&h=719',
528 | 'http://tachy.on/u/tachyon.jpg',
529 | ],
530 | ],
531 | [
532 | 'tachyon',
533 | '
',
534 | [
535 | 'http://tachy.on/u/tachyon.jpg?resize=1280%2C500',
536 | ],
537 | ],
538 | [
539 | 'tachyon',
540 | '
',
541 | [
542 | 'http://tachy.on/u/tachyon.jpg?resize=1000%2C719',
543 | ],
544 | ],
545 | [
546 | 'tachyon',
547 | '
',
548 | [
549 | 'http://tachy.on/u/tachyon.jpg?fit=1280%2C719',
550 | 'http://tachy.on/u/tachyon.jpg?resize=1280%2C719',
551 | 'http://tachy.on/u/tachyon.jpg?w=1280',
552 | 'http://tachy.on/u/tachyon.jpg?w=1280&h=719',
553 | 'http://tachy.on/u/tachyon.jpg',
554 | ],
555 | ],
556 | [
557 | 'tachyon-large',
558 | '
',
559 | [
560 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=150%2C150',
561 | ],
562 | ],
563 | [
564 | 'tachyon-large',
565 | '
',
566 | [
567 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=300%2C169',
568 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=300%2C169',
569 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=300%2C300',
570 | ],
571 | ],
572 | [
573 | 'tachyon-large',
574 | '
',
575 | [
576 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=1024%2C576',
577 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=1024%2C576',
578 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=1024%2C1024',
579 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=1024&h=576',
580 | ],
581 | ],
582 | [
583 | 'tachyon-large',
584 | '
',
585 | [
586 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=2560%2C1440',
587 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=2560%2C1440',
588 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=2560',
589 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=2560&h=1440',
590 | 'http://tachy.on/u/tachyon-large-scaled.jpg',
591 | ],
592 | ],
593 | [
594 | 'tachyon-large',
595 | '
',
596 | [
597 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=1778%2C1000',
598 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=1778%2C1000',
599 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=2000%2C1000',
600 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=1778',
601 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=1778&h=1000',
602 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=2000&h=1000',
603 | ],
604 | ],
605 | [
606 | 'tachyon-large',
607 | '
',
608 | [
609 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=1500%2C500',
610 | ],
611 | ],
612 | [
613 | 'tachyon-large',
614 | '
',
615 | [
616 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=1000%2C1000',
617 | ],
618 | ],
619 | // Block editor formatted image tags.
620 | [
621 | 'tachyon',
622 | '
',
623 | [
624 | 'http://tachy.on/u/tachyon.jpg?resize=150%2C150',
625 | ],
626 | ],
627 | [
628 | 'tachyon',
629 | '
',
630 | [
631 | 'http://tachy.on/u/tachyon.jpg?fit=300%2C169',
632 | 'http://tachy.on/u/tachyon.jpg?resize=300%2C169',
633 | 'http://tachy.on/u/tachyon.jpg?fit=300%2C300',
634 | ],
635 | ],
636 | [
637 | 'tachyon',
638 | '
',
639 | [
640 | 'http://tachy.on/u/tachyon.jpg?fit=1024%2C575',
641 | 'http://tachy.on/u/tachyon.jpg?resize=1024%2C575',
642 | 'http://tachy.on/u/tachyon.jpg?fit=1024%2C1024',
643 | 'http://tachy.on/u/tachyon.jpg?w=1024&h=1024',
644 | ],
645 | ],
646 | [
647 | 'tachyon',
648 | '
',
649 | [
650 | 'http://tachy.on/u/tachyon.jpg?fit=1280%2C719',
651 | 'http://tachy.on/u/tachyon.jpg?resize=1280%2C719',
652 | 'http://tachy.on/u/tachyon.jpg?w=1280',
653 | 'http://tachy.on/u/tachyon.jpg?w=1280&h=719',
654 | 'http://tachy.on/u/tachyon.jpg',
655 | ],
656 | ],
657 | [
658 | 'tachyon',
659 | '
',
660 | [
661 | 'http://tachy.on/u/tachyon.jpg?resize=1280%2C500',
662 | ],
663 | ],
664 | [
665 | 'tachyon',
666 | '
',
667 | [
668 | 'http://tachy.on/u/tachyon.jpg?resize=1000%2C719',
669 | ],
670 | ],
671 | [
672 | 'tachyon',
673 | '
',
674 | [
675 | 'http://tachy.on/u/tachyon.jpg?fit=1280%2C719',
676 | 'http://tachy.on/u/tachyon.jpg?resize=1280%2C719',
677 | 'http://tachy.on/u/tachyon.jpg?w=1280',
678 | 'http://tachy.on/u/tachyon.jpg?w=1280&h=719',
679 | 'http://tachy.on/u/tachyon.jpg',
680 | ],
681 | ],
682 | [
683 | 'tachyon-large',
684 | '
',
685 | [
686 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=150%2C150',
687 | ],
688 | ],
689 | [
690 | 'tachyon-large',
691 | '
',
692 | [
693 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=300%2C169',
694 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=300%2C169',
695 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=300%2C300',
696 | ],
697 | ],
698 | [
699 | 'tachyon-large',
700 | '
',
701 | [
702 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=1024%2C576',
703 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=1024%2C576',
704 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=1024%2C1024',
705 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=1024&h=1024',
706 | ],
707 | ],
708 | [
709 | 'tachyon-large',
710 | '
',
711 | [
712 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=2560%2C1440',
713 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=2560%2C1440',
714 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=2560',
715 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=2560&h=1440',
716 | 'http://tachy.on/u/tachyon-large-scaled.jpg',
717 | ],
718 | ],
719 | [
720 | 'tachyon-large',
721 | '
',
722 | [
723 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=1500%2C500',
724 | ],
725 | ],
726 | [
727 | 'tachyon-large',
728 | '
',
729 | [
730 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=1000%2C1000',
731 | ],
732 | ],
733 | [
734 | 'tachyon-large',
735 | '
',
736 | [
737 | 'http://tachy.on/u/tachyon-large-scaled.jpg?fit=1778%2C1000',
738 | 'http://tachy.on/u/tachyon-large-scaled.jpg?resize=1778%2C1000',
739 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=1778',
740 | 'http://tachy.on/u/tachyon-large-scaled.jpg?w=1778&h=1000',
741 | ],
742 | ],
743 | // Unknown attachement ID, unknown size, classic editor formatted image tags.
744 | [
745 | 'tachyon',
746 | '
',
747 | [
748 | 'http://tachy.on/u/tachyon.jpg?resize=150%2C150',
749 | ],
750 | ],
751 | ];
752 | }
753 | }
754 |
--------------------------------------------------------------------------------
/tests/tests/class-tests-srcset.php:
--------------------------------------------------------------------------------
1 | attachment->create_upload_object(
50 | realpath( __DIR__ . '/../data/tachyon.jpg' )
51 | );
52 | }
53 |
54 | /**
55 | * Runs the routine after all tests have been run.
56 | *
57 | * This deletes the files from the uploads directory
58 | * to account for the test suite returning the posts
59 | * table to the original state.
60 | */
61 | public static function wpTearDownAfterClass() {
62 | global $_wp_additional_image_sizes;
63 | $_wp_additional_image_sizes = self::$wp_additional_image_sizes;
64 |
65 | $singleton = Tachyon::instance(); // Get Tachyon instance.
66 | $reflection = new ReflectionClass( $singleton );
67 | $instance = $reflection->getProperty( 'image_sizes' );
68 | $instance->setAccessible( true ); // Allow modification of image sizes.
69 | $instance->setValue( null, null ); // Reset image sizes for next tests.
70 | $instance->setAccessible( false ); // clean up.
71 |
72 | $uploads_dir = wp_upload_dir()['basedir'];
73 |
74 | $files = glob( $uploads_dir . '/*' );
75 | array_walk( $files, function ( $file ) {
76 | if ( is_file( $file ) ) {
77 | unlink( $file );
78 | }
79 | } );
80 | rmdir( $uploads_dir );
81 | }
82 |
83 | /**
84 | * Test URL encoding of srcset attributes.
85 | *
86 | * @return void
87 | */
88 | function test_image_srcset_encoding() {
89 | $srcset = wp_get_attachment_image_srcset( self::$attachment_ids['tachyon'] );
90 | $expected = 'http://tachy.on/u/tachyon.jpg?w=1280 1280w, http://tachy.on/u/tachyon.jpg?resize=300%2C169 300w, http://tachy.on/u/tachyon.jpg?resize=1024%2C575 1024w, http://tachy.on/u/tachyon.jpg?resize=768%2C431 768w';
91 | $this->assertEquals( $expected, $srcset );
92 | }
93 | }
94 |
--------------------------------------------------------------------------------