├── .gitignore
├── inc
    ├── class-my-photon-settings.php
    ├── class-my-photon.php
    └── functions.php
├── my-photon.php
└── readme.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
--------------------------------------------------------------------------------
/inc/class-my-photon-settings.php:
--------------------------------------------------------------------------------
  1 |  false,
 11 | 		'base-url' => '',
 12 | 	);
 13 | 
 14 | 	const SLUG = 'my-photon';
 15 | 
 16 | 	protected static $instance;
 17 | 
 18 | 	public static function instance() {
 19 | 		if ( ! isset( self::$instance ) ) {
 20 | 			self::$instance = new My_Photon_Settings;
 21 | 			self::$instance->setup_actions();
 22 | 		}
 23 | 		return self::$instance;
 24 | 	}
 25 | 
 26 | 	protected function __construct() {
 27 | 		/** Don't do anything **/
 28 | 	}
 29 | 
 30 | 	public static function get( $key = null, $default = null ) {
 31 | 		if ( ! isset( self::$options ) ) {
 32 | 			self::$options = get_option( self::SLUG, array() );
 33 | 			self::$options = wp_parse_args( self::$options, self::$defaults );
 34 | 		}
 35 | 
 36 | 		if ( isset( $key ) ) {
 37 | 			return ! empty( self::$options[ $key ] ) ? self::$options[ $key ] : $default;
 38 | 		} else {
 39 | 			return self::$options;
 40 | 		}
 41 | 	}
 42 | 
 43 | 	protected function setup_actions() {
 44 | 		add_action( 'admin_init', array( self::$instance, 'action_admin_init' ) );
 45 | 		add_action( 'admin_menu', array( self::$instance, 'action_admin_menu' ) );
 46 | 	}
 47 | 
 48 | 	public function action_admin_init() {
 49 | 		register_setting( self::SLUG, self::SLUG, array( self::$instance, 'sanitize_options' ) );
 50 | 		add_settings_section( 'general', false, '__return_false', self::SLUG );
 51 | 		add_settings_field( 'active', __( 'Activate My Photon', 'my-photon' ), array( self::$instance, 'field' ), self::SLUG, 'general', array( 'name' => 'active', 'type' => 'checkbox', 'label' => __( 'Active', 'my-photon' ) ) );
 52 | 		add_settings_field( 'base-url', __( 'Base URL', 'my-photon' ), array( self::$instance, 'field' ), self::SLUG, 'general', array( 'name' => 'base-url' ) );
 53 | 	}
 54 | 
 55 | 	public function action_admin_menu() {
 56 | 		add_options_page( __( 'My Photon Settings', 'my-photon' ), __( 'My Photon Settings', 'my-photon' ), $this->options_capability, self::SLUG, array( self::$instance, 'view_settings_page' ) );
 57 | 	}
 58 | 
 59 | 	public function field( $args ) {
 60 | 		$args = wp_parse_args( $args, array(
 61 | 			'name' => '',
 62 | 			'type' => 'text',
 63 | 			'label' => null,
 64 | 		) );
 65 | 		switch ( $args['type'] ) {
 66 | 			case 'checkbox' :
 67 | 				printf(
 68 | 					'',
 69 | 					self::SLUG,
 70 | 					esc_attr( $args['name'] ),
 71 | 					checked( $this->get( $args['name'] ), true, false ),
 72 | 					$args['label']
 73 | 				);
 74 | 				break;
 75 | 
 76 | 			default :
 77 | 				printf( '', self::SLUG, esc_attr( $args['name'] ), esc_attr( $this->get( $args['name'] ) ) );
 78 | 				break;
 79 | 		}
 80 | 	}
 81 | 
 82 | 	public function sanitize_options( $in ) {
 83 | 		$in = wp_parse_args( $in, self::$defaults );
 84 | 
 85 | 		// Validate base-url
 86 | 		$out['active'] = ( '1' == $in['active'] );
 87 | 		$out['base-url'] = esc_url_raw( $in['base-url'] );
 88 | 		return $out;
 89 | 	}
 90 | 
 91 | 	public function view_settings_page() {
 92 | 	?>
 93 | 		
 94 | 		
 99 | 	
100 | 	setup();
 33 | 		}
 34 | 
 35 | 		return self::$__instance;
 36 | 	}
 37 | 
 38 | 	/**
 39 | 	 * Silence is golden.
 40 | 	 */
 41 | 	private function __construct() {}
 42 | 
 43 | 	/**
 44 | 	 * Register actions and filters, but only if basic Photon functions are available.
 45 | 	 * The basic functions are found in ./functions.photon.php.
 46 | 	 *
 47 | 	 * @uses add_action, add_filter
 48 | 	 * @return null
 49 | 	 */
 50 | 	private function setup() {
 51 | 		if ( ! function_exists( 'my_photon_url' ) )
 52 | 			return;
 53 | 
 54 | 		// Images in post content and galleries
 55 | 		add_filter( 'the_content', array( __CLASS__, 'filter_the_content' ), 999999 );
 56 | 		add_filter( 'get_post_galleries', array( __CLASS__, 'filter_the_galleries' ), 999999 );
 57 | 
 58 | 		// Core image retrieval
 59 | 		add_filter( 'image_downsize', array( $this, 'filter_image_downsize' ), 10, 3 );
 60 | 	}
 61 | 
 62 | 
 63 | 	/**
 64 | 	 ** IN-CONTENT IMAGE MANIPULATION FUNCTIONS
 65 | 	 **/
 66 | 
 67 | 	/**
 68 | 	 * Match all images and any relevant  tags in a block of HTML.
 69 | 	 *
 70 | 	 * @param string $content Some HTML.
 71 | 	 * @return array An array of $images matches, where $images[0] is
 72 | 	 *         an array of full matches, and the link_url, img_tag,
 73 | 	 *         and img_url keys are arrays of those matches.
 74 | 	 */
 75 | 	public static function parse_images_from_html( $content ) {
 76 | 		$images = array();
 77 | 
 78 | 		if ( preg_match_all( '#(?:]+?href=["|\'](?P[^\s]+?)["|\'][^>]*?>\s*)?(?P![]() ]+?src=["|\'](?P[^\s]+?)["|\'].*?>){1}(?:\s*)?#is', $content, $images ) ) {
 79 | 			foreach ( $images as $key => $unused ) {
 80 | 				// Simplify the output as much as possible, mostly for confirming test results.
 81 | 				if ( is_numeric( $key ) && $key > 0 )
 82 | 					unset( $images[$key] );
 83 | 			}
 84 | 
 85 | 			return $images;
 86 | 		}
 87 | 
 88 | 		return array();
 89 | 	}
 90 | 
 91 | 	/**
 92 | 	 * Try to determine height and width from strings WP appends to resized image filenames.
 93 | 	 *
 94 | 	 * @param string $src The image URL.
 95 | 	 * @return array An array consisting of width and height.
 96 | 	 */
 97 | 	public static function parse_dimensions_from_filename( $src ) {
 98 | 		$width_height_string = array();
 99 | 
100 | 		if ( preg_match( '#-(\d+)x(\d+)\.(?:' . implode('|', self::$extensions ) . '){1}$#i', $src, $width_height_string ) ) {
101 | 			$width = (int) $width_height_string[1];
102 | 			$height = (int) $width_height_string[2];
103 | 
104 | 			if ( $width && $height )
105 | 				return array( $width, $height );
106 | 		}
107 | 
108 | 		return array( false, false );
109 | 	}
110 | 
111 | 	/**
112 | 	 * Identify images in post content, and if images are local (uploaded to the current site), pass through Photon.
113 | 	 *
114 | 	 * @param string $content
115 | 	 * @uses self::validate_image_url, apply_filters, my_photon_url, esc_url
116 | 	 * @filter the_content
117 | 	 * @return string
118 | 	 */
119 | 	public static function filter_the_content( $content ) {
120 | 
121 | 		$images = My_Photon::parse_images_from_html( $content );
122 | 
123 | 		if ( ! empty( $images ) ) {
124 | 			global $content_width;
125 | 
126 | 			$image_sizes = self::image_sizes();
127 | 			$upload_dir = wp_upload_dir();
128 | 
129 | 			foreach ( $images[0] as $index => $tag ) {
130 | 				// Default to resize, though fit may be used in certain cases where a dimension cannot be ascertained
131 | 				$transform = 'resize';
132 | 
133 | 				// Start with a clean attachment ID each time
134 | 				$attachment_id = false;
135 | 
136 | 				// Flag if we need to munge a fullsize URL
137 | 				$fullsize_url = false;
138 | 
139 | 				// Identify image source
140 | 				$src = $src_orig = $images['img_url'][ $index ];
141 | 
142 | 				// Allow specific images to be skipped
143 | 				if ( apply_filters( 'my_photon_skip_image', false, $src, $tag ) )
144 | 					continue;
145 | 
146 | 				// Support Automattic's Lazy Load plugin
147 | 				// Can't modify $tag yet as we need unadulterated version later
148 | 				if ( preg_match( '#data-lazy-src=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
149 | 					$placeholder_src = $placeholder_src_orig = $src;
150 | 					$src = $src_orig = $lazy_load_src[1];
151 | 				}
152 | 
153 | 				// Check if image URL should be used with Photon
154 | 				if ( self::validate_image_url( $src ) ) {
155 | 					// Find the width and height attributes
156 | 					$width = $height = false;
157 | 
158 | 					// First, check the image tag
159 | 					if ( preg_match( '#width=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $width_string ) )
160 | 						$width = $width_string[1];
161 | 
162 | 					if ( preg_match( '#height=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $height_string ) )
163 | 						$height = $height_string[1];
164 | 
165 | 					// Can't pass both a relative width and height, so unset the height in favor of not breaking the horizontal layout.
166 | 					if ( false !== strpos( $width, '%' ) && false !== strpos( $height, '%' ) )
167 | 						$width = $height = false;
168 | 
169 | 					// Detect WP registered image size from HTML class
170 | 					if ( preg_match( '#class=["|\']?[^"\']*size-([^"\'\s]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $size ) ) {
171 | 						$size = array_pop( $size );
172 | 
173 | 						if ( false === $width && false === $height && 'full' != $size && array_key_exists( $size, $image_sizes ) ) {
174 | 							$width = (int) $image_sizes[ $size ]['width'];
175 | 							$height = (int) $image_sizes[ $size ]['height'];
176 | 							$transform = $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
177 | 						}
178 | 					} else {
179 | 						unset( $size );
180 | 					}
181 | 
182 | 					// WP Attachment ID, if uploaded to this site
183 | 					if ( preg_match( '#class=["|\']?[^"\']*wp-image-([\d]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $attachment_id ) && ( 0 === strpos( $src, $upload_dir['baseurl'] ) || apply_filters( 'my_photon_image_is_local', false, compact( 'src', 'tag', 'images', 'index' ) ) ) ) {
184 | 						$attachment_id = intval( array_pop( $attachment_id ) );
185 | 
186 | 						if ( $attachment_id ) {
187 | 							$attachment = get_post( $attachment_id );
188 | 
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 | 
193 | 								if ( self::validate_image_url( $src_per_wp[0] ) ) {
194 | 									$src = $src_per_wp[0];
195 | 									$fullsize_url = true;
196 | 
197 | 									// Prevent image distortion if a detected dimension exceeds the image's natural dimensions
198 | 									if ( ( false !== $width && $width > $src_per_wp[1] ) || ( false !== $height && $height > $src_per_wp[2] ) ) {
199 | 										$width = false == $width ? false : min( $width, $src_per_wp[1] );
200 | 										$height = false == $height ? false : min( $height, $src_per_wp[2] );
201 | 									}
202 | 
203 | 									// If no width and height are found, max out at source image's natural dimensions
204 | 									// Otherwise, respect registered image sizes' cropping setting
205 | 									if ( false == $width && false == $height ) {
206 | 										$width = $src_per_wp[1];
207 | 										$height = $src_per_wp[2];
208 | 										$transform = 'fit';
209 | 									} elseif ( isset( $size ) && array_key_exists( $size, $image_sizes ) && isset( $image_sizes[ $size ]['crop'] ) ) {
210 | 										$transform = (bool) $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
211 | 									}
212 | 								}
213 | 							} else {
214 | 								unset( $attachment_id );
215 | 								unset( $attachment );
216 | 							}
217 | 						}
218 | 					}
219 | 
220 | 					// If image tag lacks width and height arguments, try to determine from strings WP appends to resized image filenames.
221 | 					if ( false === $width && false === $height ) {
222 | 						list( $width, $height ) = My_Photon::parse_dimensions_from_filename( $src );
223 | 					}
224 | 
225 | 					// If width is available, constrain to $content_width
226 | 					if ( false !== $width && false === strpos( $width, '%' ) && is_numeric( $content_width ) ) {
227 | 						if ( $width > $content_width && false !== $height && false === strpos( $height, '%' ) ) {
228 | 							$height = round( ( $content_width * $height ) / $width );
229 | 							$width = $content_width;
230 | 						} elseif ( $width > $content_width ) {
231 | 							$width = $content_width;
232 | 						}
233 | 					}
234 | 
235 | 					// Set a width if none is found and $content_width is available
236 | 					// If width is set in this manner and height is available, use `fit` instead of `resize` to prevent skewing
237 | 					if ( false === $width && is_numeric( $content_width ) ) {
238 | 						$width = (int) $content_width;
239 | 
240 | 						if ( false !== $height )
241 | 							$transform = 'fit';
242 | 					}
243 | 
244 | 					// Detect if image source is for a custom-cropped thumbnail and prevent further URL manipulation.
245 | 					if ( ! $fullsize_url && preg_match_all( '#-e[a-z0-9]+(-\d+x\d+)?\.(' . implode('|', self::$extensions ) . '){1}$#i', basename( $src ), $filename ) )
246 | 						$fullsize_url = true;
247 | 
248 | 					// Build URL, first maybe removing WP's resized string so we pass the original image to Photon
249 | 					if ( ! $fullsize_url ) {
250 | 						$src = self::strip_image_dimensions_maybe( $src );
251 | 					}
252 | 
253 | 					// Build array of Photon args and expose to filter before passing to Photon URL function
254 | 					$args = array();
255 | 
256 | 					if ( false !== $width && false !== $height && false === strpos( $width, '%' ) && false === strpos( $height, '%' ) )
257 | 						$args[ $transform ] = $width . ',' . $height;
258 | 					elseif ( false !== $width )
259 | 						$args['w'] = $width;
260 | 					elseif ( false !== $height )
261 | 						$args['h'] = $height;
262 | 
263 | 					$args = apply_filters( 'my_photon_post_image_args', $args, compact( 'tag', 'src', 'src_orig', 'width', 'height' ) );
264 | 
265 | 					$photon_url = my_photon_url( $src, $args );
266 | 
267 | 					// Modify image tag if Photon function provides a URL
268 | 					// 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.
269 | 					if ( $src != $photon_url ) {
270 | 						$new_tag = $tag;
271 | 
272 | 						// If present, replace the link href with a Photoned URL for the full-size image.
273 | 						if ( ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) )
274 | 							$new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . my_photon_url( $images['link_url'][ $index ] ) . '\2', $new_tag, 1 );
275 | 
276 | 						// Supplant the original source value with our Photon URL
277 | 						$photon_url = esc_url( $photon_url );
278 | 						$new_tag = str_replace( $src_orig, $photon_url, $new_tag );
279 | 
280 | 						// If Lazy Load is in use, pass placeholder image through Photon
281 | 						if ( isset( $placeholder_src ) && self::validate_image_url( $placeholder_src ) ) {
282 | 							$placeholder_src = my_photon_url( $placeholder_src );
283 | 
284 | 							if ( $placeholder_src != $placeholder_src_orig )
285 | 								$new_tag = str_replace( $placeholder_src_orig, esc_url( $placeholder_src ), $new_tag );
286 | 
287 | 							unset( $placeholder_src );
288 | 						}
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 | 
293 | 						// Tag an image for dimension checking
294 | 						$new_tag = preg_replace( '#(\s?/)?>()?$#i', ' data-recalc-dims="1"\1>\2', $new_tag );
295 | 
296 | 						// Replace original tag with modified version
297 | 						$content = str_replace( $tag, $new_tag, $content );
298 | 					}
299 | 				} elseif ( false !== strpos( $src, My_Photon_Settings::get( 'base-url' ) ) && ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
300 | 					$new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . my_photon_url( $images['link_url'][ $index ] ) . '\2', $tag, 1 );
301 | 
302 | 					$content = str_replace( $tag, $new_tag, $content );
303 | 				}
304 | 			}
305 | 		}
306 | 
307 | 		return $content;
308 | 	}
309 | 
310 | 	/**
311 | 	 * Filter Core galleries
312 | 	 *
313 | 	 * @param array $galleries Gallery array.
314 | 	 * @return array
315 | 	 */
316 | 	public static function filter_the_galleries( $galleries ) {
317 | 		if ( empty( $galleries ) || ! is_array( $galleries ) ) {
318 | 			return $galleries;
319 | 		}
320 | 
321 | 		// Pass by reference, so we can modify them in place.
322 | 		foreach ( $galleries as &$this_gallery ) {
323 | 			if ( is_string( $this_gallery ) ) {
324 | 				$this_gallery = self::filter_the_content( $this_gallery );
325 | 			}
326 | 		}
327 | 		unset( $this_gallery ); // break the reference.
328 | 
329 | 		return $galleries;
330 | 	}
331 | 
332 | 	/**
333 | 	 ** CORE IMAGE RETRIEVAL
334 | 	 **/
335 | 
336 | 	/**
337 | 	 * Filter post thumbnail image retrieval, passing images through Photon
338 | 	 *
339 | 	 * @param string|bool $image
340 | 	 * @param int $attachment_id
341 | 	 * @param string|array $size
342 | 	 * @uses is_admin, apply_filters, wp_get_attachment_url, self::validate_image_url, this::image_sizes, my_photon_url
343 | 	 * @filter image_downsize
344 | 	 * @return string|bool
345 | 	 */
346 | 	public function filter_image_downsize( $image, $attachment_id, $size ) {
347 | 		// Don't foul up the admin side of things, and provide plugins a way of preventing Photon from being applied to images.
348 | 		if ( is_admin() || apply_filters( 'my_photon_override_image_downsize', false, compact( 'image', 'attachment_id', 'size' ) ) )
349 | 			return $image;
350 | 
351 | 		// Get the image URL and proceed with Photon-ification if successful
352 | 		$image_url = wp_get_attachment_url( $attachment_id );
353 | 
354 | 		if ( $image_url ) {
355 | 			// Check if image URL should be used with Photon
356 | 			if ( ! self::validate_image_url( $image_url ) )
357 | 				return $image;
358 | 
359 | 			// If an image is requested with a size known to WordPress, use that size's settings with Photon
360 | 			if ( ( is_string( $size ) || is_int( $size ) ) && array_key_exists( $size, self::image_sizes() ) ) {
361 | 				$image_args = self::image_sizes();
362 | 				$image_args = $image_args[ $size ];
363 | 
364 | 				$photon_args = array();
365 | 
366 | 				// `full` is a special case in WP
367 | 				// To ensure filter receives consistent data regardless of requested size, `$image_args` is overridden with dimensions of original image.
368 | 				if ( 'full' == $size ) {
369 | 					$image_meta = wp_get_attachment_metadata( $attachment_id );
370 | 					if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
371 | 						// 'crop' is true so Photon's `resize` method is used
372 | 						$image_args = array(
373 | 							'width'  => $image_meta['width'],
374 | 							'height' => $image_meta['height'],
375 | 							'crop'   => true
376 | 						);
377 | 					}
378 | 				}
379 | 
380 | 				// Expose determined arguments to a filter before passing to Photon.
381 | 				$transform = $image_args['crop'] ? 'resize' : 'fit';
382 | 
383 | 				// Check specified image dimensions and account for possible zero values; photon fails to resize if a dimension is zero.
384 | 				if ( 0 === $image_args['width'] || 0 === $image_args['height'] ) {
385 | 					if ( 0 === $image_args['width'] && 0 < $image_args['height'] ) {
386 | 						$photon_args['h'] = $image_args['height'];
387 | 					} elseif ( 0 === $image_args['height'] && 0 < $image_args['width'] ) {
388 | 						$photon_args['w'] = $image_args['width'];
389 | 					}
390 | 				} else {
391 | 					$image_meta = wp_get_attachment_metadata( $attachment_id );
392 | 					if ( ( 'resize' === $transform ) && $image_meta ) {
393 | 						if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
394 | 							// Lets make sure that we don't upscale images since wp never upscales them as well.
395 | 							$smaller_width  = ( ( $image_meta['width'] < $image_args['width'] ) ? $image_meta['width'] : $image_args['width'] );
396 | 							$smaller_height = ( ( $image_meta['height'] < $image_args['height'] ) ? $image_meta['height'] : $image_args['height'] );
397 | 
398 | 							$photon_args[ $transform ] = $smaller_width . ',' . $smaller_height;
399 | 						}
400 | 					} else {
401 | 						$photon_args[ $transform ] = $image_args['width'] . ',' . $image_args['height'];
402 | 					}
403 | 				}
404 | 
405 | 				$photon_args = apply_filters( 'my_photon_image_downsize_string', $photon_args, compact( 'image_args', 'image_url', 'attachment_id', 'size', 'transform' ) );
406 | 
407 | 				// Generate Photon URL
408 | 				$image = array(
409 | 					my_photon_url( $image_url, $photon_args ),
410 | 					false,
411 | 					false
412 | 				);
413 | 			} elseif ( is_array( $size ) ) {
414 | 				// Pull width and height values from the provided array, if possible
415 | 				$width = isset( $size[0] ) ? (int) $size[0] : false;
416 | 				$height = isset( $size[1] ) ? (int) $size[1] : false;
417 | 
418 | 				// Don't bother if necessary parameters aren't passed.
419 | 				if ( ! $width || ! $height )
420 | 					return $image;
421 | 
422 | 				// Expose arguments to a filter before passing to Photon
423 | 				$photon_args = array(
424 | 					'fit' => $width . ',' . $height
425 | 				);
426 | 
427 | 				$photon_args = apply_filters( 'my_photon_image_downsize_array', $photon_args, compact( 'width', 'height', 'image_url', 'attachment_id' ) );
428 | 
429 | 				// Generate Photon URL
430 | 				$image = array(
431 | 					my_photon_url( $image_url, $photon_args ),
432 | 					false,
433 | 					false
434 | 				);
435 | 			}
436 | 		}
437 | 
438 | 		return $image;
439 | 	}
440 | 
441 | 	/**
442 | 	 ** GENERAL FUNCTIONS
443 | 	 **/
444 | 
445 | 	/**
446 | 	 * Ensure image URL is valid for Photon.
447 | 	 * 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.
448 | 	 *
449 | 	 * @param string $url
450 | 	 * @uses wp_parse_args
451 | 	 * @return bool
452 | 	 */
453 | 	protected static function validate_image_url( $url ) {
454 | 		$base_url = My_Photon_Settings::get( 'base-url' );
455 | 		$base_url_parsed = @parse_url( $base_url );
456 | 		$parsed_url = @parse_url( $url );
457 | 
458 | 		if ( ! $parsed_url ) {
459 | 			return false;
460 | 		}
461 | 
462 | 		// Parse URL and ensure needed keys exist, since the array returned by `parse_url` only includes the URL components it finds.
463 | 		$url_info = wp_parse_args( $parsed_url, array(
464 | 			'scheme' => null,
465 | 			'host'   => null,
466 | 			'port'   => null,
467 | 			'path'   => null
468 | 		) );
469 | 
470 | 		// Ensure port/protocol matches that of the image server.
471 | 		if ( $url_info['scheme'] !== $base_url_parsed['scheme']
472 | 			|| ( 'https' === $url_info['scheme']
473 | 			     && apply_filters( 'my_photon_reject_https', false )
474 | 			)
475 | 		) {
476 | 			return false;
477 | 		}
478 | 
479 | 		// Bail if no host is found
480 | 		if ( is_null( $url_info['host'] ) ) {
481 | 			return false;
482 | 		}
483 | 
484 | 		// Bail if the image alredy went through Photon
485 | 		if ( false !== strpos( $base_url, $url_info['host'] ) ) {
486 | 			return false;
487 | 		}
488 | 
489 | 		// Bail if no path is found
490 | 		if ( is_null( $url_info['path'] ) ) {
491 | 			return false;
492 | 		}
493 | 
494 | 		// Ensure image extension is acceptable
495 | 		if ( ! in_array(
496 | 			strtolower( pathinfo( $url_info['path'], PATHINFO_EXTENSION ) ),
497 | 			self::$extensions
498 | 		) ) {
499 | 			return false;
500 | 		}
501 | 
502 | 		/**
503 | 		 * Allow themes and plugins to add additional logic to determine
504 | 		 * whether the URL should be Photonized.
505 | 		 *
506 | 		 * @param bool $is_valid Whether the image URL should be Photonized.
507 | 		 * @param string $url The URL that is being examined.
508 | 		 * @param array $parsed_url The URL as passed through parse_url().
509 | 		 */
510 | 		return apply_filters(
511 | 			'photon_validate_image_url',
512 | 			true,
513 | 			$url,
514 | 			$parsed_url
515 | 		);
516 | 	}
517 | 
518 | 	/**
519 | 	 * Checks if the file exists before it passes the file to photon
520 | 	 *
521 | 	 * @param string $src The image URL
522 | 	 * @return string
523 | 	 **/
524 | 	protected static function strip_image_dimensions_maybe( $src ) {
525 | 		$stripped_src = $src;
526 | 
527 | 		// Build URL, first removing WP's resized string so we pass the original image to Photon
528 | 		if ( preg_match( '#(-\d+x\d+)\.(' . implode('|', self::$extensions ) . '){1}$#i', $src, $src_parts ) ) {
529 | 			$stripped_src = str_replace( $src_parts[1], '', $src );
530 | 			$upload_dir = wp_upload_dir();
531 | 
532 | 			// Extracts the file path to the image minus the base url
533 | 			$file_path = substr( $stripped_src, strlen ( $upload_dir['baseurl'] ) );
534 | 
535 | 			if( file_exists( $upload_dir["basedir"] . $file_path ) )
536 | 				$src = $stripped_src;
537 | 		}
538 | 
539 | 		return $src;
540 | 	}
541 | 
542 | 	/**
543 | 	 * Provide an array of available image sizes and corresponding dimensions.
544 | 	 * Similar to get_intermediate_image_sizes() except that it includes image sizes' dimensions, not just their names.
545 | 	 *
546 | 	 * @global $wp_additional_image_sizes
547 | 	 * @uses get_option
548 | 	 * @return array
549 | 	 */
550 | 	protected static function image_sizes() {
551 | 		if ( null == self::$image_sizes ) {
552 | 			global $_wp_additional_image_sizes;
553 | 
554 | 			// Populate an array matching the data structure of $_wp_additional_image_sizes so we have a consistent structure for image sizes
555 | 			$images = array(
556 | 				'thumb'  => array(
557 | 					'width'  => intval( get_option( 'thumbnail_size_w' ) ),
558 | 					'height' => intval( get_option( 'thumbnail_size_h' ) ),
559 | 					'crop'   => (bool) get_option( 'thumbnail_crop' )
560 | 				),
561 | 				'medium' => array(
562 | 					'width'  => intval( get_option( 'medium_size_w' ) ),
563 | 					'height' => intval( get_option( 'medium_size_h' ) ),
564 | 					'crop'   => false
565 | 				),
566 | 				'large'  => array(
567 | 					'width'  => intval( get_option( 'large_size_w' ) ),
568 | 					'height' => intval( get_option( 'large_size_h' ) ),
569 | 					'crop'   => false
570 | 				),
571 | 				'full'   => array(
572 | 					'width'  => null,
573 | 					'height' => null,
574 | 					'crop'   => false
575 | 				)
576 | 			);
577 | 
578 | 			// Compatibility mapping as found in wp-includes/media.php
579 | 			$images['thumbnail'] = $images['thumb'];
580 | 
581 | 			// Update class variable, merging in $_wp_additional_image_sizes if any are set
582 | 			if ( is_array( $_wp_additional_image_sizes ) && ! empty( $_wp_additional_image_sizes ) )
583 | 				self::$image_sizes = array_merge( $images, $_wp_additional_image_sizes );
584 | 			else
585 | 				self::$image_sizes = $images;
586 | 		}
587 | 
588 | 		return is_array( self::$image_sizes ) ? self::$image_sizes : array();
589 | 	}
590 | 
591 | 	/**
592 | 	 * Pass og:image URLs through Photon
593 | 	 *
594 | 	 * @param array $tags
595 | 	 * @param array $parameters
596 | 	 * @uses my_photon_url
597 | 	 * @return array
598 | 	 */
599 | 	function filter_open_graph_tags( $tags, $parameters ) {
600 | 		if ( empty( $tags['og:image'] ) ) {
601 | 			return $tags;
602 | 		}
603 | 
604 | 		$photon_args = array(
605 | 			'fit' => sprintf( '%d,%d', 2 * $parameters['image_width'], 2 * $parameters['image_height'] ),
606 | 		);
607 | 
608 | 		if ( is_array( $tags['og:image'] ) ) {
609 | 			$images = array();
610 | 			foreach ( $tags['og:image'] as $image ) {
611 | 				$images[] = my_photon_url( $image, $photon_args );
612 | 			}
613 | 			$tags['og:image'] = $images;
614 | 		} else {
615 | 			$tags['og:image'] = my_photon_url( $tags['og:image'], $photon_args );
616 | 		}
617 | 
618 | 		return $tags;
619 | 	}
620 | }
621 | My_Photon::instance();
622 | 
--------------------------------------------------------------------------------
/inc/functions.php:
--------------------------------------------------------------------------------
 1 |  '300', 'resize' => array( 123, 456 ) ), or in string form (w=123&h=456)
