Completed
Push — master ( 64afc7...75e1d1 )
by LA
03:54
created

Images_Via_Imgix::instance()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 7
rs 9.4285
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
		add_filter( 'wp_get_attachment_url', [ $this, 'replace_image_url' ] );
33
		add_filter( 'imgix/add-image-url', [ $this, 'replace_image_url' ] );
34
35
		add_filter( 'image_downsize', [ $this, 'image_downsize' ], 10, 3 );
36
37
		add_filter( 'wp_calculate_image_srcset', [ $this, 'calculate_image_srcset' ], 10, 3 );
38
39
		add_filter( 'the_content', [ $this, 'replace_images_in_content' ] );
40
		add_action( 'wp_head', [ $this, 'prefetch_cdn' ], 1 );
41
42
		add_action( 'after_setup_theme', [ $this, 'buffer_start_for_retina' ] );
43
		add_action( 'shutdown', [ $this, 'buffer_end_for_retina' ] );
44
	}
45
46
	/**
47
	 * Plugin loader instance.
48
	 *
49
	 * @return Images_Via_Imgix
50
	 */
51
	public static function instance() {
52
		if ( ! isset( self::$instance ) ) {
53
			self::$instance = new self;
54
		}
55
56
		return self::$instance;
57
	}
58
59
	/**
60
	 * Override options from settings.
61
	 * Used in unit tests.
62
	 *
63
	 * @param array $options
64
	 */
65
	public function set_options( $options ) {
66
		$this->options = $options;
67
	}
68
69
	/**
70
	 * Find all img tags with sources matching "imgix.net" without the parameter
71
	 * "srcset" and add the "srcset" parameter to all those images, appending a new
72
	 * source using the "dpr=2" modifier.
73
	 *
74
	 * @param $content
75
	 *
76
	 * @return string Content with retina-enriched image tags.
77
	 */
78
	public function add_retina( $content ) {
79
		$pattern = '/<img((?![^>]+srcset )([^>]*)';
80
		$pattern .= 'src=[\'"]([^\'"]*imgix.net[^\'"]*\?[^\'"]*w=[^\'"]*)[\'"]([^>]*)*?)>/i';
81
		$repl    = '<img$2src="$3" srcset="${3}, ${3}&amp;dpr=2 2x, ${3}&amp;dpr=3 3x,"$4>';
82
		$content = preg_replace( $pattern, $repl, $content );
83
84
		return preg_replace( $pattern, $repl, $content );
85
	}
86
87
	/**
88
	 * Modify image urls for attachments to use imgix host.
89
	 *
90
	 * @param string $url
91
	 *
92
	 * @return string
93
	 */
94
	public function replace_image_url( $url ) {
95
		if ( ! empty ( $this->options['cdn_link'] ) ) {
96
			$parsed_url = parse_url( $url );
97
98
			if ( isset( $parsed_url['host'], $parsed_url['path'] ) && $parsed_url['host'] === parse_url( home_url( '/' ), PHP_URL_HOST ) && preg_match( '/\.(jpg|jpeg|gif|png)$/', $parsed_url['path'] ) ) {
99
				$cdn = parse_url( $this->options['cdn_link'] );
100
				foreach ( [ 'scheme', 'host', 'port' ] as $url_part ) {
101
					if ( isset( $cdn[ $url_part ] ) ) {
102
						$parsed_url[ $url_part ] = $cdn[ $url_part ];
103
					} else {
104
						unset( $parsed_url[ $url_part ] );
105
					}
106
				}
107
				$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 96 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...
108
109
				$url = add_query_arg( $this->get_global_params(), $url );
110
			}
111
		}
112
113
		return $url;
114
	}
115
116
	/**
117
	 * Set params when running image_downsize
118
	 *
119
	 * @param false|array  $return
120
	 * @param int          $attachment_id
121
	 * @param string|array $size
122
	 *
123
	 * @return false|array
124
	 */
125
	public function image_downsize( $return, $attachment_id, $size ) {
126
		if ( ! empty ( $this->options['cdn_link'] ) ) {
127
			$img_url = wp_get_attachment_url( $attachment_id );
128
129
			$params = [];
130
			if ( is_array( $size ) ) {
131
				$params['w'] = $width = isset( $size[0] ) ? $size[0] : 0;
132
				$params['h'] = $height = isset( $size[1] ) ? $size[1] : 0;
133
			} else {
134
				$available_sizes = $this->get_all_defined_sizes();
135
				if ( isset( $available_sizes[ $size ] ) ) {
136
					$size        = $available_sizes[ $size ];
137
					$params['w'] = $width = $size['width'];
138
					$params['h'] = $height = $size['height'];
139
				}
140
			}
141
142
			$params = array_filter( $params );
143
144
			$img_url = add_query_arg( $params, $img_url );
145
146
			if ( ! isset( $width ) || ! isset( $height ) ) {
147
				// any other type: use the real image
148
				$meta   = wp_get_attachment_metadata( $attachment_id );
149
				$width  = isset( $width ) ? $width : $meta['width'];
150
				$height = isset( $height ) ? $height : $meta['height'];
151
			}
152
153
			$return = [ $img_url, $width, $height, true ];
154
		}
155
156
		return $return;
157
	}
