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: f82f691c20aab849d5dea815a24f59855e78d1fd (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
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
<?php
/**
 * Piwik - Open source web analytics
 *
 * @link http://piwik.org
 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 *
 * @category Piwik
 * @package Piwik
 */
use Piwik\Config;
use Piwik\Piwik;
use Piwik\Common;

/**
 * @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 server-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 MERGED_CSS_FILE = "asset_manager_global_css.css";
    const MERGED_JS_FILE = "asset_manager_global_js.js";
    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 GET_CSS_MODULE_ACTION = "index.php?module=Proxy&action=getCss";
    const GET_JS_MODULE_ACTION = "index.php?module=Proxy&action=getJs";
    const MINIFIED_JS_RATIO = 100;

    /**
     * Returns CSS file inclusion directive(s) using the markup <link>
     *
     * @return string
     */
    public static function getCssAssets()
    {
        return sprintf(self::CSS_IMPORT_DIRECTIVE, self::GET_CSS_MODULE_ACTION);
    }

    /**
     * Returns JS file inclusion directive(s) using the markup <script>
     *
     * @return string
     */
    public static function getJsAssets()
    {
        if (self::isMergedAssetsDisabled()) {
            // Individual includes mode
            self::removeMergedAsset(self::MERGED_JS_FILE);
            return self::getIndividualJsIncludes();
        }
        return sprintf(self::JS_IMPORT_DIRECTIVE, self::GET_JS_MODULE_ACTION);
    }

    /**
     * Assets are cached in the browser and Piwik server returns 304 after initial download.
     * when the Cache buster string changes, the assets will be re-generated
     *
     * @return string
     */
    public static function generateAssetsCacheBuster()
    {
        $pluginList = md5(implode(",", PluginsManager::getInstance()->getLoadedPluginsName()));
        $cacheBuster = md5(Common::getSalt() . $pluginList . PHP_VERSION . Piwik_Version::VERSION);
        return $cacheBuster;
    }

    /**
     * Generate the merged css file.
     *
     * @throws Exception if a file can not be opened in write mode
     */
    private static function prepareMergedCssFile()
    {
        $mergedCssAlreadyGenerated = self::isGenerated(self::MERGED_CSS_FILE);
        $isDevelopingPiwik = self::isMergedAssetsDisabled();

        if ($mergedCssAlreadyGenerated
            && !$isDevelopingPiwik
        ) {
            return;
        }

        $less = self::makeLess();

        // Loop through each css file
        $files = self::getCssFiles();
        $mergedContent = "";
        foreach ($files as $file) {

            self::validateCssFile($file);

            $fileLocation = self::getAbsoluteLocation($file);
            $less->addImportDir(dirname($fileLocation));

            $content = file_get_contents($fileLocation);

            $content = self::rewriteCssPathsDirectives($file, $content);

            $mergedContent = $mergedContent . $content;
        }

        $fileHash = md5($mergedContent);
        $firstLineCompileHash = "/* compile_me_once=$fileHash */";

        // Disable Merged Assets ==> Check on each request if file needs re-compiling
        if ($mergedCssAlreadyGenerated
            && $isDevelopingPiwik
        ) {
            $pathMerged = self::getAbsoluteMergedFileLocation(self::MERGED_CSS_FILE);
            $f = fopen($pathMerged, 'r');
            $firstLine = fgets($f);
            fclose($f);
            if (!empty($firstLine)
                && trim($firstLine) == trim($firstLineCompileHash)) {
                return;
            }
            // Some CSS file in the merge, has changed since last merged asset was generated
            // Note: we do not detect changes in @import'ed LESS files
        }

        $mergedContent = $less->compile($mergedContent);

        Piwik_PostEvent('AssetManager.filterMergedCss', array(&$mergedContent));

        $mergedContent =
              $firstLineCompileHash . "\n"
            . "/* Piwik CSS file is compiled with Less. You may be interested in writing a custom Theme for Piwik! */\n"
            . $mergedContent;

        self::writeAssetToFile($mergedContent, self::MERGED_CSS_FILE);
    }

    protected static function makeLess()
    {
        if (!class_exists("lessc")) {
            throw new Exception("Less was added to composer during 2.0. ==> Execute this command to update composer packages: \$ php composer.phar update");
        }
        $less = new lessc;
        return $less;
    }

    /*
     * Rewrite css url directives
     * - assumes these are all relative paths
     *  - rewrite windows directory separator \\ to /
     */
    protected static function rewriteCssPathsDirectives($relativePath, $content)
    {
        static $rootDirectoryLength = null;
        if (is_null($rootDirectoryLength)) {
            $rootDirectoryLength = self::countDirectoriesInPathToRoot();
        }

        $baseDirectory = dirname($relativePath);
        $content = preg_replace_callback(
            "/(url\(['\"]?)([^'\")]*)/",
            create_function(
                '$matches',
                "return \$matches[1] . str_replace('\\\\', '/', substr(realpath(PIWIK_DOCUMENT_ROOT . '/$baseDirectory/' . \$matches[2]), $rootDirectoryLength));"
            ),
            $content
        );
        return $content;
    }

    protected static function countDirectoriesInPathToRoot()
    {
        $rootDirectory = realpath(PIWIK_DOCUMENT_ROOT);
        if ($rootDirectory != '/' && substr_compare($rootDirectory, '/', -1)) {
            $rootDirectory .= '/';
        }
        $rootDirectoryLen = strlen($rootDirectory);
        return $rootDirectoryLen;
    }

    private static function writeAssetToFile($mergedContent, $name)
    {
        // Remove the previous file
        self::removeMergedAsset($name);

        // Tries to open the new file
        $newFilePath = self::getAbsoluteMergedFileLocation($name);
        $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);
    }

    /**
     * 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()
    {
        $cssFiles = array();
        Piwik_PostEvent(self::CSS_IMPORT_EVENT, array(&$cssFiles));
        $cssFiles = self::sortCssFiles($cssFiles);
        return $cssFiles;
    }

    /**
     * Ensure CSS stylesheets are loaded in a particular order regardless of the order that plugins are loaded.
     *
     * @param array $cssFiles Array of CSS stylesheet files
     * @return array
     */
    private static function sortCssFiles($cssFiles)
    {
        $priorityCssOrdered = array(
            'libs/',
            'plugins/Zeitgeist/stylesheets/base.less',
            'plugins/Zeitgeist/stylesheets/',
            'plugins/',
        );

        return self::prioritySort($priorityCssOrdered, $cssFiles);
    }

    /**
     * Check the validity of the css file
     *
     * @param string $cssFile CSS file name
     * @return boolean
     * @throws Exception if a file can not be opened in write mode
     */
    private static function validateCssFile($cssFile)
    {
        if (!self::assetIsReadable($cssFile)) {
            throw new Exception("The css asset with 'href' = " . $cssFile . " is not readable");
        }
    }

    /**
     * Generate the merged js file.
     *
     * @throws Exception if a file can not be opened in write mode
     */
    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;
        }
        $mergedContent = str_replace("\n", "\r\n", $mergedContent);

        Piwik_PostEvent('AssetManager.filterMergedJs', array(&$mergedContent));

        self::writeAssetToFile($mergedContent, self::MERGED_JS_FILE);
    }

    /**
     * Returns individual JS file inclusion directive(s) using the markup <script>
     *
     * @return string
     */
    private static function getIndividualJsIncludes()
    {
        $jsFiles = self::getJsFiles();
        $jsIncludeString = '';
        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()
    {
        $jsFiles = array();
        Piwik_PostEvent(self::JS_IMPORT_EVENT, array(&$jsFiles));
        $jsFiles = self::sortJsFiles($jsFiles);
        return $jsFiles;
    }

    /**
     * Ensure core JS (jQuery etc.) are loaded in a particular order regardless of the order that plugins are loaded.
     *
     * @param array $jsFiles Arry of JavaScript files
     * @return array
     */
    private static function sortJsFiles($jsFiles)
    {
        $priorityJsOrdered = array(
            'libs/jquery/jquery.js',
            'libs/jquery/jquery-ui.js',
            'libs/jquery/jquery.browser.js',
            'libs/',
            'plugins/Zeitgeist/javascripts/piwikHelper.js',
            'plugins/Zeitgeist/javascripts/',
            'plugins/CoreHome/javascripts/broadcast.js',
            'plugins/',
        );

        return self::prioritySort($priorityJsOrdered, $jsFiles);
    }

    /**
     * Check the validity of the js file
     *
     * @param string $jsFile JavaScript file name
     * @return boolean
     * @throws Exception if js file is not valid
     */
    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 isMergedAssetsDisabled()
    {
        return Config::getInstance()->Debug['disable_merged_assets'];
    }

    /**
     * Returns the css merged file absolute location.
     * If there is none, the generation process will be triggered.
     *
     * @return string The absolute location of the css merged file
     */
    public static function getMergedCssFileLocation()
    {
        self::prepareMergedCssFile();
        return self::getAbsoluteMergedFileLocation(self::MERGED_CSS_FILE);
    }

    /**
     * Returns the js merged file absolute location.
     * If there is none, the generation process will be triggered.
     *
     * @return string The absolute location of the js merged file
     */
    public static function getMergedJsFileLocation()
    {
        $isGenerated = self::isGenerated(self::MERGED_JS_FILE);

        if (!$isGenerated) {
            self::generateMergedJsFile();
        }

        return self::getAbsoluteMergedFileLocation(self::MERGED_JS_FILE);
    }

    /**
     * Check if the provided merged file is generated
     *
     * @param string $filename filename of the merged asset
     * @return boolean true is file exists and is readable, false otherwise
     */
    private static function isGenerated($filename)
    {
        return is_readable(self::getAbsoluteMergedFileLocation($filename));
    }

    /**
     * Removes the previous merged file if it exists.
     * Also tries to remove compressed version of the merged file.
     *
     * @param string $filename filename of the merged asset
     * @see Piwik::serveStaticFile()
     * @throws Exception if the file couldn't be deleted
     */
    private static function removeMergedAsset($filename)
    {
        $isGenerated = self::isGenerated($filename);

        if ($isGenerated) {
            if (!unlink(self::getAbsoluteMergedFileLocation($filename))) {
                throw new Exception("Unable to delete merged file : " . $filename . ". Please delete the file and refresh");
            }

            // Tries to remove compressed version of the merged file.
            // See Piwik::serveStaticFile() for more info on static file compression
            $compressedFileLocation = PIWIK_USER_PATH . Piwik::COMPRESSED_FILE_LOCATION . $filename;

            @unlink($compressedFileLocation . ".deflate");
            @unlink($compressedFileLocation . ".gz");
        }
    }

    /**
     * Remove previous merged assets
     */
    public static function removeMergedAssets()
    {
        self::removeMergedAsset(self::MERGED_CSS_FILE);
        self::removeMergedAsset(self::MERGED_JS_FILE);
    }

    /**
     * Check if asset is readable
     *
     * @param string $relativePath Relative path to file
     * @return boolean
     */
    private static function assetIsReadable($relativePath)
    {
        return is_readable(self::getAbsoluteLocation($relativePath));
    }

    /**
     * Check if the merged file directory exists and is writable.
     *
     * @return string The directory location
     * @throws Exception if directory is not writable.
     */
    private static function getMergedFileDirectory()
    {
        $mergedFileDirectory = PIWIK_USER_PATH . '/' . self::MERGED_FILE_DIR;

        if (!is_dir($mergedFileDirectory)) {
            Common::mkdir($mergedFileDirectory);
        }

        if (!is_writable($mergedFileDirectory)) {
            throw new Exception("Directory " . $mergedFileDirectory . " has to be writable.");
        }

        return $mergedFileDirectory;
    }

    /**
     * Builds the absolute location of the requested merged file
     *
     * @param string $mergedFile Name of the merge file
     * @return string absolute location of the merged file
     */
    private static function getAbsoluteMergedFileLocation($mergedFile)
    {
        return self::getMergedFileDirectory() . $mergedFile;
    }

    /**
     * Returns the full path of an asset file
     *
     * @param string $relativePath Relative path to file
     * @return string
     */
    private static function getAbsoluteLocation($relativePath)
    {
        // served by web server directly, so must be a public path
        return PIWIK_DOCUMENT_ROOT . "/" . $relativePath;
    }

    /**
     * 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.
     *
     * @param string $content Contents of the JavaScript file
     * @return 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;
    }

    /**
     * Sort files according to priority order. Duplicates are also removed.
     *
     * @param array $priorityOrder Ordered array of paths (first to last) serving as buckets
     * @param array $files Unsorted array of files
     * @return array
     */
    public static function prioritySort($priorityOrder, $files)
    {
        $newFiles = array();
        foreach ($priorityOrder as $filePattern) {
            $newFiles = array_merge($newFiles, preg_grep('~^' . $filePattern . '~', $files));
        }
        return array_keys(array_flip($newFiles));
    }
}