10 |  * @return string The raw final URL. You should run this through esc_url() before displaying it.
11 |  */
12 | function my_photon_url( $image_url, $args = array(), $scheme = null ) {
13 | 	$image_url = trim( $image_url );
14 | 
15 | 	$image_url = apply_filters( 'my_photon_pre_image_url', $image_url, $args,      $scheme );
16 | 	$args      = apply_filters( 'my_photon_pre_args',      $args,      $image_url, $scheme );
17 | 
18 | 	if ( empty( $image_url ) )
19 | 		return $image_url;
20 | 
21 | 	$image_url_parts = @parse_url( $image_url );
22 | 
23 | 	// Unable to parse
24 | 	if ( ! is_array( $image_url_parts ) || empty( $image_url_parts['host'] ) || empty( $image_url_parts['path'] ) )
25 | 		return $image_url;
26 | 
27 | 	if ( is_array( $args ) ){
28 | 		// Convert values that are arrays into strings
29 | 		foreach ( $args as $arg => $value ) {
30 | 			if ( is_array( $value ) ) {
31 | 				$args[$arg] = implode( ',', $value );
32 | 			}
33 | 		}
34 | 
35 | 		// Encode values
36 | 		// See http://core.trac.wordpress.org/ticket/17923
37 | 		$args = rawurlencode_deep( $args );
38 | 	}
39 | 
40 | 	// You can't run a Photon URL through Photon again because query strings are stripped.
41 | 	// So if the image is already a Photon URL, append the new arguments to the existing URL.
42 | 	if ( false !== strpos( My_Photon_Settings::get( 'base-url' ), $image_url_parts['host'] ) ) {
43 | 		$photon_url = add_query_arg( $args, $image_url );
44 | 
45 | 		return my_photon_url_scheme( $photon_url, $scheme );
46 | 	}
47 | 
48 | 	// This setting is Photon Server dependent
49 | 	if ( ! apply_filters( 'my_photon_any_extension_for_domain', false, $image_url_parts['host'] ) ) {
50 | 		// Photon doesn't support query strings so we ignore them and look only at the path.
51 | 		// However some source images are served via PHP so check the no-query-string extension.
52 | 		// For future proofing, this is a blacklist of common issues rather than a whitelist.
53 | 		$extension = pathinfo( $image_url_parts['path'], PATHINFO_EXTENSION );
54 | 		if ( empty( $extension ) || in_array( $extension, array( 'php' ) ) )
55 | 			return $image_url;
56 | 	}
57 | 
58 | 	$image_host_path = $image_url_parts['host'] . $image_url_parts['path'];
59 | 
60 | 	$photon_url  = My_Photon_Settings::get( 'base-url' ) . $image_host_path;
61 | 
62 | 	// This setting is Photon Server dependent
63 | 	if ( isset( $image_url_parts['query'] ) && apply_filters( 'my_photon_add_query_string_to_domain', false, $image_url_parts['host'] ) ) {
64 | 		$photon_url .= '?q=' . rawurlencode( $image_url_parts['query'] );
65 | 	}
66 | 
67 | 	if ( $args ) {
68 | 		if ( is_array( $args ) ) {
69 | 			$photon_url = add_query_arg( $args, $photon_url );
70 | 		} else {
71 | 			// You can pass a query string for complicated requests but where you still want CDN subdomain help, etc.
72 | 			$photon_url .= '?' . $args;
73 | 		}
74 | 	}
75 | 
76 | 	return my_photon_url_scheme( $photon_url, $scheme );
77 | }
78 | add_filter( 'my_photon_url', 'my_photon_url', 10, 3 );
79 | 
80 | 
81 | function my_photon_url_scheme( $url, $scheme ) {
82 | 	if ( ! in_array( $scheme, array( 'http', 'https', 'network_path' ) ) ) {
83 | 		$scheme = is_ssl() ? 'https' : 'http';
84 | 	}
85 | 
86 | 	if ( 'network_path' == $scheme ) {
87 | 		$scheme_slashes = '//';
88 | 	} else {
89 | 		$scheme_slashes = "$scheme://";
90 | 	}
91 | 
92 | 	return preg_replace( '#^[a-z:]+//#i', $scheme_slashes, $url );
93 | }
94 | 
--------------------------------------------------------------------------------
/my-photon.php:
--------------------------------------------------------------------------------
 1 |  Use of this service is for users of the Jetpack by WordPress.com plugin only,