158
159
	/**
160
	 * Change url for images in srcset
161
	 *
162
	 * @param array  $image_meta
163
	 * @param array  $size_array
164
	 * @param string $image_src
165
	 *
166
	 * @return array
167
	 */
168
	public function calculate_image_srcset( $image_meta, $size_array, $image_src ) {
169
		foreach ( $image_meta as $i => $image_size ) {
170
			if ( $image_size['descriptor'] === 'w' ) {
171
				$image_meta[ $i ]['url'] = add_query_arg( 'w', $image_size['value'], $image_src );
172
			}
173
		}
174
175
		return $image_meta;
176
	}
177
178
	/**
179
	 * Modify image urls in content to use imgix host.
180
	 *
181
	 * @param $content
182
	 *
183
	 * @return string
184
	 */
185
	public function replace_images_in_content( $content ) {
186
		if ( ! empty ( $this->options['cdn_link'] ) ) {
187 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...
188
				foreach ( $matches[2] as $image_src ) {
189
					$content = str_replace( $image_src, $this->replace_image_url( $image_src ), $content );
190
				}
191
			}
192
193
			if ( preg_match_all( '/<img\s[^>]*srcset=([\"\']??)([^\">]*?)\1[^>]*\/?>/iU', $content, $matches ) ) {
194
195
				foreach ( $matches[2] as $image_srcset ) {
196
					$new_image_srcset = preg_replace_callback( '/(\S+)(\s\d+\w)/', function ( $srcset_matches ) {
197
						return $this->replace_image_url( $srcset_matches[1] ) . $srcset_matches[2];
198
					}, $image_srcset );
199
200
					$content = str_replace( $image_srcset, $new_image_srcset, $content );
201
				}
202
			}
203
204 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...
205
				foreach ( $matches[0] as $link ) {
206
					$content = str_replace( $link[2], $this->replace_image_url( $link[2] ), $content );
207
				}
208
			}
209
		}
210
211
		return $content;
212
	}
213
214
	/**
215
	 * Add tag to dns prefetch cdn host
216
	 */
217
	public function prefetch_cdn() {
218
		if ( ! empty ( $this->options['cdn_link'] ) ) {
219
			$host = parse_url( $this->options['cdn_link'], PHP_URL_HOST );
220
221
			printf(
222
				'<link rel="dns-prefetch" href="%s"/>',
223
				esc_attr( '//' . $host )
224
			);
225
		}
226
	}
227
228
	/**
229
	 * Start output buffer if auto retina is enabled
230
	 */
231
	public function buffer_start_for_retina() {
232
		if ( ! empty ( $this->options['add_dpi2_srcset'] ) ) {
233
			$this->buffer_started = true;
234
			ob_start( [ $this, 'add_retina' ] );
235
		}
236
	}
237
238
	/**
239
	 * Stop output buffer if it was enabled by the plugin
240
	 */
241
	public function buffer_end_for_retina() {
242
		if ( $this->buffer_started === true ) {
243
			ob_end_flush();
244
		}
245
	}
246
247
	/**
248
	 * Returns a array of global parameters to be applied in all images,
249
	 * according to plugin's settings.
250
	 *
251
	 * @return array Global parameters to be appened at the end of each img URL.
252
	 */
253
	protected function get_global_params() {
254
		$params = [];
255
256
		// For now, only "auto" is supported.
257
		$auto = [];
258
		if ( ! empty ( $this->options['auto_format'] ) ) {
259
			array_push( $auto, 'format' );
260
		}
261
262
		if ( ! empty ( $this->options['auto_enhance'] ) ) {
263
			array_push( $auto, 'enhance' );
264
		}
265
266
		if ( ! empty ( $this->options['auto_compress'] ) ) {
267
			array_push( $auto, 'compress' );
268
		}
269
270
		if ( ! empty( $auto ) ) {
271
			$params['auto'] = implode( '%2C', $auto );
272
		}
273
274
		return $params;
275
	}
276
277
	/**
278
	 * Get all defined image sizes
279
	 *
280
	 * @return array
281
	 */
282
	protected function get_all_defined_sizes() {
283
		// Make thumbnails and other intermediate sizes.
284
		$theme_image_sizes = wp_get_additional_image_sizes();
285
286
		$sizes = [];
287
		foreach ( get_intermediate_image_sizes() as $s ) {
288
			$sizes[ $s ] = [ 'width' => '', 'height' => '', 'crop' => false ];
289
			if ( isset( $theme_image_sizes[ $s ] ) ) {
290
				// For theme-added sizes
291
				$sizes[ $s ]['width']  = intval( $theme_image_sizes[ $s ]['width'] );
292
				$sizes[ $s ]['height'] = intval( $theme_image_sizes[ $s ]['height'] );
293
				$sizes[ $s ]['crop']   = $theme_image_sizes[ $s ]['crop'];
294
			} else {
295
				// For default sizes set in options
296
				$sizes[ $s ]['width']  = get_option( "{$s}_size_w" );
297
				$sizes[ $s ]['height'] = get_option( "{$s}_size_h" );
298
				$sizes[ $s ]['crop']   = get_option( "{$s}_crop" );
299
			}
300
		}
301
302
		return $sizes;
303
	}
304
}
305
306
Images_Via_Imgix::instance();
307