PageRenderTime 53ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/wp-content/plugins/jetpack/class.photon.php

https://gitlab.com/hunt9310/ras
PHP | 945 lines | 465 code | 143 blank | 337 comment | 150 complexity | 59eb1fa5727521d7a93a677d692b2ced MD5 | raw file
  1. <?php
  2. class Jetpack_Photon {
  3. /**
  4. * Class variables
  5. */
  6. // Oh look, a singleton
  7. private static $__instance = null;
  8. // Allowed extensions must match http://code.trac.wordpress.org/browser/photon/index.php#L31
  9. protected static $extensions = array(
  10. 'gif',
  11. 'jpg',
  12. 'jpeg',
  13. 'png'
  14. );
  15. // Don't access this directly. Instead, use self::image_sizes() so it's actually populated with something.
  16. protected static $image_sizes = null;
  17. /**
  18. * Singleton implementation
  19. *
  20. * @return object
  21. */
  22. public static function instance() {
  23. if ( ! is_a( self::$__instance, 'Jetpack_Photon' ) ) {
  24. self::$__instance = new Jetpack_Photon;
  25. self::$__instance->setup();
  26. }
  27. return self::$__instance;
  28. }
  29. /**
  30. * Silence is golden.
  31. */
  32. private function __construct() {}
  33. /**
  34. * Register actions and filters, but only if basic Photon functions are available.
  35. * The basic functions are found in ./functions.photon.php.
  36. *
  37. * @uses add_action, add_filter
  38. * @return null
  39. */
  40. private function setup() {
  41. if ( ! function_exists( 'jetpack_photon_url' ) )
  42. return;
  43. // Images in post content and galleries
  44. add_filter( 'the_content', array( __CLASS__, 'filter_the_content' ), 999999 );
  45. add_filter( 'get_post_galleries', array( __CLASS__, 'filter_the_galleries' ), 999999 );
  46. // Core image retrieval
  47. add_filter( 'image_downsize', array( $this, 'filter_image_downsize' ), 10, 3 );
  48. // Responsive image srcset substitution
  49. add_filter( 'wp_calculate_image_srcset', array( $this, 'filter_srcset_array' ), 10, 4 );
  50. add_filter( 'wp_calculate_image_sizes', array( $this, 'filter_sizes' ), 1, 2 ); // Early so themes can still easily filter.
  51. // Helpers for maniuplated images
  52. add_action( 'wp_enqueue_scripts', array( $this, 'action_wp_enqueue_scripts' ), 9 );
  53. }
  54. /**
  55. ** IN-CONTENT IMAGE MANIPULATION FUNCTIONS
  56. **/
  57. /**
  58. * Match all images and any relevant <a> tags in a block of HTML.
  59. *
  60. * @param string $content Some HTML.
  61. * @return array An array of $images matches, where $images[0] is
  62. * an array of full matches, and the link_url, img_tag,
  63. * and img_url keys are arrays of those matches.
  64. */
  65. public static function parse_images_from_html( $content ) {
  66. $images = array();
  67. if ( preg_match_all( '#(?:<a[^>]+?href=["|\'](?P<link_url>[^\s]+?)["|\'][^>]*?>\s*)?(?P<img_tag><img[^>]*?\s+?src=["|\'](?P<img_url>[^\s]+?)["|\'].*?>){1}(?:\s*</a>)?#is', $content, $images ) ) {
  68. foreach ( $images as $key => $unused ) {
  69. // Simplify the output as much as possible, mostly for confirming test results.
  70. if ( is_numeric( $key ) && $key > 0 )
  71. unset( $images[$key] );
  72. }
  73. return $images;
  74. }
  75. return array();
  76. }
  77. /**
  78. * Try to determine height and width from strings WP appends to resized image filenames.
  79. *
  80. * @param string $src The image URL.
  81. * @return array An array consisting of width and height.
  82. */
  83. public static function parse_dimensions_from_filename( $src ) {
  84. $width_height_string = array();
  85. if ( preg_match( '#-(\d+)x(\d+)\.(?:' . implode('|', self::$extensions ) . '){1}$#i', $src, $width_height_string ) ) {
  86. $width = (int) $width_height_string[1];
  87. $height = (int) $width_height_string[2];
  88. if ( $width && $height )
  89. return array( $width, $height );
  90. }
  91. return array( false, false );
  92. }
  93. /**
  94. * Identify images in post content, and if images are local (uploaded to the current site), pass through Photon.
  95. *
  96. * @param string $content
  97. * @uses self::validate_image_url, apply_filters, jetpack_photon_url, esc_url
  98. * @filter the_content
  99. * @return string
  100. */
  101. public static function filter_the_content( $content ) {
  102. $images = Jetpack_Photon::parse_images_from_html( $content );
  103. if ( ! empty( $images ) ) {
  104. $content_width = Jetpack::get_content_width();
  105. $image_sizes = self::image_sizes();
  106. $upload_dir = wp_upload_dir();
  107. foreach ( $images[0] as $index => $tag ) {
  108. // Default to resize, though fit may be used in certain cases where a dimension cannot be ascertained
  109. $transform = 'resize';
  110. // Start with a clean attachment ID each time
  111. $attachment_id = false;
  112. // Flag if we need to munge a fullsize URL
  113. $fullsize_url = false;
  114. // Identify image source
  115. $src = $src_orig = $images['img_url'][ $index ];
  116. /**
  117. * Allow specific images to be skipped by Photon.
  118. *
  119. * @module photon
  120. *
  121. * @since 2.0.3
  122. *
  123. * @param bool false Should Photon ignore this image. Default to false.
  124. * @param string $src Image URL.
  125. * @param string $tag Image Tag (Image HTML output).
  126. */
  127. if ( apply_filters( 'jetpack_photon_skip_image', false, $src, $tag ) )
  128. continue;
  129. // Support Automattic's Lazy Load plugin
  130. // Can't modify $tag yet as we need unadulterated version later
  131. if ( preg_match( '#data-lazy-src=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
  132. $placeholder_src = $placeholder_src_orig = $src;
  133. $src = $src_orig = $lazy_load_src[1];
  134. } elseif ( preg_match( '#data-lazy-original=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
  135. $placeholder_src = $placeholder_src_orig = $src;
  136. $src = $src_orig = $lazy_load_src[1];
  137. }
  138. // Check if image URL should be used with Photon
  139. if ( self::validate_image_url( $src ) ) {
  140. // Find the width and height attributes
  141. $width = $height = false;
  142. // First, check the image tag
  143. if ( preg_match( '#width=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $width_string ) )
  144. $width = $width_string[1];
  145. if ( preg_match( '#height=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $height_string ) )
  146. $height = $height_string[1];
  147. // Can't pass both a relative width and height, so unset the height in favor of not breaking the horizontal layout.
  148. if ( false !== strpos( $width, '%' ) && false !== strpos( $height, '%' ) )
  149. $width = $height = false;
  150. // Detect WP registered image size from HTML class
  151. if ( preg_match( '#class=["|\']?[^"\']*size-([^"\'\s]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $size ) ) {
  152. $size = array_pop( $size );
  153. if ( false === $width && false === $height && 'full' != $size && array_key_exists( $size, $image_sizes ) ) {
  154. $width = (int) $image_sizes[ $size ]['width'];
  155. $height = (int) $image_sizes[ $size ]['height'];
  156. $transform = $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
  157. }
  158. } else {
  159. unset( $size );
  160. }
  161. // WP Attachment ID, if uploaded to this site
  162. if (
  163. preg_match( '#class=["|\']?[^"\']*wp-image-([\d]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $attachment_id ) &&
  164. (
  165. 0 === strpos( $src, $upload_dir['baseurl'] ) ||
  166. /**
  167. * Filter whether an image using an attachment ID in its class has to be uploaded to the local site to go through Photon.
  168. *
  169. * @module photon
  170. *
  171. * @since 2.0.3
  172. *
  173. * @param bool false Was the image uploaded to the local site. Default to false.
  174. * @param array $args {
  175. * Array of image details.
  176. *
  177. * @type $src Image URL.
  178. * @type tag Image tag (Image HTML output).
  179. * @type $images Array of information about the image.
  180. * @type $index Image index.
  181. * }
  182. */
  183. apply_filters( 'jetpack_photon_image_is_local', false, compact( 'src', 'tag', 'images', 'index' ) )
  184. )
  185. ) {
  186. $attachment_id = intval( array_pop( $attachment_id ) );
  187. if ( $attachment_id ) {
  188. $attachment = get_post( $attachment_id );
  189. // Basic check on returned post object
  190. if ( is_object( $attachment ) && ! is_wp_error( $attachment ) && 'attachment' == $attachment->post_type ) {
  191. $src_per_wp = wp_get_attachment_image_src( $attachment_id, isset( $size ) ? $size : 'full' );
  192. if ( self::validate_image_url( $src_per_wp[0] ) ) {
  193. $src = $src_per_wp[0];
  194. $fullsize_url = true;
  195. // Prevent image distortion if a detected dimension exceeds the image's natural dimensions
  196. if ( ( false !== $width && $width > $src_per_wp[1] ) || ( false !== $height && $height > $src_per_wp[2] ) ) {
  197. $width = false == $width ? false : min( $width, $src_per_wp[1] );
  198. $height = false == $height ? false : min( $height, $src_per_wp[2] );
  199. }
  200. // If no width and height are found, max out at source image's natural dimensions
  201. // Otherwise, respect registered image sizes' cropping setting
  202. if ( false == $width && false == $height ) {
  203. $width = $src_per_wp[1];
  204. $height = $src_per_wp[2];
  205. $transform = 'fit';
  206. } elseif ( isset( $size ) && array_key_exists( $size, $image_sizes ) && isset( $image_sizes[ $size ]['crop'] ) ) {
  207. $transform = (bool) $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
  208. }
  209. }
  210. } else {
  211. unset( $attachment_id );
  212. unset( $attachment );
  213. }
  214. }
  215. }
  216. // If image tag lacks width and height arguments, try to determine from strings WP appends to resized image filenames.
  217. if ( false === $width && false === $height ) {
  218. list( $width, $height ) = Jetpack_Photon::parse_dimensions_from_filename( $src );
  219. }
  220. // If width is available, constrain to $content_width
  221. if ( false !== $width && false === strpos( $width, '%' ) && is_numeric( $content_width ) ) {
  222. if ( $width > $content_width && false !== $height && false === strpos( $height, '%' ) ) {
  223. $height = round( ( $content_width * $height ) / $width );
  224. $width = $content_width;
  225. } elseif ( $width > $content_width ) {
  226. $width = $content_width;
  227. }
  228. }
  229. // Set a width if none is found and $content_width is available
  230. // If width is set in this manner and height is available, use `fit` instead of `resize` to prevent skewing
  231. if ( false === $width && is_numeric( $content_width ) ) {
  232. $width = (int) $content_width;
  233. if ( false !== $height )
  234. $transform = 'fit';
  235. }
  236. // Detect if image source is for a custom-cropped thumbnail and prevent further URL manipulation.
  237. if ( ! $fullsize_url && preg_match_all( '#-e[a-z0-9]+(-\d+x\d+)?\.(' . implode('|', self::$extensions ) . '){1}$#i', basename( $src ), $filename ) )
  238. $fullsize_url = true;
  239. // Build URL, first maybe removing WP's resized string so we pass the original image to Photon
  240. if ( ! $fullsize_url ) {
  241. $src = self::strip_image_dimensions_maybe( $src );
  242. }
  243. // Build array of Photon args and expose to filter before passing to Photon URL function
  244. $args = array();
  245. if ( false !== $width && false !== $height && false === strpos( $width, '%' ) && false === strpos( $height, '%' ) )
  246. $args[ $transform ] = $width . ',' . $height;
  247. elseif ( false !== $width )
  248. $args['w'] = $width;
  249. elseif ( false !== $height )
  250. $args['h'] = $height;
  251. /**
  252. * Filter the array of Photon arguments added to an image when it goes through Photon.
  253. * By default, only includes width and height values.
  254. * @see https://developer.wordpress.com/docs/photon/api/
  255. *
  256. * @module photon
  257. *
  258. * @since 2.0.0
  259. *
  260. * @param array $args Array of Photon Arguments.
  261. * @param array $args {
  262. * Array of image details.
  263. *
  264. * @type $tag Image tag (Image HTML output).
  265. * @type $src Image URL.
  266. * @type $src_orig Original Image URL.
  267. * @type $width Image width.
  268. * @type $height Image height.
  269. * }
  270. */
  271. $args = apply_filters( 'jetpack_photon_post_image_args', $args, compact( 'tag', 'src', 'src_orig', 'width', 'height' ) );
  272. $photon_url = jetpack_photon_url( $src, $args );
  273. // Modify image tag if Photon function provides a URL
  274. // 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.
  275. if ( $src != $photon_url ) {
  276. $new_tag = $tag;
  277. // If present, replace the link href with a Photoned URL for the full-size image.
  278. if ( ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) )
  279. $new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . jetpack_photon_url( $images['link_url'][ $index ] ) . '\2', $new_tag, 1 );
  280. // Supplant the original source value with our Photon URL
  281. $photon_url = esc_url( $photon_url );
  282. $new_tag = str_replace( $src_orig, $photon_url, $new_tag );
  283. // If Lazy Load is in use, pass placeholder image through Photon
  284. if ( isset( $placeholder_src ) && self::validate_image_url( $placeholder_src ) ) {
  285. $placeholder_src = jetpack_photon_url( $placeholder_src );
  286. if ( $placeholder_src != $placeholder_src_orig )
  287. $new_tag = str_replace( $placeholder_src_orig, esc_url( $placeholder_src ), $new_tag );
  288. unset( $placeholder_src );
  289. }
  290. // Remove the width and height arguments from the tag to prevent distortion
  291. $new_tag = preg_replace( '#(?<=\s)(width|height)=["|\']?[\d%]+["|\']?\s?#i', '', $new_tag );
  292. // Tag an image for dimension checking
  293. $new_tag = preg_replace( '#(\s?/)?>(\s*</a>)?$#i', ' data-recalc-dims="1"\1>\2', $new_tag );
  294. // Replace original tag with modified version
  295. $content = str_replace( $tag, $new_tag, $content );
  296. }
  297. } elseif ( preg_match( '#^http(s)?://i[\d]{1}.wp.com#', $src ) && ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
  298. $new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . jetpack_photon_url( $images['link_url'][ $index ] ) . '\2', $tag, 1 );
  299. $content = str_replace( $tag, $new_tag, $content );
  300. }
  301. }
  302. }
  303. return $content;
  304. }
  305. public static function filter_the_galleries( $galleries ) {
  306. if ( empty( $galleries ) || ! is_array( $galleries ) ) {
  307. return $galleries;
  308. }
  309. // Pass by reference, so we can modify them in place.
  310. foreach ( $galleries as &$this_gallery ) {
  311. if ( is_string( $this_gallery ) ) {
  312. $this_gallery = self::filter_the_content( $this_gallery );
  313. // LEAVING COMMENTED OUT as for the moment it doesn't seem
  314. // necessary and I'm not sure how it would propagate through.
  315. // } elseif ( is_array( $this_gallery )
  316. // && ! empty( $this_gallery['src'] )
  317. // && ! empty( $this_gallery['type'] )
  318. // && in_array( $this_gallery['type'], array( 'rectangle', 'square', 'circle' ) ) ) {
  319. // $this_gallery['src'] = array_map( 'jetpack_photon_url', $this_gallery['src'] );
  320. }
  321. }
  322. unset( $this_gallery ); // break the reference.
  323. return $galleries;
  324. }
  325. /**
  326. ** CORE IMAGE RETRIEVAL
  327. **/
  328. /**
  329. * Filter post thumbnail image retrieval, passing images through Photon
  330. *
  331. * @param string|bool $image
  332. * @param int $attachment_id
  333. * @param string|array $size
  334. * @uses is_admin, apply_filters, wp_get_attachment_url, self::validate_image_url, this::image_sizes, jetpack_photon_url
  335. * @filter image_downsize
  336. * @return string|bool
  337. */
  338. public function filter_image_downsize( $image, $attachment_id, $size ) {
  339. // Don't foul up the admin side of things, and provide plugins a way of preventing Photon from being applied to images.
  340. if (
  341. is_admin() ||
  342. /**
  343. * Provide plugins a way of preventing Photon from being applied to images retrieved from WordPress Core.
  344. *
  345. * @module photon
  346. *
  347. * @since 2.0.0
  348. *
  349. * @param bool false Stop Photon from being applied to the image. Default to false.
  350. * @param array $args {
  351. * Array of image details.
  352. *
  353. * @type $image Image URL.
  354. * @type $attachment_id Attachment ID of the image.
  355. * @type $size Image size. Can be a string (name of the image size, e.g. full) or an integer.
  356. * }
  357. */
  358. apply_filters( 'jetpack_photon_override_image_downsize', false, compact( 'image', 'attachment_id', 'size' ) )
  359. )
  360. return $image;
  361. // Get the image URL and proceed with Photon-ification if successful
  362. $image_url = wp_get_attachment_url( $attachment_id );
  363. // Set this to true later when we know we have size meta.
  364. $has_size_meta = false;
  365. if ( $image_url ) {
  366. // Check if image URL should be used with Photon
  367. if ( ! self::validate_image_url( $image_url ) )
  368. return $image;
  369. $intermediate = true; // For the fourth array item returned by the image_downsize filter.
  370. // If an image is requested with a size known to WordPress, use that size's settings with Photon
  371. if ( ( is_string( $size ) || is_int( $size ) ) && array_key_exists( $size, self::image_sizes() ) ) {
  372. $image_args = self::image_sizes();
  373. $image_args = $image_args[ $size ];
  374. $photon_args = array();
  375. $image_meta = image_get_intermediate_size( $attachment_id, $size );
  376. // 'full' is a special case: We need consistent data regardless of the requested size.
  377. if ( 'full' == $size ) {
  378. $image_meta = wp_get_attachment_metadata( $attachment_id );
  379. $intermediate = false;
  380. } elseif ( ! $image_meta ) {
  381. // If we still don't have any image meta at this point, it's probably from a custom thumbnail size
  382. // for an image that was uploaded before the custom image was added to the theme. Try to determine the size manually.
  383. $image_meta = wp_get_attachment_metadata( $attachment_id );
  384. if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
  385. $image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $image_args['width'], $image_args['height'], $image_args['crop'] );
  386. if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
  387. $image_meta['width'] = $image_resized[6];
  388. $image_meta['height'] = $image_resized[7];
  389. }
  390. }
  391. }
  392. if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
  393. $image_args['width'] = $image_meta['width'];
  394. $image_args['height'] = $image_meta['height'];
  395. list( $image_args['width'], $image_args['height'] ) = image_constrain_size_for_editor( $image_args['width'], $image_args['height'], $size, 'display' );
  396. $has_size_meta = true;
  397. }
  398. // Expose determined arguments to a filter before passing to Photon
  399. $transform = $image_args['crop'] ? 'resize' : 'fit';
  400. // Check specified image dimensions and account for possible zero values; photon fails to resize if a dimension is zero.
  401. if ( 0 == $image_args['width'] || 0 == $image_args['height'] ) {
  402. if ( 0 == $image_args['width'] && 0 < $image_args['height'] ) {
  403. $photon_args['h'] = $image_args['height'];
  404. } elseif ( 0 == $image_args['height'] && 0 < $image_args['width'] ) {
  405. $photon_args['w'] = $image_args['width'];
  406. }
  407. } else {
  408. if ( ( 'resize' === $transform ) && $image_meta = wp_get_attachment_metadata( $attachment_id ) ) {
  409. if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
  410. // Lets make sure that we don't upscale images since wp never upscales them as well
  411. $smaller_width = ( ( $image_meta['width'] < $image_args['width'] ) ? $image_meta['width'] : $image_args['width'] );
  412. $smaller_height = ( ( $image_meta['height'] < $image_args['height'] ) ? $image_meta['height'] : $image_args['height'] );
  413. $photon_args[ $transform ] = $smaller_width . ',' . $smaller_height;
  414. }
  415. } else {
  416. $photon_args[ $transform ] = $image_args['width'] . ',' . $image_args['height'];
  417. }
  418. }
  419. /**
  420. * Filter the Photon Arguments added to an image when going through Photon, when that image size is a string.
  421. * Image size will be a string (e.g. "full", "medium") when it is known to WordPress.
  422. *
  423. * @module photon
  424. *
  425. * @since 2.0.0
  426. *
  427. * @param array $photon_args Array of Photon arguments.
  428. * @param array $args {
  429. * Array of image details.
  430. *
  431. * @type $image_args Array of Image arguments (width, height, crop).
  432. * @type $image_url Image URL.
  433. * @type $attachment_id Attachment ID of the image.
  434. * @type $size Image size. Can be a string (name of the image size, e.g. full) or an integer.
  435. * @type $transform Value can be resize or fit.
  436. * @see https://developer.wordpress.com/docs/photon/api
  437. * }
  438. */
  439. $photon_args = apply_filters( 'jetpack_photon_image_downsize_string', $photon_args, compact( 'image_args', 'image_url', 'attachment_id', 'size', 'transform' ) );
  440. // Generate Photon URL
  441. $image = array(
  442. jetpack_photon_url( $image_url, $photon_args ),
  443. $has_size_meta ? $image_args['width'] : false,
  444. $has_size_meta ? $image_args['height'] : false,
  445. $intermediate
  446. );
  447. } elseif ( is_array( $size ) ) {
  448. // Pull width and height values from the provided array, if possible
  449. $width = isset( $size[0] ) ? (int) $size[0] : false;
  450. $height = isset( $size[1] ) ? (int) $size[1] : false;
  451. // Don't bother if necessary parameters aren't passed.
  452. if ( ! $width || ! $height ) {
  453. return $image;
  454. }
  455. $image_meta = wp_get_attachment_metadata( $attachment_id );
  456. if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
  457. $image_resized = image_resize_dimensions( $image_meta['width'], $image_meta['height'], $width, $height );
  458. if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
  459. $width = $image_resized[6];
  460. $height = $image_resized[7];
  461. } else {
  462. $width = $image_meta['width'];
  463. $height = $image_meta['height'];
  464. }
  465. $has_size_meta = true;
  466. }
  467. list( $width, $height ) = image_constrain_size_for_editor( $width, $height, $size );
  468. // Expose arguments to a filter before passing to Photon
  469. $photon_args = array(
  470. 'fit' => $width . ',' . $height
  471. );
  472. /**
  473. * Filter the Photon Arguments added to an image when going through Photon,
  474. * when the image size is an array of height and width values.
  475. *
  476. * @module photon
  477. *
  478. * @since 2.0.0
  479. *
  480. * @param array $photon_args Array of Photon arguments.
  481. * @param array $args {
  482. * Array of image details.
  483. *
  484. * @type $width Image width.
  485. * @type height Image height.
  486. * @type $image_url Image URL.
  487. * @type $attachment_id Attachment ID of the image.
  488. * }
  489. */
  490. $photon_args = apply_filters( 'jetpack_photon_image_downsize_array', $photon_args, compact( 'width', 'height', 'image_url', 'attachment_id' ) );
  491. // Generate Photon URL
  492. $image = array(
  493. jetpack_photon_url( $image_url, $photon_args ),
  494. $has_size_meta ? $width : false,
  495. $has_size_meta ? $height : false,
  496. $intermediate
  497. );
  498. }
  499. }
  500. return $image;
  501. }
  502. /**
  503. * Filters an array of image `srcset` values, replacing each URL with its Photon equivalent.
  504. *
  505. * @since 3.8.0
  506. * @since 4.0.4 Added automatically additional sizes beyond declared image sizes.
  507. * @param array $sources An array of image urls and widths.
  508. * @uses self::validate_image_url, jetpack_photon_url, Jetpack_Photon::parse_from_filename
  509. * @uses Jetpack_Photon::strip_image_dimensions_maybe, Jetpack::get_content_width
  510. * @return array An array of Photon image urls and widths.
  511. */
  512. public function filter_srcset_array( $sources, $size_array, $image_src, $image_meta ) {
  513. $upload_dir = wp_upload_dir();
  514. foreach ( $sources as $i => $source ) {
  515. if ( ! self::validate_image_url( $source['url'] ) ) {
  516. continue;
  517. }
  518. /** This filter is already documented in class.photon.php */
  519. if ( apply_filters( 'jetpack_photon_skip_image', false, $source['url'], $source ) ) {
  520. continue;
  521. }
  522. $url = $source['url'];
  523. list( $width, $height ) = Jetpack_Photon::parse_dimensions_from_filename( $url );
  524. // It's quicker to get the full size with the data we have already, if available
  525. if ( isset( $image_meta['file'] ) ) {
  526. $url = trailingslashit( $upload_dir['baseurl'] ) . $image_meta['file'];
  527. } else {
  528. $url = Jetpack_Photon::strip_image_dimensions_maybe( $url );
  529. }
  530. $args = array();
  531. if ( 'w' === $source['descriptor'] ) {
  532. if ( $height && ( $source['value'] == $width ) ) {
  533. $args['resize'] = $width . ',' . $height;
  534. } else {
  535. $args['w'] = $source['value'];
  536. }
  537. }
  538. $sources[ $i ]['url'] = jetpack_photon_url( $url, $args );
  539. }
  540. /**
  541. * At this point, $sources is the original srcset with Photonized URLs.
  542. * Now, we're going to construct additional sizes based on multiples of the content_width.
  543. * This will reduce the gap between the largest defined size and the original image.
  544. */
  545. /**
  546. * Filter the multiplier Photon uses to create new srcset items.
  547. * Return false to short-circuit and bypass auto-generation.
  548. *
  549. * @module photon
  550. *
  551. * @since 4.0.4
  552. *
  553. * @param array|bool $multipliers Array of multipliers to use or false to bypass.
  554. */
  555. $multipliers = apply_filters( 'jetpack_photon_srcset_multipliers', array( 2, 3 ) );
  556. $url = trailingslashit( $upload_dir['baseurl'] ) . $image_meta['file'];
  557. if (
  558. /** Short-circuit via jetpack_photon_srcset_multipliers filter. */
  559. is_array( $multipliers )
  560. /** This filter is already documented in class.photon.php */
  561. && ! apply_filters( 'jetpack_photon_skip_image', false, $url, null )
  562. /** Verify basic meta is intact. */
  563. && isset( $image_meta['width'] ) && isset( $image_meta['height'] ) && isset( $image_meta['file'] )
  564. /** Verify we have the requested width/height. */
  565. && isset( $size_array[0] ) && isset( $size_array[1] )
  566. ) {
  567. $fullwidth = $image_meta['width'];
  568. $fullheight = $image_meta['height'];
  569. $reqwidth = $size_array[0];
  570. $reqheight = $size_array[1];
  571. $constrained_size = wp_constrain_dimensions( $fullwidth, $fullheight, $reqwidth );
  572. $expected_size = array( $reqwidth, $reqheight );
  573. if ( abs( $constrained_size[0] - $expected_size[0] ) <= 1 && abs( $constrained_size[1] - $expected_size[1] ) <= 1 ) {
  574. $crop = 'soft';
  575. $base = Jetpack::get_content_width() ? Jetpack::get_content_width() : 1000; // Provide a default width if none set by the theme.
  576. }
  577. else {
  578. $crop = 'hard';
  579. $base = $reqwidth;
  580. }
  581. $currentwidths = array_keys( $sources );
  582. $newsources = null;
  583. foreach ( $multipliers as $multiplier ) {
  584. $newwidth = $base * $multiplier;
  585. foreach ( $currentwidths as $currentwidth ){
  586. // If a new width would be within 100 pixes of an existing one or larger than the full size image, skip.
  587. if ( abs( $currentwidth - $newwidth ) < 50 || ( $newwidth > $fullwidth ) ) {
  588. continue 2; // Back to the foreach ( $multipliers as $multiplier )
  589. }
  590. } // foreach ( $currentwidths as $currentwidth ){
  591. if ( 'soft' == $crop ) {
  592. $args = array(
  593. 'w' => $newwidth,
  594. );
  595. }
  596. else { // hard crop, e.g. add_image_size( 'example', 200, 200, true );
  597. $args = array(
  598. 'zoom' => $multiplier,
  599. 'resize' => $reqwidth . ',' . $reqheight,
  600. );
  601. }
  602. $newsources[ $newwidth ] = array(
  603. 'url' => jetpack_photon_url( $url, $args ),
  604. 'descriptor' => 'w',
  605. 'value' => $newwidth,
  606. );
  607. } // foreach ( $multipliers as $multiplier )
  608. if ( is_array( $newsources ) ) {
  609. $sources = array_merge( $sources, $newsources );
  610. }
  611. } // if ( isset( $image_meta['width'] ) && isset( $image_meta['file'] ) )
  612. return $sources;
  613. }
  614. /**
  615. * Filters an array of image `sizes` values, using $content_width instead of image's full size.
  616. *
  617. * @since 4.0.4
  618. * @since 4.1.0 Returns early for images not within the_content.
  619. * @param array $sizes An array of media query breakpoints.
  620. * @param array $size Width and height of the image
  621. * @uses Jetpack::get_content_width
  622. * @return array An array of media query breakpoints.
  623. */
  624. public function filter_sizes( $sizes, $size ) {
  625. if ( ! doing_filter( 'the_content' ) ){
  626. return $sizes;
  627. }
  628. $content_width = Jetpack::get_content_width();
  629. if ( ! $content_width ) {
  630. $content_width = 1000;
  631. }
  632. if ( ( is_array( $size ) && $size[0] < $content_width ) ) {
  633. return $sizes;
  634. }
  635. return sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $content_width );
  636. }
  637. /**
  638. ** GENERAL FUNCTIONS
  639. **/
  640. /**
  641. * Ensure image URL is valid for Photon.
  642. * Though Photon functions address some of the URL issues, we should avoid unnecessary processing if we know early on that the image isn't supported.
  643. *
  644. * @param string $url
  645. * @uses wp_parse_args
  646. * @return bool
  647. */
  648. protected static function validate_image_url( $url ) {
  649. $parsed_url = @parse_url( $url );
  650. if ( ! $parsed_url )
  651. return false;
  652. // Parse URL and ensure needed keys exist, since the array returned by `parse_url` only includes the URL components it finds.
  653. $url_info = wp_parse_args( $parsed_url, array(
  654. 'scheme' => null,
  655. 'host' => null,
  656. 'port' => null,
  657. 'path' => null
  658. ) );
  659. // Bail if scheme isn't http or port is set that isn't port 80
  660. if (
  661. ( 'http' != $url_info['scheme'] || ! in_array( $url_info['port'], array( 80, null ) ) ) &&
  662. /**
  663. * Allow Photon to fetch images that are served via HTTPS.
  664. *
  665. * @module photon
  666. *
  667. * @since 2.4.0
  668. * @since 3.9.0 Default to false.
  669. *
  670. * @param bool $reject_https Should Photon ignore images using the HTTPS scheme. Default to false.
  671. */
  672. apply_filters( 'jetpack_photon_reject_https', false )
  673. ) {
  674. return false;
  675. }
  676. // Bail if no host is found
  677. if ( is_null( $url_info['host'] ) )
  678. return false;
  679. // Bail if the image alredy went through Photon
  680. if ( preg_match( '#^i[\d]{1}.wp.com$#i', $url_info['host'] ) )
  681. return false;
  682. // Bail if no path is found
  683. if ( is_null( $url_info['path'] ) )
  684. return false;
  685. // Ensure image extension is acceptable
  686. if ( ! in_array( strtolower( pathinfo( $url_info['path'], PATHINFO_EXTENSION ) ), self::$extensions ) )
  687. return false;
  688. // If we got this far, we should have an acceptable image URL
  689. // But let folks filter to decline if they prefer.
  690. /**
  691. * Overwrite the results of the validation steps an image goes through before to be considered valid to be used by Photon.
  692. *
  693. * @module photon
  694. *
  695. * @since 3.0.0
  696. *
  697. * @param bool true Is the image URL valid and can it be used by Photon. Default to true.
  698. * @param string $url Image URL.
  699. * @param array $parsed_url Array of information about the image.
  700. */
  701. return apply_filters( 'photon_validate_image_url', true, $url, $parsed_url );
  702. }
  703. /**
  704. * Checks if the file exists before it passes the file to photon
  705. *
  706. * @param string $src The image URL
  707. * @return string
  708. **/
  709. protected static function strip_image_dimensions_maybe( $src ){
  710. $stripped_src = $src;
  711. // Build URL, first removing WP's resized string so we pass the original image to Photon
  712. if ( preg_match( '#(-\d+x\d+)\.(' . implode('|', self::$extensions ) . '){1}$#i', $src, $src_parts ) ) {
  713. $stripped_src = str_replace( $src_parts[1], '', $src );
  714. $upload_dir = wp_upload_dir();
  715. // Extracts the file path to the image minus the base url
  716. $file_path = substr( $stripped_src, strlen ( $upload_dir['baseurl'] ) );
  717. if( file_exists( $upload_dir["basedir"] . $file_path ) )
  718. $src = $stripped_src;
  719. }
  720. return $src;
  721. }
  722. /**
  723. * Provide an array of available image sizes and corresponding dimensions.
  724. * Similar to get_intermediate_image_sizes() except that it includes image sizes' dimensions, not just their names.
  725. *
  726. * @global $wp_additional_image_sizes
  727. * @uses get_option
  728. * @return array
  729. */
  730. protected static function image_sizes() {
  731. if ( null == self::$image_sizes ) {
  732. global $_wp_additional_image_sizes;
  733. // Populate an array matching the data structure of $_wp_additional_image_sizes so we have a consistent structure for image sizes
  734. $images = array(
  735. 'thumb' => array(
  736. 'width' => intval( get_option( 'thumbnail_size_w' ) ),
  737. 'height' => intval( get_option( 'thumbnail_size_h' ) ),
  738. 'crop' => (bool) get_option( 'thumbnail_crop' )
  739. ),
  740. 'medium' => array(
  741. 'width' => intval( get_option( 'medium_size_w' ) ),
  742. 'height' => intval( get_option( 'medium_size_h' ) ),
  743. 'crop' => false
  744. ),
  745. 'large' => array(
  746. 'width' => intval( get_option( 'large_size_w' ) ),
  747. 'height' => intval( get_option( 'large_size_h' ) ),
  748. 'crop' => false
  749. ),
  750. 'full' => array(
  751. 'width' => null,
  752. 'height' => null,
  753. 'crop' => false
  754. )
  755. );
  756. // Compatibility mapping as found in wp-includes/media.php
  757. $images['thumbnail'] = $images['thumb'];
  758. // Update class variable, merging in $_wp_additional_image_sizes if any are set
  759. if ( is_array( $_wp_additional_image_sizes ) && ! empty( $_wp_additional_image_sizes ) )
  760. self::$image_sizes = array_merge( $images, $_wp_additional_image_sizes );
  761. else
  762. self::$image_sizes = $images;
  763. }
  764. return is_array( self::$image_sizes ) ? self::$image_sizes : array();
  765. }
  766. /**
  767. * Pass og:image URLs through Photon
  768. *
  769. * @param array $tags
  770. * @param array $parameters
  771. * @uses jetpack_photon_url
  772. * @return array
  773. */
  774. function filter_open_graph_tags( $tags, $parameters ) {
  775. if ( empty( $tags['og:image'] ) ) {
  776. return $tags;
  777. }
  778. $photon_args = array(
  779. 'fit' => sprintf( '%d,%d', 2 * $parameters['image_width'], 2 * $parameters['image_height'] ),
  780. );
  781. if ( is_array( $tags['og:image'] ) ) {
  782. $images = array();
  783. foreach ( $tags['og:image'] as $image ) {
  784. $images[] = jetpack_photon_url( $image, $photon_args );
  785. }
  786. $tags['og:image'] = $images;
  787. } else {
  788. $tags['og:image'] = jetpack_photon_url( $tags['og:image'], $photon_args );
  789. }
  790. return $tags;
  791. }
  792. /**
  793. * Enqueue Photon helper script
  794. *
  795. * @uses wp_enqueue_script, plugins_url
  796. * @action wp_enqueue_script
  797. * @return null
  798. */
  799. public function action_wp_enqueue_scripts() {
  800. wp_enqueue_script( 'jetpack-photon', plugins_url( 'modules/photon/photon.js', JETPACK__PLUGIN_FILE ), array( 'jquery' ), 20130122, true );
  801. }
  802. }