45 | > and may be used by sites hosted on WordPress.com, or on Jetpack-connected
46 | > WordPress sites. If you move to another platform, or disconnect Jetpack from
47 | > your site, we can't promise it will continue to work.
48 | 
49 | == Changelog ==
50 | 
51 | = 0.1 =
52 | 
53 | * Initial release
--------------------------------------------------------------------------------
]+?src=["|\'](?P[^\s]+?)["|\'].*?>){1}(?:\s*)?#is', $content, $images ) ) {
 79 | 			foreach ( $images as $key => $unused ) {
 80 | 				// Simplify the output as much as possible, mostly for confirming test results.
 81 | 				if ( is_numeric( $key ) && $key > 0 )
 82 | 					unset( $images[$key] );
 83 | 			}
 84 | 
 85 | 			return $images;
 86 | 		}
 87 | 
 88 | 		return array();
 89 | 	}
 90 | 
 91 | 	/**
 92 | 	 * Try to determine height and width from strings WP appends to resized image filenames.
 93 | 	 *
 94 | 	 * @param string $src The image URL.
 95 | 	 * @return array An array consisting of width and height.
 96 | 	 */
 97 | 	public static function parse_dimensions_from_filename( $src ) {
 98 | 		$width_height_string = array();
 99 | 
100 | 		if ( preg_match( '#-(\d+)x(\d+)\.(?:' . implode('|', self::$extensions ) . '){1}$#i', $src, $width_height_string ) ) {
101 | 			$width = (int) $width_height_string[1];
102 | 			$height = (int) $width_height_string[2];
103 | 
104 | 			if ( $width && $height )
105 | 				return array( $width, $height );
106 | 		}
107 | 
108 | 		return array( false, false );
109 | 	}
110 | 
111 | 	/**
112 | 	 * Identify images in post content, and if images are local (uploaded to the current site), pass through Photon.
113 | 	 *
114 | 	 * @param string $content
115 | 	 * @uses self::validate_image_url, apply_filters, my_photon_url, esc_url
116 | 	 * @filter the_content
117 | 	 * @return string
118 | 	 */
119 | 	public static function filter_the_content( $content ) {
120 | 
121 | 		$images = My_Photon::parse_images_from_html( $content );
122 | 
123 | 		if ( ! empty( $images ) ) {
124 | 			global $content_width;
125 | 
126 | 			$image_sizes = self::image_sizes();
127 | 			$upload_dir = wp_upload_dir();
128 | 
129 | 			foreach ( $images[0] as $index => $tag ) {
130 | 				// Default to resize, though fit may be used in certain cases where a dimension cannot be ascertained
131 | 				$transform = 'resize';
132 | 
133 | 				// Start with a clean attachment ID each time
134 | 				$attachment_id = false;
135 | 
136 | 				// Flag if we need to munge a fullsize URL
137 | 				$fullsize_url = false;
138 | 
139 | 				// Identify image source
140 | 				$src = $src_orig = $images['img_url'][ $index ];
141 | 
142 | 				// Allow specific images to be skipped
143 | 				if ( apply_filters( 'my_photon_skip_image', false, $src, $tag ) )
144 | 					continue;
145 | 
146 | 				// Support Automattic's Lazy Load plugin
147 | 				// Can't modify $tag yet as we need unadulterated version later
148 | 				if ( preg_match( '#data-lazy-src=["|\'](.+?)["|\']#i', $images['img_tag'][ $index ], $lazy_load_src ) ) {
149 | 					$placeholder_src = $placeholder_src_orig = $src;
150 | 					$src = $src_orig = $lazy_load_src[1];
151 | 				}
152 | 
153 | 				// Check if image URL should be used with Photon
154 | 				if ( self::validate_image_url( $src ) ) {
155 | 					// Find the width and height attributes
156 | 					$width = $height = false;
157 | 
158 | 					// First, check the image tag
159 | 					if ( preg_match( '#width=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $width_string ) )
160 | 						$width = $width_string[1];
161 | 
162 | 					if ( preg_match( '#height=["|\']?([\d%]+)["|\']?#i', $images['img_tag'][ $index ], $height_string ) )
163 | 						$height = $height_string[1];
164 | 
165 | 					// Can't pass both a relative width and height, so unset the height in favor of not breaking the horizontal layout.
166 | 					if ( false !== strpos( $width, '%' ) && false !== strpos( $height, '%' ) )
167 | 						$width = $height = false;
168 | 
169 | 					// Detect WP registered image size from HTML class
170 | 					if ( preg_match( '#class=["|\']?[^"\']*size-([^"\'\s]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $size ) ) {
171 | 						$size = array_pop( $size );
172 | 
173 | 						if ( false === $width && false === $height && 'full' != $size && array_key_exists( $size, $image_sizes ) ) {
174 | 							$width = (int) $image_sizes[ $size ]['width'];
175 | 							$height = (int) $image_sizes[ $size ]['height'];
176 | 							$transform = $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
177 | 						}
178 | 					} else {
179 | 						unset( $size );
180 | 					}
181 | 
182 | 					// WP Attachment ID, if uploaded to this site
183 | 					if ( preg_match( '#class=["|\']?[^"\']*wp-image-([\d]+)[^"\']*["|\']?#i', $images['img_tag'][ $index ], $attachment_id ) && ( 0 === strpos( $src, $upload_dir['baseurl'] ) || apply_filters( 'my_photon_image_is_local', false, compact( 'src', 'tag', 'images', 'index' ) ) ) ) {
184 | 						$attachment_id = intval( array_pop( $attachment_id ) );
185 | 
186 | 						if ( $attachment_id ) {
187 | 							$attachment = get_post( $attachment_id );
188 | 
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 | 
193 | 								if ( self::validate_image_url( $src_per_wp[0] ) ) {
194 | 									$src = $src_per_wp[0];
195 | 									$fullsize_url = true;
196 | 
197 | 									// Prevent image distortion if a detected dimension exceeds the image's natural dimensions
198 | 									if ( ( false !== $width && $width > $src_per_wp[1] ) || ( false !== $height && $height > $src_per_wp[2] ) ) {
199 | 										$width = false == $width ? false : min( $width, $src_per_wp[1] );
200 | 										$height = false == $height ? false : min( $height, $src_per_wp[2] );
201 | 									}
202 | 
203 | 									// If no width and height are found, max out at source image's natural dimensions
204 | 									// Otherwise, respect registered image sizes' cropping setting
205 | 									if ( false == $width && false == $height ) {
206 | 										$width = $src_per_wp[1];
207 | 										$height = $src_per_wp[2];
208 | 										$transform = 'fit';
209 | 									} elseif ( isset( $size ) && array_key_exists( $size, $image_sizes ) && isset( $image_sizes[ $size ]['crop'] ) ) {
210 | 										$transform = (bool) $image_sizes[ $size ]['crop'] ? 'resize' : 'fit';
211 | 									}
212 | 								}
213 | 							} else {
214 | 								unset( $attachment_id );
215 | 								unset( $attachment );
216 | 							}
217 | 						}
218 | 					}
219 | 
220 | 					// If image tag lacks width and height arguments, try to determine from strings WP appends to resized image filenames.
221 | 					if ( false === $width && false === $height ) {
222 | 						list( $width, $height ) = My_Photon::parse_dimensions_from_filename( $src );
223 | 					}
224 | 
225 | 					// If width is available, constrain to $content_width
226 | 					if ( false !== $width && false === strpos( $width, '%' ) && is_numeric( $content_width ) ) {
227 | 						if ( $width > $content_width && false !== $height && false === strpos( $height, '%' ) ) {
228 | 							$height = round( ( $content_width * $height ) / $width );
229 | 							$width = $content_width;
230 | 						} elseif ( $width > $content_width ) {
231 | 							$width = $content_width;
232 | 						}
233 | 					}
234 | 
235 | 					// Set a width if none is found and $content_width is available
236 | 					// If width is set in this manner and height is available, use `fit` instead of `resize` to prevent skewing
237 | 					if ( false === $width && is_numeric( $content_width ) ) {
238 | 						$width = (int) $content_width;
239 | 
240 | 						if ( false !== $height )
241 | 							$transform = 'fit';
242 | 					}
243 | 
244 | 					// Detect if image source is for a custom-cropped thumbnail and prevent further URL manipulation.
245 | 					if ( ! $fullsize_url && preg_match_all( '#-e[a-z0-9]+(-\d+x\d+)?\.(' . implode('|', self::$extensions ) . '){1}$#i', basename( $src ), $filename ) )
246 | 						$fullsize_url = true;
247 | 
248 | 					// Build URL, first maybe removing WP's resized string so we pass the original image to Photon
249 | 					if ( ! $fullsize_url ) {
250 | 						$src = self::strip_image_dimensions_maybe( $src );
251 | 					}
252 | 
253 | 					// Build array of Photon args and expose to filter before passing to Photon URL function
254 | 					$args = array();
255 | 
256 | 					if ( false !== $width && false !== $height && false === strpos( $width, '%' ) && false === strpos( $height, '%' ) )
257 | 						$args[ $transform ] = $width . ',' . $height;
258 | 					elseif ( false !== $width )
259 | 						$args['w'] = $width;
260 | 					elseif ( false !== $height )
261 | 						$args['h'] = $height;
262 | 
263 | 					$args = apply_filters( 'my_photon_post_image_args', $args, compact( 'tag', 'src', 'src_orig', 'width', 'height' ) );
264 | 
265 | 					$photon_url = my_photon_url( $src, $args );
266 | 
267 | 					// Modify image tag if Photon function provides a URL
268 | 					// 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.
269 | 					if ( $src != $photon_url ) {
270 | 						$new_tag = $tag;
271 | 
272 | 						// If present, replace the link href with a Photoned URL for the full-size image.
273 | 						if ( ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) )
274 | 							$new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . my_photon_url( $images['link_url'][ $index ] ) . '\2', $new_tag, 1 );
275 | 
276 | 						// Supplant the original source value with our Photon URL
277 | 						$photon_url = esc_url( $photon_url );
278 | 						$new_tag = str_replace( $src_orig, $photon_url, $new_tag );
279 | 
280 | 						// If Lazy Load is in use, pass placeholder image through Photon
281 | 						if ( isset( $placeholder_src ) && self::validate_image_url( $placeholder_src ) ) {
282 | 							$placeholder_src = my_photon_url( $placeholder_src );
283 | 
284 | 							if ( $placeholder_src != $placeholder_src_orig )
285 | 								$new_tag = str_replace( $placeholder_src_orig, esc_url( $placeholder_src ), $new_tag );
286 | 
287 | 							unset( $placeholder_src );
288 | 						}
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 | 
293 | 						// Tag an image for dimension checking
294 | 						$new_tag = preg_replace( '#(\s?/)?>()?$#i', ' data-recalc-dims="1"\1>\2', $new_tag );
295 | 
296 | 						// Replace original tag with modified version
297 | 						$content = str_replace( $tag, $new_tag, $content );
298 | 					}
299 | 				} elseif ( false !== strpos( $src, My_Photon_Settings::get( 'base-url' ) ) && ! empty( $images['link_url'][ $index ] ) && self::validate_image_url( $images['link_url'][ $index ] ) ) {
300 | 					$new_tag = preg_replace( '#(href=["|\'])' . $images['link_url'][ $index ] . '(["|\'])#i', '\1' . my_photon_url( $images['link_url'][ $index ] ) . '\2', $tag, 1 );
301 | 
302 | 					$content = str_replace( $tag, $new_tag, $content );
303 | 				}
304 | 			}
305 | 		}
306 | 
307 | 		return $content;
308 | 	}
309 | 
310 | 	/**
311 | 	 * Filter Core galleries
312 | 	 *
313 | 	 * @param array $galleries Gallery array.
314 | 	 * @return array
315 | 	 */
316 | 	public static function filter_the_galleries( $galleries ) {
317 | 		if ( empty( $galleries ) || ! is_array( $galleries ) ) {
318 | 			return $galleries;
319 | 		}
320 | 
321 | 		// Pass by reference, so we can modify them in place.
322 | 		foreach ( $galleries as &$this_gallery ) {
323 | 			if ( is_string( $this_gallery ) ) {
324 | 				$this_gallery = self::filter_the_content( $this_gallery );
325 | 			}
326 | 		}
327 | 		unset( $this_gallery ); // break the reference.
328 | 
329 | 		return $galleries;
330 | 	}
331 | 
332 | 	/**
333 | 	 ** CORE IMAGE RETRIEVAL
334 | 	 **/
335 | 
336 | 	/**
337 | 	 * Filter post thumbnail image retrieval, passing images through Photon
338 | 	 *
339 | 	 * @param string|bool $image
340 | 	 * @param int $attachment_id
341 | 	 * @param string|array $size
342 | 	 * @uses is_admin, apply_filters, wp_get_attachment_url, self::validate_image_url, this::image_sizes, my_photon_url
343 | 	 * @filter image_downsize
344 | 	 * @return string|bool
345 | 	 */
346 | 	public function filter_image_downsize( $image, $attachment_id, $size ) {
347 | 		// Don't foul up the admin side of things, and provide plugins a way of preventing Photon from being applied to images.
348 | 		if ( is_admin() || apply_filters( 'my_photon_override_image_downsize', false, compact( 'image', 'attachment_id', 'size' ) ) )
349 | 			return $image;
350 | 
351 | 		// Get the image URL and proceed with Photon-ification if successful
352 | 		$image_url = wp_get_attachment_url( $attachment_id );
353 | 
354 | 		if ( $image_url ) {
355 | 			// Check if image URL should be used with Photon
356 | 			if ( ! self::validate_image_url( $image_url ) )
357 | 				return $image;
358 | 
359 | 			// If an image is requested with a size known to WordPress, use that size's settings with Photon
360 | 			if ( ( is_string( $size ) || is_int( $size ) ) && array_key_exists( $size, self::image_sizes() ) ) {
361 | 				$image_args = self::image_sizes();
362 | 				$image_args = $image_args[ $size ];
363 | 
364 | 				$photon_args = array();
365 | 
366 | 				// `full` is a special case in WP
367 | 				// To ensure filter receives consistent data regardless of requested size, `$image_args` is overridden with dimensions of original image.
368 | 				if ( 'full' == $size ) {
369 | 					$image_meta = wp_get_attachment_metadata( $attachment_id );
370 | 					if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
371 | 						// 'crop' is true so Photon's `resize` method is used
372 | 						$image_args = array(
373 | 							'width'  => $image_meta['width'],
374 | 							'height' => $image_meta['height'],
375 | 							'crop'   => true
376 | 						);
377 | 					}
378 | 				}
379 | 
380 | 				// Expose determined arguments to a filter before passing to Photon.
381 | 				$transform = $image_args['crop'] ? 'resize' : 'fit';
382 | 
383 | 				// Check specified image dimensions and account for possible zero values; photon fails to resize if a dimension is zero.
384 | 				if ( 0 === $image_args['width'] || 0 === $image_args['height'] ) {
385 | 					if ( 0 === $image_args['width'] && 0 < $image_args['height'] ) {
386 | 						$photon_args['h'] = $image_args['height'];
387 | 					} elseif ( 0 === $image_args['height'] && 0 < $image_args['width'] ) {
388 | 						$photon_args['w'] = $image_args['width'];
389 | 					}
390 | 				} else {
391 | 					$image_meta = wp_get_attachment_metadata( $attachment_id );
392 | 					if ( ( 'resize' === $transform ) && $image_meta ) {
393 | 						if ( isset( $image_meta['width'], $image_meta['height'] ) ) {
394 | 							// Lets make sure that we don't upscale images since wp never upscales them as well.
395 | 							$smaller_width  = ( ( $image_meta['width'] < $image_args['width'] ) ? $image_meta['width'] : $image_args['width'] );
396 | 							$smaller_height = ( ( $image_meta['height'] < $image_args['height'] ) ? $image_meta['height'] : $image_args['height'] );
397 | 
398 | 							$photon_args[ $transform ] = $smaller_width . ',' . $smaller_height;
399 | 						}
400 | 					} else {
401 | 						$photon_args[ $transform ] = $image_args['width'] . ',' . $image_args['height'];
402 | 					}
403 | 				}
404 | 
405 | 				$photon_args = apply_filters( 'my_photon_image_downsize_string', $photon_args, compact( 'image_args', 'image_url', 'attachment_id', 'size', 'transform' ) );
406 | 
407 | 				// Generate Photon URL
408 | 				$image = array(
409 | 					my_photon_url( $image_url, $photon_args ),
410 | 					false,
411 | 					false
412 | 				);
413 | 			} elseif ( is_array( $size ) ) {
414 | 				// Pull width and height values from the provided array, if possible
415 | 				$width = isset( $size[0] ) ? (int) $size[0] : false;
416 | 				$height = isset( $size[1] ) ? (int) $size[1] : false;
417 | 
418 | 				// Don't bother if necessary parameters aren't passed.
419 | 				if ( ! $width || ! $height )
420 | 					return $image;
421 | 
422 | 				// Expose arguments to a filter before passing to Photon
423 | 				$photon_args = array(
424 | 					'fit' => $width . ',' . $height
425 | 				);
426 | 
427 | 				$photon_args = apply_filters( 'my_photon_image_downsize_array', $photon_args, compact( 'width', 'height', 'image_url', 'attachment_id' ) );
428 | 
429 | 				// Generate Photon URL
430 | 				$image = array(
431 | 					my_photon_url( $image_url, $photon_args ),
432 | 					false,
433 | 					false
434 | 				);
435 | 			}
436 | 		}
437 | 
438 | 		return $image;
439 | 	}
440 | 
441 | 	/**
442 | 	 ** GENERAL FUNCTIONS
443 | 	 **/
444 | 
445 | 	/**
446 | 	 * Ensure image URL is valid for Photon.
447 | 	 * 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.
448 | 	 *
449 | 	 * @param string $url
450 | 	 * @uses wp_parse_args
451 | 	 * @return bool
452 | 	 */
453 | 	protected static function validate_image_url( $url ) {
454 | 		$base_url = My_Photon_Settings::get( 'base-url' );
455 | 		$base_url_parsed = @parse_url( $base_url );
456 | 		$parsed_url = @parse_url( $url );
457 | 
458 | 		if ( ! $parsed_url ) {
459 | 			return false;
460 | 		}
461 | 
462 | 		// Parse URL and ensure needed keys exist, since the array returned by `parse_url` only includes the URL components it finds.
463 | 		$url_info = wp_parse_args( $parsed_url, array(
464 | 			'scheme' => null,
465 | 			'host'   => null,
466 | 			'port'   => null,
467 | 			'path'   => null
468 | 		) );
469 | 
470 | 		// Ensure port/protocol matches that of the image server.
471 | 		if ( $url_info['scheme'] !== $base_url_parsed['scheme']
472 | 			|| ( 'https' === $url_info['scheme']
473 | 			     && apply_filters( 'my_photon_reject_https', false )
474 | 			)
475 | 		) {
476 | 			return false;
477 | 		}
478 | 
479 | 		// Bail if no host is found
480 | 		if ( is_null( $url_info['host'] ) ) {
481 | 			return false;
482 | 		}
483 | 
484 | 		// Bail if the image alredy went through Photon
485 | 		if ( false !== strpos( $base_url, $url_info['host'] ) ) {
486 | 			return false;
487 | 		}
488 | 
489 | 		// Bail if no path is found
490 | 		if ( is_null( $url_info['path'] ) ) {
491 | 			return false;
492 | 		}
493 | 
494 | 		// Ensure image extension is acceptable
495 | 		if ( ! in_array(
496 | 			strtolower( pathinfo( $url_info['path'], PATHINFO_EXTENSION ) ),
497 | 			self::$extensions
498 | 		) ) {
499 | 			return false;
500 | 		}
501 | 
502 | 		/**
503 | 		 * Allow themes and plugins to add additional logic to determine
504 | 		 * whether the URL should be Photonized.
505 | 		 *
506 | 		 * @param bool $is_valid Whether the image URL should be Photonized.
507 | 		 * @param string $url The URL that is being examined.
508 | 		 * @param array $parsed_url The URL as passed through parse_url().
509 | 		 */
510 | 		return apply_filters(
511 | 			'photon_validate_image_url',
512 | 			true,
513 | 			$url,
514 | 			$parsed_url
515 | 		);
516 | 	}
517 | 
518 | 	/**
519 | 	 * Checks if the file exists before it passes the file to photon
520 | 	 *
521 | 	 * @param string $src The image URL
522 | 	 * @return string
523 | 	 **/
524 | 	protected static function strip_image_dimensions_maybe( $src ) {
525 | 		$stripped_src = $src;
526 | 
527 | 		// Build URL, first removing WP's resized string so we pass the original image to Photon
528 | 		if ( preg_match( '#(-\d+x\d+)\.(' . implode('|', self::$extensions ) . '){1}$#i', $src, $src_parts ) ) {
529 | 			$stripped_src = str_replace( $src_parts[1], '', $src );
530 | 			$upload_dir = wp_upload_dir();
531 | 
532 | 			// Extracts the file path to the image minus the base url
533 | 			$file_path = substr( $stripped_src, strlen ( $upload_dir['baseurl'] ) );
534 | 
535 | 			if( file_exists( $upload_dir["basedir"] . $file_path ) )
536 | 				$src = $stripped_src;
537 | 		}
538 | 
539 | 		return $src;
540 | 	}
541 | 
542 | 	/**
543 | 	 * Provide an array of available image sizes and corresponding dimensions.
544 | 	 * Similar to get_intermediate_image_sizes() except that it includes image sizes' dimensions, not just their names.
545 | 	 *
546 | 	 * @global $wp_additional_image_sizes
547 | 	 * @uses get_option
548 | 	 * @return array
549 | 	 */
550 | 	protected static function image_sizes() {
551 | 		if ( null == self::$image_sizes ) {
552 | 			global $_wp_additional_image_sizes;
553 | 
554 | 			// Populate an array matching the data structure of $_wp_additional_image_sizes so we have a consistent structure for image sizes
555 | 			$images = array(
556 | 				'thumb'  => array(
557 | 					'width'  => intval( get_option( 'thumbnail_size_w' ) ),
558 | 					'height' => intval( get_option( 'thumbnail_size_h' ) ),
559 | 					'crop'   => (bool) get_option( 'thumbnail_crop' )
560 | 				),
561 | 				'medium' => array(
562 | 					'width'  => intval( get_option( 'medium_size_w' ) ),
563 | 					'height' => intval( get_option( 'medium_size_h' ) ),
564 | 					'crop'   => false
565 | 				),
566 | 				'large'  => array(
567 | 					'width'  => intval( get_option( 'large_size_w' ) ),
568 | 					'height' => intval( get_option( 'large_size_h' ) ),
569 | 					'crop'   => false
570 | 				),
571 | 				'full'   => array(
572 | 					'width'  => null,
573 | 					'height' => null,
574 | 					'crop'   => false
575 | 				)
576 | 			);
577 | 
578 | 			// Compatibility mapping as found in wp-includes/media.php
579 | 			$images['thumbnail'] = $images['thumb'];
580 | 
581 | 			// Update class variable, merging in $_wp_additional_image_sizes if any are set
582 | 			if ( is_array( $_wp_additional_image_sizes ) && ! empty( $_wp_additional_image_sizes ) )
583 | 				self::$image_sizes = array_merge( $images, $_wp_additional_image_sizes );
584 | 			else
585 | 				self::$image_sizes = $images;
586 | 		}
587 | 
588 | 		return is_array( self::$image_sizes ) ? self::$image_sizes : array();
589 | 	}
590 | 
591 | 	/**
592 | 	 * Pass og:image URLs through Photon
593 | 	 *
594 | 	 * @param array $tags
595 | 	 * @param array $parameters
596 | 	 * @uses my_photon_url
597 | 	 * @return array
598 | 	 */
599 | 	function filter_open_graph_tags( $tags, $parameters ) {
600 | 		if ( empty( $tags['og:image'] ) ) {
601 | 			return $tags;
602 | 		}
603 | 
604 | 		$photon_args = array(
605 | 			'fit' => sprintf( '%d,%d', 2 * $parameters['image_width'], 2 * $parameters['image_height'] ),
606 | 		);
607 | 
608 | 		if ( is_array( $tags['og:image'] ) ) {
609 | 			$images = array();
610 | 			foreach ( $tags['og:image'] as $image ) {
611 | 				$images[] = my_photon_url( $image, $photon_args );
612 | 			}
613 | 			$tags['og:image'] = $images;
614 | 		} else {
615 | 			$tags['og:image'] = my_photon_url( $tags['og:image'], $photon_args );
616 | 		}
617 | 
618 | 		return $tags;
619 | 	}
620 | }
621 | My_Photon::instance();
622 | 
--------------------------------------------------------------------------------
/inc/functions.php:
--------------------------------------------------------------------------------
 1 |  '300', 'resize' => array( 123, 456 ) ), or in string form (w=123&h=456)
