Completed
Push — master ( 671afb...c3ae58 )
by Nazar
09:49
created

Includes_processing   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 392
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 2
Bugs 1 Features 1
Metric Value
wmc 43
c 2
b 1
f 1
lcom 1
cbo 0
dl 0
loc 392
rs 8.3157

7 Methods

Rating   Name   Duplication   Size   Complexity  
B css() 0 77 5
C js() 0 68 12
A html() 0 5 1
B html_process_scripts() 0 54 7
C html_process_links_and_styles() 0 94 12
A has_relative_href() 0 10 4
A is_relative_path_and_exists() 0 3 2

How to fix   Complexity   

Complex Class

Complex classes like Includes_processing often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Includes_processing, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @package   CleverStyle CMS
4
 * @author    Nazar Mokrynskyi <[email protected]>
5
 * @copyright Copyright (c) 2014-2016, Nazar Mokrynskyi
6
 * @license   MIT License, see license.txt
7
 */
8
namespace cs\Page;
9
10
/**
11
 * Class includes few methods used for processing CSS and HTML files before putting into cache.
12
 *
13
 * This is because CSS and HTML files may contain other includes of other CSS, JS files, images, fonts and so on with absolute and relative paths.
14
 * Methods of this class handles all this includes and put them into single resulting file compressed with gzip.
15
 * This allows to decrease number of HTTP requests on page and avoid breaking of relative paths for fonts, images and other includes
16
 * after putting them into cache directory.
17
 */
