Completed
Push — master ( 6852ce...4bf2d8 )
by
unknown
12s
created

Images_Via_Imgix::get_option()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 2
nc 2
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
class Images_Via_Imgix {
4
5
	/**
6
	 * The instance of the class.
7
	 *
8
	 * @var Images_Via_Imgix
9
	 */
10
	protected static $instance;
11
12
	/**
13
	 * Plugin options
14
	 *
15
	 * @var array
16
	 */
17
	protected $options = [];
18
19
	/**
20
	 * Buffer is started by plugin and should be ended on shutdown.
21
	 *
22
	 * @var bool
23
	 */
24
	protected $buffer_started = false;
25
26
	/**
27
	 * ImagesViaImgix constructor.
28
	 */
29
	public function __construct() {
30
		$this->options = get_option( 'imgix_settings', [] );
31
32
		// Change filter load order to ensure it loads after other CDN url transformations i.e. Amazon S3 which loads at position 99.
33
		add_filter( 'wp_get_attachment_url', [ $this, 'replace_image_url' ], 100 );
34
		add_filter( 'imgix/add-image-url', [ $this, 'replace_image_url' ] );
35
36
		add_filter( 'image_downsize', [ $this, 'image_downsize' ], 10, 3 );
37
38
		add_filter( 'wp_calculate_image_srcset', [ $this, 'calculate_image_srcset' ], 10, 5 );
39
40
		add_filter( 'the_content', [ $this, 'replace_images_in_content' ] );
41
		add_action( 'wp_head', [ $this, 'prefetch_cdn' ], 1 );
42
43
		add_action( 'after_setup_theme', [ $this, 'buffer_start_for_retina' ] );
44
		add_action( 'shutdown', [ $this, 'buffer_end_for_retina' ], 0 );
45
	}
46
47
	/**
48
	 * Plugin loader instance.
49
	 *
50
	 * @return Images_Via_Imgix
51
	 */
52
	public static function instance() {
53
		if ( ! isset( self::$instance ) ) {
54
			self::$instance = new self;
55
		}
56
57
		return self::$instance;
58
	}
59
60
	/**
61
	 * Set a single option.
62
	 *
63
	 * @param string $key
64
	 * @param mixed $value
65
	 */
66
	public function set_option( $key, $value ) {
67
		$this->options[ $key ] = $value;
68
	}
69
70
	/**
71
	 * Get a single option.
72
	 *
73
	 * @param  string $key
74
	 * @param  mixed $default
75
	 * @return mixed
76
	 */
77
	public function get_option( $key, $default = '' ) {
78
		return array_key_exists( $key, $this->options ) ? $this->options[ $key ] : $default;
79
	}
80
81
	/**
82
	 * Override options from settings.
83
	 * Used in unit tests.
84
	 *
85
	 * @param array $options
86
	 */
87
	public function set_options( $options ) {
88
		$this->options = $options;
89
	}
90
91
	/**
92
	 * Find all img tags with sources matching "imgix.net" without the parameter
93
	 * "srcset" and add the "srcset" parameter to all those images, appending a new
94
	 * source using the "dpr=2" modifier.
95
	 *
96
	 * @param $content
97
	 *
98
	 * @return string Content with retina-enriched image tags.
99
	 */
100
	public function add_retina( $content ) {
101
		$pattern = '/<img((?![^>]+srcset )([^>]*)';
102
		$pattern .= 'src=[\'"]([^\'"]*imgix.net[^\'"]*\?[^\'"]*w=[^\'"]*)[\'"]([^>]*)*?)>/i';
103
		$repl    = '<img$2src="$3" srcset="${3}, ${3}&amp;dpr=2 2x, ${3}&amp;dpr=3 3x,"$4>';
104
		$content = preg_replace( $pattern, $repl, $content );
105
106
		return preg_replace( $pattern, $repl, $content );
107
	}
108
109
	/**
110
	 * Modify image urls for attachments to use imgix host.
111
	 *
112
	 * @param string $url
113
	 *
114
	 * @return string
115
	 */
116
	public function replace_image_url( $url ) {
117
		if ( ! empty ( $this->options['cdn_link'] ) ) {
118
			$parsed_url = parse_url( $url );
119
120
			//Check if image is hosted on current site url -OR- the CDN url specified. Using strpos because we're comparing the host to a full CDN url.
121
			if (
122
				isset( $parsed_url['host'], $parsed_url['path'] )
123
				&& ($parsed_url['host'] === parse_url( home_url( '/' ), PHP_URL_HOST ) || ( isset($this->options['external_cdn_link']) && ! empty($this->options['external_cdn_link']) && strpos( $this->options['external_cdn_link'], $parsed_url['host']) !== false ) )
124
				&& preg_match( '/\.(jpg|jpeg|gif|png)$/i', $parsed_url['path'] )
125
			) {
126
				$cdn = parse_url( $this->options['cdn_link'] );
127
128
				foreach ( [ 'scheme', 'host', 'port' ] as $url_part ) {
129
					if ( isset( $cdn[ $url_part ] ) ) {
130
						$parsed_url[ $url_part ] = $cdn[ $url_part ];
131
					} else {
132
						unset( $parsed_url[ $url_part ] );
133
					}
134
				}
135
136
				if ( ! empty( $this->options['external_cdn_link'] ) ) {
137
					$cdn_path = parse_url( $this->options['external_cdn_link'],  PHP_URL_PATH );
138
139
					if ( isset( $cdn_path, $parsed_url['path'] ) && $cdn_path !== '/' && ! empty( $parsed_url['path'] ) ) {
140
						$parsed_url['path'] = str_replace( $cdn_path, '', $parsed_url['path'] );
141
					}
142
				}
143
144
				$url = http_build_url( $parsed_url );
0 ignored issues
show
Security Bug introduced by
It seems like $parsed_url defined by parse_url($url) on line 118 can also be of type false; however, http_build_url() does only seem to accept array, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
145
146
				$url = add_query_arg( $this->get_global_params(), $url );
147
			}
148
		}
149
150
		return $url;
151
	}
152
153
	/**
154
	 * Set params when running image_downsize
155
	 *
156
	 * @param false|array  $return
157
	 * @param int          $attachment_id
158
	 * @param string|array $size
159
	 *
160
	 * @return false|array
161
	 */
162
	public function image_downsize( $return, $attachment_id, $size ) {
163
		if ( ! empty ( $this->options['cdn_link'] ) ) {
164
			$img_url = wp_get_attachment_url( $attachment_id );
165
166
			$params = [];
167
			if ( is_array( $size ) ) {
168
				$params['w'] = $width = isset( $size[0] ) ? $size[0] : 0;
169
				$params['h'] = $height = isset( $size[1] ) ? $size[1] : 0;
170
			} else {
171
				$available_sizes = $this->get_all_defined_sizes();
172
				if ( isset( $available_sizes[ $size ] ) ) {
173
					$size        = $available_sizes[ $size ];
174
					$params['w'] = $width = $size['width'];
175
					$params['h'] = $height = $size['height'];
176
				}
177
			}
178
179
			$params = array_filter( $params );
180
181
			$img_url = add_query_arg( $params, $img_url );
182
183
			if ( ! isset( $width ) || ! isset( $height ) ) {
184
				// any other type: use the real image
185
				$meta   = wp_get_attachment_metadata( $attachment_id );
186
187
				// Image sizes is missing for pdf thumbnails
188
				$meta['width']  = isset( $meta['width'] ) ? $meta['width'] : 0;
189
				$meta['height'] = isset( $meta['height'] ) ? $meta['height'] : 0;
190
191
				$width  = isset( $width ) ? $width : $meta['width'];
192
				$height = isset( $height ) ? $height : $meta['height'];
193
			}
194
195
			$return = [ $img_url, $width, $height, true ];
196
		}
197
198
		return $return;
199
	}
200
201
	/**
202
	 * Change url for images in srcset
203
	 *
204
	 * @param array  $sources
205
	 * @param array  $size_array
206
	 * @param string $image_src
207
	 * @param array  $image_meta
208
	 * @param int    $attachment_id
209
	 *
210
	 * @return array
211
	 */
212
	public function calculate_image_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) {
213
		if ( ! empty ( $this->options['cdn_link'] ) ) {
214
			foreach ( $sources as $i => $image_size ) {
215
				if ( $image_size['descriptor'] === 'w' ) {
216
					if ( $attachment_id ) {
217
						$image_src = wp_get_attachment_url( $attachment_id );
218
					}
219
220
					$image_src            = remove_query_arg( 'h', $image_src );
221
					$sources[ $i ]['url'] = add_query_arg( 'w', $image_size['value'], $image_src );
222
				}
223
			}
224
		}
225
226
		return $sources;
227
	}