10 |  * @return string The raw final URL. You should run this through esc_url() before displaying it.
11 |  */
12 | function my_photon_url( $image_url, $args = array(), $scheme = null ) {
13 | 	$image_url = trim( $image_url );
14 | 
15 | 	$image_url = apply_filters( 'my_photon_pre_image_url', $image_url, $args,      $scheme );
16 | 	$args      = apply_filters( 'my_photon_pre_args',      $args,      $image_url, $scheme );
17 | 
18 | 	if ( empty( $image_url ) )
19 | 		return $image_url;
20 | 
21 | 	$image_url_parts = @parse_url( $image_url );
22 | 
23 | 	// Unable to parse
24 | 	if ( ! is_array( $image_url_parts ) || empty( $image_url_parts['host'] ) || empty( $image_url_parts['path'] ) )
25 | 		return $image_url;
26 | 
27 | 	if ( is_array( $args ) ){
28 | 		// Convert values that are arrays into strings
29 | 		foreach ( $args as $arg => $value ) {
30 | 			if ( is_array( $value ) ) {
31 | 				$args[$arg] = implode( ',', $value );
32 | 			}
33 | 		}
34 | 
35 | 		// Encode values
36 | 		// See http://core.trac.wordpress.org/ticket/17923
37 | 		$args = rawurlencode_deep( $args );
38 | 	}
39 | 
40 | 	// You can't run a Photon URL through Photon again because query strings are stripped.
41 | 	// So if the image is already a Photon URL, append the new arguments to the existing URL.
42 | 	if ( false !== strpos( My_Photon_Settings::get( 'base-url' ), $image_url_parts['host'] ) ) {
43 | 		$photon_url = add_query_arg( $args, $image_url );
44 | 
45 | 		return my_photon_url_scheme( $photon_url, $scheme );
46 | 	}
47 | 
48 | 	// This setting is Photon Server dependent
49 | 	if ( ! apply_filters( 'my_photon_any_extension_for_domain', false, $image_url_parts['host'] ) ) {
50 | 		// Photon doesn't support query strings so we ignore them and look only at the path.
51 | 		// However some source images are served via PHP so check the no-query-string extension.
52 | 		// For future proofing, this is a blacklist of common issues rather than a whitelist.
53 | 		$extension = pathinfo( $image_url_parts['path'], PATHINFO_EXTENSION );
54 | 		if ( empty( $extension ) || in_array( $extension, array( 'php' ) ) )
55 | 			return $image_url;
56 | 	}
57 | 
58 | 	$image_host_path = $image_url_parts['host'] . $image_url_parts['path'];
59 | 
60 | 	$photon_url  = My_Photon_Settings::get( 'base-url' ) . $image_host_path;
61 | 
62 | 	// This setting is Photon Server dependent
63 | 	if ( isset( $image_url_parts['query'] ) && apply_filters( 'my_photon_add_query_string_to_domain', false, $image_url_parts['host'] ) ) {
64 | 		$photon_url .= '?q=' . rawurlencode( $image_url_parts['query'] );
65 | 	}
66 | 
67 | 	if ( $args ) {
68 | 		if ( is_array( $args ) ) {
69 | 			$photon_url = add_query_arg( $args, $photon_url );
70 | 		} else {
71 | 			// You can pass a query string for complicated requests but where you still want CDN subdomain help, etc.
72 | 			$photon_url .= '?' . $args;
73 | 		}
74 | 	}
75 | 
76 | 	return my_photon_url_scheme( $photon_url, $scheme );
77 | }
78 | add_filter( 'my_photon_url', 'my_photon_url', 10, 3 );
79 | 
80 | 
81 | function my_photon_url_scheme( $url, $scheme ) {
82 | 	if ( ! in_array( $scheme, array( 'http', 'https', 'network_path' ) ) ) {
83 | 		$scheme = is_ssl() ? 'https' : 'http';
84 | 	}
85 | 
86 | 	if ( 'network_path' == $scheme ) {
87 | 		$scheme_slashes = '//';
88 | 	} else {
89 | 		$scheme_slashes = "$scheme://";
90 | 	}
91 | 
92 | 	return preg_replace( '#^[a-z:]+//#i', $scheme_slashes, $url );
93 | }
94 | 
--------------------------------------------------------------------------------
/my-photon.php:
--------------------------------------------------------------------------------
 1 |  Use of this service is for users of the Jetpack by WordPress.com plugin only,
45 | > and may be used by sites hosted on WordPress.com, or on Jetpack-connected
46 | > WordPress sites. If you move to another platform, or disconnect Jetpack from
47 | > your site, we can't promise it will continue to work.
48 | 
49 | == Changelog ==
50 | 
51 | = 0.1 =
52 | 
53 | * Initial release
--------------------------------------------------------------------------------