├── .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 | 7 | 15 | 16 | 17 | 20 | 23 | 24 |
4 | Tachyon
5 | Faster than light image processing. Inspired / forked from Photon. 6 |
8 | 9 | Build status 10 | 11 | 12 | Coverage via codecov.io 13 | 14 |
18 | A Human Made project. Maintained by @joehoyle. 19 | 21 | 22 |
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 | --------------------------------------------------------------------------------