228
229
	/**
230
	 * Modify image urls in content to use imgix host.
231
	 *
232
	 * @param $content
233
	 *
234
	 * @return string
235
	 */
236
	public function replace_images_in_content( $content ) {
237
		// Added null to apply filters wp_get_attachment_url to improve compatibility with https://en-gb.wordpress.org/plugins/amazon-s3-and-cloudfront/ - does not break wordpress if the plugin isn't present.
238
		if ( ! empty ( $this->options['cdn_link'] ) ) {
239 View Code Duplication
			if ( preg_match_all( '/<img\s[^>]*src=([\"\']??)([^\" >]*?)\1[^>]*>/iU', $content, $matches ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
240
				foreach ( $matches[2] as $image_src ) {
241
					$content = str_replace( $image_src, apply_filters( 'wp_get_attachment_url', $image_src, null ), $content );
242
				}
243
			}
244
245
			if ( preg_match_all( '/<img\s[^>]*srcset=([\"\']??)([^\">]*?)\1[^>]*\/?>/iU', $content, $matches ) ) {
246
247
				foreach ( $matches[2] as $image_srcset ) {
248
					$new_image_srcset = preg_replace_callback( '/(\S+)(\s\d+\w)/', function ( $srcset_matches ) {
249
						return apply_filters( 'wp_get_attachment_url', $srcset_matches[1], null ) . $srcset_matches[2];
250
					}, $image_srcset );
251
252
					$content = str_replace( $image_srcset, $new_image_srcset, $content );
253
				}
254
			}
255
256 View Code Duplication
			if ( preg_match_all( '/<a\s[^>]*href=([\"\']??)([^\" >]*?)\1[^>]*>(.*)<\/a>/iU', $content, $matches ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
257
				foreach ( $matches[0] as $link ) {
258
					$content = str_replace( $link[2], apply_filters( 'wp_get_attachment_url', $link[2], null ), $content );
259
				}
260
			}
261
		}
262
		return $content;
263
	}
264
265
	/**
266
	 * Add tag to dns prefetch cdn host
267
	 */
268
	public function prefetch_cdn() {
269
		if ( ! empty ( $this->options['cdn_link'] ) ) {
270
			$host = parse_url( $this->options['cdn_link'], PHP_URL_HOST );
271
272
			printf(
273
				'<link rel="dns-prefetch" href="%s"/>',
274
				esc_attr( '//' . $host )
275
			);
276
		}
277
	}
278
279
	/**
280
	 * Start output buffer if auto retina is enabled
281
	 */
282
	public function buffer_start_for_retina() {
283
		if ( ! empty ( $this->options['add_dpi2_srcset'] ) ) {
284
			$this->buffer_started = ob_start( [ $this, 'add_retina' ] );
285
		}
286
	}
287
288
	/**
289
	 * Stop output buffer if it was enabled by the plugin
290
	 */
291
	public function buffer_end_for_retina() {
292
		if ( $this->buffer_started && ob_get_level() ) {
293
			ob_end_flush();
294
		}
295
	}
296
297
	/**
298
	 * Returns a array of global parameters to be applied in all images,
299
	 * according to plugin's settings.
300
	 *
301
	 * @return array Global parameters to be appened at the end of each img URL.
302
	 */
303
	protected function get_global_params() {
304
		$params = [];
305
306
		// For now, only "auto" is supported.
307
		$auto = [];
308
		if ( ! empty ( $this->options['auto_format'] ) ) {
309
			array_push( $auto, 'format' );
310
		}
311
312
		if ( ! empty ( $this->options['auto_enhance'] ) ) {
313
			array_push( $auto, 'enhance' );
314
		}
315
316
		if ( ! empty ( $this->options['auto_compress'] ) ) {
317
			array_push( $auto, 'compress' );
318
		}
319
320
		if ( ! empty( $auto ) ) {
321
			$params['auto'] = implode( '%2C', $auto );
322
		}
323
324
		return $params;
325
	}
326
327
	/**
328
	 * Get all defined image sizes
329
	 *
330
	 * @return array
331
	 */
332
	protected function get_all_defined_sizes() {
333
		// Make thumbnails and other intermediate sizes.
334
		$theme_image_sizes = wp_get_additional_image_sizes();
335
336
		$sizes = [];
337
		foreach ( get_intermediate_image_sizes() as $s ) {
338
			$sizes[ $s ] = [ 'width' => '', 'height' => '', 'crop' => false ];
339
			if ( isset( $theme_image_sizes[ $s ] ) ) {
340
				// For theme-added sizes
341
				$sizes[ $s ]['width']  = intval( $theme_image_sizes[ $s ]['width'] );
342
				$sizes[ $s ]['height'] = intval( $theme_image_sizes[ $s ]['height'] );
343
				$sizes[ $s ]['crop']   = $theme_image_sizes[ $s ]['crop'];
344
			} else {
345
				// For default sizes set in options
346
				$sizes[ $s ]['width']  = get_option( "{$s}_size_w" );
347
				$sizes[ $s ]['height'] = get_option( "{$s}_size_h" );
348
				$sizes[ $s ]['crop']   = get_option( "{$s}_crop" );
349
			}
350
		}
351
352
		return $sizes;
353
	}
354
}
355
356
Images_Via_Imgix::instance();
357