Welcome to mirror list, hosted at ThFree Co, Russian Federation.

AssetManager.php « core - github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: f71ed203f7de2931b68e7ae8ca2a40433019ad27 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
<?php
/**
 * Piwik - Open source web analytics
 *
 * @link http://piwik.org
 * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later
 * @version $Id: AssetManager.php
 *
 * @category Piwik
 * @package Piwik
 */

/**
 * @see libs/cssmin/cssmin.php
 */
require_once PIWIK_INCLUDE_PATH . '/libs/cssmin/cssmin.php';

/**
 * @see libs/jsmin/jsmin.php
 */
require_once PIWIK_INCLUDE_PATH . '/libs/jsmin/jsmin.php';

/**
 * Piwik_AssetManager is the class used to manage the inclusion of UI assets:
 * JavaScript and CSS files.
 * 
 * It performs the following actions:
 * 	- Identifies required assets
 *  - Includes assets in the rendered HTML page
 *  - Manages asset merging and minifying
 *  - Manages both server-side and client-side cache
 *
 * Whether assets are included individually or as merged files is defined by
 * the global option 'disable_merged_assets'. When set to 1, files will be
 * included individually.
 * When set to 0, files will be included within a pair of files: 1 JavaScript
 * and 1 css file.
 *
 * @package Piwik
 */
class Piwik_AssetManager
{		
	const CSS_IMPORT_EVENT = "AssetManager.getCssFiles";
	const JS_IMPORT_EVENT = "AssetManager.getJsFiles";	
	const MERGED_FILE_DIR = "tmp/assets/";
	const CSS_IMPORT_DIRECTIVE = "<link rel='stylesheet' type='text/css' href='%s' /> \n";
	const JS_IMPORT_DIRECTIVE = "<script type='text/javascript' src='%s'> </script> \n";
	const MINIFIED_JS_RATIO = 100;
	
	/**
	 * Returns CSS file inclusion directive(s) using the markup <link>
	 *
	 * @return string
	 */
	public static function getCssAssets()
	{
		if ( self::getDisableMergedAssets() ) {
			
			// Individual includes mode
			self::removeMergedAsset("css");
			return self::getIndividualCssIncludes();
						
		} else {
			
			return self::getMergedCssInclude();			
		}	
	}
	
	/**
	 * Returns JS file inclusion directive(s) using the markup <script>
	 *
	 * @return string
	 */
	public static function getJsAssets()
	{
		if ( self::getDisableMergedAssets() ) {
			
			// Individual includes mode
			self::removeMergedAsset("js");
			return self::getIndividualJsIncludes();
		
		} else {
			
			return self::getMergedJsInclude();
		}
	}
	
	/**
	 * Returns the merged CSS file inclusion directive(s) using the getAsset.php file.
	 *
	 * @return string
	 */
	private static function getMergedCssInclude()   
	{
		// Check existing merged asset
		$mergedCssFileHash = self::getMergedAssetHash("css");	
		
		// Generate asset when none exists
		if ( !$mergedCssFileHash )
		{
			$mergedCssFileHash = self::generateMergedCssFile();
		}

		return sprintf ( self::CSS_IMPORT_DIRECTIVE, self::MERGED_FILE_DIR . $mergedCssFileHash . ".css" );
	}

	/**
	 * Generate the merged css file.
	 *
	 * @throws Exception if a file can not be opened in write mode
	 * @return string Hashcode of the merged file.
	 */
	private static function generateMergedCssFile()
	{
		$mergedContent = "";
		
		// Loop through each css file
		$files = self::getCssFiles();
		foreach ($files as $file) {
			
			self::validateCssFile ( $file );
			
			$fileLocation = self::getAbsoluteLocation($file);
			$content = file_get_contents ($fileLocation);
			
			// Rewrite css url directives
			$baseDirectory = "../../" . dirname($file) . "/";
			$content = preg_replace ("/(url\(['\"]?)([^'\")]*)/", "$1" . $baseDirectory . "$2", $content);
			
			$mergedContent = $mergedContent . $content;
		}

		$mergedContent = cssmin::minify($mergedContent);
		
		// Compute HASH
		$hashcode = md5($mergedContent);
		
		// Remove the previous file
		self::removeMergedAsset("css");
		
		// Tries to open the new file
		$newFilePath = self::getLocationFromHash($hashcode, "css");
		$newFile = fopen($newFilePath, "w");	

		if (!$newFile) {
			throw new Exception ("The file : " . $newFile . " can not be opened in write mode.");
		}
	
		// Write the content in the new file
		fwrite($newFile, $mergedContent);
		fclose($newFile);

		return $hashcode;
	}
	
