class.photon.php 37 KB

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