18
class Includes_processing {
19
	/**
20
	 * Do not inline files bigger than 4 KiB
21
	 */
22
	const MAX_EMBEDDING_SIZE = 4096;
23
	protected static $extension_to_mime = [
24
		'jpeg'  => 'image/jpg',
25
		'jpe'   => 'image/jpg',
26
		'jpg'   => 'image/jpg',
27
		'gif'   => 'image/gif',
28
		'png'   => 'image/png',
29
		'svg'   => 'image/svg+xml',
30
		'svgz'  => 'image/svg+xml',
31
		'ttf'   => 'application/font-ttf',
32
		'ttc'   => 'application/font-ttf',
33
		'otf'   => 'application/font-otf',
34
		'woff'  => 'application/font-woff',
35
		'woff2' => 'application/font-woff2',
36
		'eot'   => 'application/vnd.ms-fontobject'
37
	];
38
	/**
39
	 * Analyses file for images, fonts and css links and include they content into single resulting css file.
40
	 *
41
	 * Supports next file extensions for possible includes:
42
	 * jpeg, jpe, jpg, gif, png, ttf, ttc, svg, svgz, woff, eot, css
43
	 *
44
	 * @param string $data Content of processed file
45
	 * @param string $file Path to file, that includes specified in previous parameter content
46
	 *
47
	 * @return string    $data
48
	 */
49
	static function css ($data, $file) {
50
		$dir = dirname($file);
51
		/**
52
		 * Remove comments, tabs and new lines
53
		 */
54
		$data = preg_replace('#(/\*.*?\*/)|\t|\n|\r#s', ' ', $data);
55
		/**
56
		 * Remove unnecessary spaces
57
		 */
58
		$data = preg_replace('/\s*([,;>{}\(])\s*/', '$1', $data);
59
		$data = preg_replace('/\s+/', ' ', $data);
60
		/**
61
		 * Return spaces required in media queries
62
		 */
63
		$data = preg_replace('/\s(and|or)\(/', ' $1 (', $data);
64
		/**
65
		 * Duplicated semicolons
66
		 */
67
		$data = preg_replace('/;+/m', ';', $data);
68
		/**
69
		 * Minify repeated colors declarations
70
		 */
71
		$data = preg_replace('/#([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3/i', '#$1$2$3', $data);
72
		/**
73
		 * Minify rgb colors declarations
74
		 */
75
		$data = preg_replace_callback(
76
			'/rgb\(([0-9,\.]+)\)/i',
77
			function ($rgb) {
78
				$rgb = explode(',', $rgb[1]);
79
				return
80
					'#'.
81
					str_pad(dechex($rgb[0]), 2, 0, STR_PAD_LEFT).
82
					str_pad(dechex($rgb[1]), 2, 0, STR_PAD_LEFT).
83
					str_pad(dechex($rgb[2]), 2, 0, STR_PAD_LEFT);
84
			},
85
			$data
86
		);
87
		/**
88
		 * Remove unnecessary zeros
89
		 */
90
		$data = preg_replace('/(\D)0\.(\d+)/i', '$1.$2', $data);
91
		/**
92
		 * Includes processing
93
		 */
94
		$data = preg_replace_callback(
95
			'/url\((.*?)\)|@import[\s\t\n\r]*[\'"](.*?)[\'"]/',
96
			function ($match) use ($dir) {
97
				$link = trim($match[1], '\'" ');
98
				$link = explode('?', $link, 2)[0];
99
				if (!static::is_relative_path_and_exists($link, $dir)) {
100
					return $match[0];
101
				}
102
				$content = file_get_contents("$dir/$link");
103
				if (filesize("$dir/$link") > static::MAX_EMBEDDING_SIZE) {
104
					$path_relatively_to_the_root = str_replace(getcwd(), '', realpath("$dir/$link"));
105
					$path_relatively_to_the_root .= '?'.substr(md5($content), 0, 5);
106
					return str_replace($match[1], $path_relatively_to_the_root, $match[0]);
107
				}
108
				$mime_type = 'text/html';
109
				$extension = file_extension($link);
110
				if (isset(static::$extension_to_mime[$extension])) {
111
					$mime_type = static::$extension_to_mime[$extension];
112
				} elseif ($extension == 'css') {
113
					$mime_type = 'text/css';
114
					/**
115
					 * For recursive includes processing, if CSS file includes others CSS files
116
					 */
117
					$content = static::css($content, $link);
118
				}
119
				$content = base64_encode($content);
120
				return str_replace($match[1], "data:$mime_type;charset=utf-8;base64,$content", $match[0]);
121
			},
122
			$data
123
		);
124
		return trim($data);
125
	}
126
	/**
127
	 * Simple and fast JS minification
128
	 *
129
	 * @param string $data
130
	 *
131
	 * @return string
132
	 */
133
	static function js ($data) {
134
		/**
135
		 * Split into array of lines
136
		 */
137
		$data = explode("\n", $data);
138
		/**
139
		 * Flag that is `true` when inside comment
140
		 */
141
		$comment = false;
142
		/**
143
		 * Set of symbols that are safe to be concatenated without new line with anything else
144
		 */
145
		$regexp = '[:;,.+\-*\/{}?><^\'"\[\]=&\(]';
146
		foreach ($data as $index => &$d) {
147
			$next_line = isset($data[$index + 1]) ? trim($data[$index + 1]) : '';
148
			/**
149
			 * Remove starting and trailing spaces
150
			 */
151
			$d = trim($d);
152
			/**
153
			 * Remove single-line comments
154
			 */
155
			if (mb_strpos($d, '//') === 0) {
156
				$d = '';
157
				continue;
158
			}
159
			/**
160
			 * Starts with multi-line comment
161
			 */
162
			if (mb_strpos($d, '/*') === 0) {
163
				$comment = true;
164
			}
165
			/**
166
			 * Add new line at the end if only needed
167
			 */
168
			if (
169
				$d &&
170
				$next_line &&
171
				!$comment &&
172
				!preg_match("/$regexp\$/", $d) &&
173
				!preg_match("/^$regexp/", $next_line)
174
			) {
175
				$d .= "\n";
176
			}
177
			if ($comment) {
178
				/**
179
				 * End of multi-line comment
180
				 */
181
				if (strpos($d, '*/') !== false) {
182
					$d       = explode('*/', $d)[1];
183
					$comment = false;
184
				} else {
185
					$d = '';
186
				}
187
			} else {
188
				/**
189
				 * Single-line comment
190
				 */
191
				$d = preg_replace('#^\s*//[^\'"]+$#', '', $d);
192
				/**
193
				 * If we are not sure - just add new like afterwards
194
				 */
195
				$d = preg_replace('#//.*$#', "\\0\n", $d);
196
			}
197
		}
198
		$data = implode('', $data);
199
		return trim($data, ';').';';
200
	}
201
	/**
202
	 * Analyses file for scripts and styles, combines them into resulting files in order to optimize loading process
203
	 * (files with combined scripts and styles will be created)
204
	 *
205
	 * @param string      $data          Content of processed file
206
	 * @param string      $file          Path to file, that includes specified in previous parameter content
207
	 * @param string      $base_filename Base filename for resulting combined files
208
	 * @param bool|string $destination   Directory where to put combined files or <i>false</i> to make includes built-in (vulcanization)
209
	 *
210
	 * @return string    $data
211
	 */
212
	static function html ($data, $file, $base_filename, $destination) {
213
		static::html_process_scripts($data, $file, $base_filename, $destination);
214
		static::html_process_links_and_styles($data, $file, $base_filename, $destination);
215
		return preg_replace("/\n+/", "\n", $data);
216
	}
217
	/**
218
	 * @param string      $data          Content of processed file
219
	 * @param string      $file          Path to file, that includes specified in previous parameter content
220
	 * @param string      $base_filename Base filename for resulting combined files
221
	 * @param bool|string $destination   Directory where to put combined files or <i>false</i> to make includes built-in (vulcanization)
222
	 *
223
	 * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
224
	 */
225
	protected static function html_process_scripts (&$data, $file, $base_filename, $destination) {
226
		if (!preg_match_all('/<script(.*)<\/script>/Uims', $data, $scripts)) {
227
			return;
228
		}
229
		$scripts_content    = '';
230
		$scripts_to_replace = [];
231
		$dir                = dirname($file);
232
		foreach ($scripts[1] as $index => $script) {
233
			$script = explode('>', $script);
234
			if (preg_match('/src\s*=\s*[\'"](.*)[\'"]/Uims', $script[0], $url)) {
235
				$url = $url[1];
236
				if (!static::is_relative_path_and_exists($url, $dir)) {
237
					continue;
238
				}
239
				$scripts_to_replace[] = $scripts[0][$index];
240
				$scripts_content .= file_get_contents("$dir/$url").";\n";
241
			} else {
242
				$scripts_content .= "$script[1];\n";
243
			}
244
		}
245
		$scripts_content = static::js($scripts_content);
246
		if (!$scripts_to_replace) {
247
			return;
248
		}
249
		/**
250
		 * If there is destination - put contents into the file, and put link to it, otherwise put minified content back
251
		 */
252
		if ($destination) {
253
			/**
254
			 * md5 to distinguish modifications of the files
255
			 */
256
			$content_md5 = substr(md5($scripts_content), 0, 5);
257
			file_put_contents(
258
				"$destination/$base_filename.js",
259
				gzencode($scripts_content, 9),
260
				LOCK_EX | FILE_BINARY
261
			);
262
			// Replace first script with combined file
263
			$data = str_replace(
264
				$scripts_to_replace[0],
265
				"<script src=\"$base_filename.js?$content_md5\"></script>",
266
				$data
267
			);
268
		} else {
269
			// Replace first script with combined content
270
			$data = str_replace(
271
				$scripts_to_replace[0],
272
				"<script>$scripts_content</script>",
273
				$data
274
			);
275
		}
276
		// Remove the rest of scripts
277
		$data = str_replace($scripts_to_replace, '', $data);
278
	}
279
	/**
280
	 * @param string      $data          Content of processed file
281
	 * @param string      $file          Path to file, that includes specified in previous parameter content
282
	 * @param string      $base_filename Base filename for resulting combined files
283
	 * @param bool|string $destination   Directory where to put combined files or <i>false</i> to make includes built-in (vulcanization)
284
	 *
285
	 * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
286
	 */
287
	protected static function html_process_links_and_styles (&$data, $file, $base_filename, $destination) {
288
		// Drop Polymer inclusion, since it is already present
289
		$data = str_replace('<link rel="import" href="../polymer/polymer.html">', '', $data);
290
		if (!preg_match_all('/<link(.*)>|<style(.*)<\/style>/Uims', $data, $links_and_styles)) {
291
			return;
292
		}
293
		$styles_content              = '';
294
		$imports_content             = '';
295
		$links_and_styles_to_replace = [];
296
		$dir                         = dirname($file);
297
		foreach ($links_and_styles[1] as $index => $link) {
298
			/**
299
			 * Check for custom styles `is="custom-style"` or styles includes `include=".."` - we'll skip them
300
			 * Or if content is plain CSS
301
			 */
302
			if (
303
				preg_match('/^[^>]*(is="custom-style"|include=)[^>]*>/Uim', $links_and_styles[2][$index]) ||
304
				mb_strpos($links_and_styles[0][$index], '</style>') > 0
305
			) {
306
				$content = explode('>', $links_and_styles[2][$index], 2)[1];
307
				$data    = str_replace(
308
					$content,
309
					static::css($content, $file),
310
					$data
311
				);
312
				continue;
313
			}
314
			if (!static::has_relative_href($link, $url, $dir)) {
315
				continue;
316
			}
317
			$import = preg_match('/rel\s*=\s*[\'"]import[\'"]/Uim', $link);
318
			/**
319
			 * CSS imports are available in Polymer alongside with HTML imports
320
			 */
321
			$css_import = $import && preg_match('/type\s*=\s*[\'"]css[\'"]/Uim', $link);
322
			$stylesheet = preg_match('/rel\s*=\s*[\'"]stylesheet[\'"]/Uim', $link);
323
			/**
324
			 * If content is link to CSS file
325
			 */
326
			if ($css_import || $stylesheet) {
327
				$links_and_styles_to_replace[] = $links_and_styles[0][$index];
328
				$styles_content .= static::css(
329
					file_get_contents("$dir/$url"),
330
					"$dir/$url"
331
				);
332
				/**
333
				 * If content is HTML import
334
				 */
335
			} elseif ($import) {
336
				$links_and_styles_to_replace[] = $links_and_styles[0][$index];
337
				$imports_content .= static::html(
338
					file_get_contents("$dir/$url"),
339
					"$dir/$url",
340
					"$base_filename-".basename($url, '.html'),
341
					$destination
342
				);
343
			}
344
		}
345
		if (!$links_and_styles_to_replace) {
346
			return;
347
		}
348
		/**
349
		 * If there is destination - put contents into the file, and put link to it, otherwise put minified content back
350
		 */
351
		if ($destination) {
352
			/**
353
			 * md5 to distinguish modifications of the files
354
			 */
355
			$content_md5 = substr(md5($styles_content), 0, 5);
356
			file_put_contents(
357
				"$destination/$base_filename.css",
358
				gzencode($styles_content, 9),
359
				LOCK_EX | FILE_BINARY
360
			);
361
			// Replace first link or style with combined file
362
			$data = str_replace(
363
				$links_and_styles_to_replace[0],
364
				"<link rel=\"import\" type=\"css\" href=\"$base_filename.css?$content_md5\">",
365
				$data
366
			);
367
		} else {
368
			// Replace first `<template>` with combined content
369
			$data = preg_replace(
370
				'/<template>/',
371
				"$0<style>$styles_content</style>",
372
				$data,
373
				1
374
			);
375
		}
376
		// Remove the rest of links and styles
377
		$data = str_replace($links_and_styles_to_replace, '', $data);
378
		// Add imports to the end of file
379
		$data .= $imports_content;
380
	}
381
	/**
382
	 * @param string $link
383
	 * @param string $url
384
	 * @param string $dir
385
	 *
386
	 * @return bool
387
	 */
388
	protected static function has_relative_href ($link, &$url, $dir) {
389
		$result =
390
			$link &&
391
			preg_match('/href\s*=\s*[\'"](.*)[\'"]/Uims', $link, $url);
392
		if ($result && static::is_relative_path_and_exists($url[1], $dir)) {
393
			$url = $url[1];
394
			return true;
395
		}
396
		return false;
397
	}
398
	/**
399
	 * Simple check for http[s], ftp and absolute links
400
	 *
401
	 * @param string $path
402
	 * @param string $dir
403
	 *
404
	 * @return bool
405
	 */
406
	protected static function is_relative_path_and_exists ($path, $dir) {
407
		return !preg_match('#^(http://|https://|ftp://|/)#i', $path) && file_exists("$dir/$path");
408
	}
409
}
410