	/**
	 * Returns individual CSS file inclusion directive(s) using the markup <link>
	 *
	 * @return string
	 */
	private static function getIndividualCssIncludes()   
	{
		$cssIncludeString = '';
		
		$cssFiles = self::getCssFiles();
		
		foreach ($cssFiles as $cssFile) {
			
			self::validateCssFile ( $cssFile );	
			$cssIncludeString = $cssIncludeString . sprintf ( self::CSS_IMPORT_DIRECTIVE, $cssFile ); 
		}
		
		return $cssIncludeString;
	}
	
	/**
	 * Returns required CSS files
	 *
	 * @return Array
	 */
	private static function getCssFiles()   
	{
		Piwik_PostEvent(self::CSS_IMPORT_EVENT, $cssFiles);
		return array_unique ( $cssFiles ); 		
	}
	
	/**
	 * Check the validity of the css file
	 *
	 * @throws Exception if css file is not valid
	 * @return boolean
	 */
	private static function validateCssFile ( $cssFile )   
	{
		if(!self::assetIsReadable($cssFile))
		{
			throw new Exception("The css asset with 'href' = " . $cssFile . " is not readable");
		}
	}
	
	/**
	 * Returns the merged JS file inclusion directive(s) using the getAsset.php file.
	 *
	 * @return string
	 */
	private static function getMergedJsInclude()   
	{	
		// Check existing merged asset
		$mergedJsFileHash = self::getMergedAssetHash("js");
		
		// Generate asset when none exists
		if ( !$mergedJsFileHash )
		{
			$mergedJsFileHash = self::generateMergedJsFile();
		}
		
		return sprintf ( self::JS_IMPORT_DIRECTIVE, self::MERGED_FILE_DIR . $mergedJsFileHash . ".js" ); 
	}

	/**
	 * Generate the merged js file.
	 *
	 * @throws Exception if a file can not be opened in write mode
	 * @return string Hashcode of the merged file.
	 */
	private static function generateMergedJsFile()
	{
		$mergedContent = "";
		
		// Loop through each js file
		$files = self::getJsFiles();
		foreach ($files as $file) {
			
			self::validateJsFile ( $file );
			
			$fileLocation = self::getAbsoluteLocation($file);
			$content = file_get_contents ($fileLocation);
			
			if ( !self::isMinifiedJs($content) )
			{
				$content = JSMin::minify($content);
			}
			
			$mergedContent = $mergedContent . PHP_EOL . $content;
		}
		
		// Compute HASH
		$hashcode = md5($mergedContent);
		
		// Remove the previous file
		self::removeMergedAsset("js");
		
		// Tries to open the new file
		$newFilePath = self::getLocationFromHash($hashcode, "js"); 		
		$newFile = fopen($newFilePath, "w");	

		if (!$newFile) {
			throw new Exception ("The file : " . $newFile . " can not be opened in write mode.");
		}
		
		// Write the content in the new file
		fwrite($newFile, $mergedContent);
		fclose($newFile);
	
		return $hashcode;
	}
	
	/**
	 * Returns individual JS file inclusion directive(s) using the markup <script>
	 *
	 * @return string
	 */
	private static function getIndividualJsIncludes()   
	{
		$jsIncludeString = '';
		
		$jsFiles = self::getJsFiles();
		
		foreach ($jsFiles as $jsFile) {

			self::validateJsFile ( $jsFile );
			$jsIncludeString = $jsIncludeString . sprintf ( self::JS_IMPORT_DIRECTIVE, $jsFile );
		}
		
		return $jsIncludeString;	
	}
	
	/**
	 * Returns required JS files
	 *
	 * @return Array
	 */
	private static function getJsFiles()   
	{	
		Piwik_PostEvent(self::JS_IMPORT_EVENT, $jsFiles);
		return array_unique($jsFiles); 		
	}
	
	/**
	 * Check the validity of the js file
	 *
	 * @throws Exception if js file is not valid
	 * @return boolean
	 */
	private static function validateJsFile ( $jsFile )   
	{
		if(!self::assetIsReadable($jsFile))
		{
			throw new Exception("The js asset with 'src' = " . $jsFile . " is not readable");
		}
	}
	
	/**
	 * Returns the global option disable_merged_assets
	 *
	 * @return string
	 */
	private static function getDisableMergedAssets()
	{
		return Zend_Registry::get('config')->Debug->disable_merged_assets;
	}

	/**
	 * Gets the hashcode of the merged file according to its type
	 *
	 * @throws Exception if there is more than one file of the same type.
	 * @return string The hashcode of the merged file, false if not present.
	 */
	private static function getMergedAssetHash ($type)
	{	
		$mergedFileDirectory = self::getMergedFileDirectory();
		
		$matchingFiles = glob( $mergedFileDirectory . "*." . $type );
		
		switch ( count($matchingFiles) )
		{
			case 0:				
				return false;
				
			case 1:
				
				$mergedFile = $matchingFiles[0];
				$hashcode = basename($mergedFile, ".".$type);
				
				if ( empty($hashcode) ) {
					throw new Exception ("The merged asset : " . $mergedFile . " couldn't be parsed for getting the hashcode.");
				}
				
				return $hashcode;
				
			default:
				throw Exception ("There are more than 1 merged file of the same type in the merged file directory. This should never happen. Please delete all files in piwik/tmp/assets/ and refresh the page.");	
		}		
	}
	
	/**
	 * Check if the merged file directory exists and is writable.
	 *
	 * @throws Exception if directory is not writable.
	 * @return string The directory location
	 */
	private static function getMergedFileDirectory ()
	{
 		$mergedFileDirectory = self::getAbsoluteLocation(self::MERGED_FILE_DIR);
			
		if (!is_dir($mergedFileDirectory))
		{
			Piwik_Common::mkdir($mergedFileDirectory, 0755, false);
		}
		
		if (!is_writable($mergedFileDirectory))
		{
			throw new Exception("Directory " . $mergedFileDirectory . " has to be writable.");
		}

		return $mergedFileDirectory;
	}

	/**
	 * Remove the previous merged file if it exists
	 *
	 * @throws Exception if the file couldn't be deleted
	 */	
	private static function removeMergedAsset($type)
	{
		$mergedAssetHash = self::getMergedAssetHash($type);
		
		if ( $mergedAssetHash != false )
		{
			$previousFileLocation = self::getMergedFileDirectory() . $mergedAssetHash . "." . $type;
			
			if ( !unlink ( $previousFileLocation ) ) {
				throw Exception ("Unable to delete merged file : " . $previousFileLocation . ". Please delete the file and refresh");
			}
		}
	}

	/**
	 * Check if asset is readable
	 *
	 * @throws Boolean
	 */  
	private static function assetIsReadable ($relativePath)
	{
		return is_readable(self::getAbsoluteLocation($relativePath));
	}

	/**
	 * Returns the full path of an asset file
	 *
	 * @throws string
	 */  
	private static function getAbsoluteLocation ($relativePath)
	{
		return PIWIK_USER_PATH . "/" . $relativePath;
	}	
	
	/**
	 * Returns the full path of the merged file based on its hash.
	 *
	 * @throws string
	 */
	private static function getLocationFromHash ( $hash, $type )
	{
		return self::getMergedFileDirectory() . $hash . "." . $type; 
	}
	
	/**
	 * Remove previous merged assets
	 */
	public static function removeMergedAssets()
	{
		self::removeMergedAsset("css");
		self::removeMergedAsset("js");
	}
	
	/**
	 * Indicates if the provided javascript content has already been minified or not.
	 * The heuristic is based on a custom ratio : (size of file) / (number of lines).
	 * The threshold (100) has been found empirically on existing files : 
	 * - the ratio never exceeds 50 for non-minified content and
	 * - it never goes under 150 for minified content.
	 *
	 * @throws Boolean
	 */
	private static function isMinifiedJs ( $content )
	{
		$lineCount = substr_count($content, "\n");
		if ( $lineCount == 0 )
		{
			return true;
		}
		
		$contentSize = strlen($content);
		
		$ratio = $contentSize / $lineCount;
		
		return $ratio > self::MINIFIED_JS_RATIO;
	}
}