Issues (3627)

CoreBundle/Templating/Helper/AssetsHelper.php (1 issue)

1
<?php
2
3
/*
4
 * @copyright   2014 Mautic Contributors. All rights reserved
5
 * @author      Mautic
6
 *
7
 * @link        http://mautic.org
8
 *
9
 * @license     GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
10
 */
11
12
namespace Mautic\CoreBundle\Templating\Helper;
13
14
use Mautic\CoreBundle\Helper\AssetGenerationHelper;
15
use Mautic\CoreBundle\Helper\InputHelper;
16
use Mautic\CoreBundle\Helper\PathsHelper;
17
use Symfony\Component\Asset\Packages;
18
19
class AssetsHelper
20
{
21
    /**
22
     * Used for Mautic app.
23
     */
24
    const CONTEXT_APP = 'app';
25
26
    /**
27
     * Used within the content iframe when building content with a theme.
28
     */
29
    const CONTEXT_BUILDER = 'builder';
30
31
    /**
32
     * @var AssetGenerationHelper
33
     */
34
    protected $assetHelper;
35
36
    /**
37
     * @var string
38
     */
39
    protected $context = self::CONTEXT_APP;
40
41
    /**
42
     * @var array
43
     */
44
    protected $assets = [
45
        self::CONTEXT_APP => [],
46
    ];
47
48
    /**
49
     * @var string|null
50
     */
51
    protected $version;
52
53
    /**
54
     * @var Packages
55
     */
56
    protected $packages;
57
58
    /**
59
     * @var string
60
     */
61
    protected $siteUrl;
62
63
    /**
64
     * @var PathsHelper
65
     */
66
    protected $pathsHelper;
67
68
    public function __construct(Packages $packages)
69
    {
70
        $this->packages = $packages;
71
    }
72
73
    /**
74
     * Gets asset prefix.
75
     *
76
     * @param bool $includeEndingSlash
77
     *
78
     * @return string
79
     */
80
    public function getAssetPrefix($includeEndingSlash = false)
81
    {
82
        $prefix = $this->pathsHelper->getSystemPath('asset_prefix');
83
        if (!empty($prefix)) {
84
            if ($includeEndingSlash && '/' != substr($prefix, -1)) {
85
                $prefix .= '/';
86
            } elseif (!$includeEndingSlash && '/' == substr($prefix, -1)) {
87
                $prefix = substr($prefix, 0, -1);
88
            }
89
        }
90
91
        return $prefix;
92
    }
93
94
    public function getImagesPath($absolute = false)
95
    {
96
        return $this->pathsHelper->getSystemPath('images', $absolute);
97
    }
98
99
    /**
100
     * Set asset url path.
101
     *
102
     * @param string      $path
103
     * @param string|null $packageName
104
     * @param string|null $version
105
     * @param bool|false  $absolute
106
     * @param bool|false  $ignorePrefix
107
     *
108
     * @return string
109
     */
110
    public function getUrl($path, $packageName = null, $version = null, $absolute = false, $ignorePrefix = false)
111
    {
112
        // if we have http in the url it is absolute and we can just return it
113
        if (0 === strpos($path, 'http')) {
114
            return $path;
115
        }
116
117
        // otherwise build the complete path
118
        if (!$ignorePrefix) {
119
            $assetPrefix = $this->getAssetPrefix(0 !== strpos($path, '/'));
120
            $path        = $assetPrefix.$path;
121
        }
122
123
        $url = $this->packages->getUrl($path, $packageName, $version);
0 ignored issues
show
The call to Symfony\Component\Asset\Packages::getUrl() has too many arguments starting with $version. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

123
        /** @scrutinizer ignore-call */ 
124
        $url = $this->packages->getUrl($path, $packageName, $version);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
124
125
        if ($absolute) {
126
            $url = $this->getBaseUrl().'/'.$path;
127
        }
128
129
        // Remove the dev index so the assets work in the dev mode
130
        if (strpos($url, '/index_dev.php/')) {
131
            $url = str_replace('index_dev.php/', '', $url);
132
        }
133
134
        return $url;
135
    }
136
137
    /**
138
     * Get base URL.
139
     *
140
     * @return string
141
     */
142
    public function getBaseUrl()
143
    {
144
        return $this->siteUrl;
145
    }
146
147
    /**
148
     * Define the context for which the assets will be injected and/or retrieved.
149
     *
150
     * If changing the context from app, it's important to reset the context back to app after
151
     * injecting/fetching assets for a different context.
152
     *
153
     * @param $context
154
     *
155
     * @return $this
156
     */
157
    public function setContext($context = self::CONTEXT_APP)
158
    {
159
        $this->context = $context;
160
        if (!isset($this->assets[$context])) {
161
            $this->assets[$context] = [];
162
        }
163
164
        return $this;
165
    }
166
167
    /**
168
     * Adds a JS script to the template.
169
     *
170
     * @param string $script
171
     * @param string $location
172
     * @param bool   $async
173
     * @param string $name
174
     *
175
     * @return $this
176
     */
177
    public function addScript($script, $location = 'head', $async = false, $name = null)
178
    {
179
        $assets     = &$this->assets[$this->context];
180
        $addScripts = function ($s) use ($location, &$assets, $async, $name) {
181
            $name = $name ?: 'script_'.hash('sha1', uniqid(mt_rand()));
182
183
            if ('head' == $location) {
184
                //special place for these so that declarations and scripts can be mingled
185
                $assets['headDeclarations'][$name] = ['script' => [$s, $async]];
186
            } else {
187
                if (!isset($assets['scripts'][$location])) {
188
                    $assets['scripts'][$location] = [];
189
                }
190
191
                if (!in_array($s, $assets['scripts'][$location])) {
192
                    $assets['scripts'][$location][$name] = [$s, $async];
193
                }
194
            }
195
        };
196
197
        if (is_array($script)) {
198
            foreach ($script as $s) {
199
                $addScripts($s);
200
            }
201
        } else {
202
            $addScripts($script);
203
        }
204
205
        return $this;
206
    }
207
208
    /**
209
     * Adds JS script declarations to the template.
210
     *
211
     * @param string $script
212
     * @param string $location
213
     *
214
     * @return $this
215
     */
216
    public function addScriptDeclaration($script, $location = 'head')
217
    {
218
        if ('head' == $location) {
219
            //special place for these so that declarations and scripts can be mingled
220
            $this->assets[$this->context]['headDeclarations'][] = ['declaration' => $script];
221
        } else {
222
            if (!isset($this->assets[$this->context]['scriptDeclarations'][$location])) {
223
                $this->assets[$this->context]['scriptDeclarations'][$location] = [];
224
            }
225
226
            if (!in_array($script, $this->assets[$this->context]['scriptDeclarations'][$location])) {
227
                $this->assets[$this->context]['scriptDeclarations'][$location][] = $script;
228
            }
229
        }
230
231
        return $this;
232
    }
233
234
    /**
235
     * Adds a stylesheet to be loaded in the template header.
236
     *
237
     * @param string $stylesheet
238
     *
239
     * @return $this
240
     */
241
    public function addStylesheet($stylesheet)
242
    {
243
        $addSheet = function ($s) {
244
            if (!isset($this->assets[$this->context]['stylesheets'])) {
245
                $this->assets[$this->context]['stylesheets'] = [];
246
            }
247
248
            if (!in_array($s, $this->assets[$this->context]['stylesheets'])) {
249
                $this->assets[$this->context]['stylesheets'][] = $s;
250
            }
251
        };
252
253
        if (is_array($stylesheet)) {
254
            foreach ($stylesheet as $s) {
255
                $addSheet($s);
256
            }
257
        } else {
258
            $addSheet($stylesheet);
259
        }
260
261
        return $this;
262
    }
263
264
    /**
265
     * Add style tag to the header.
266
     *
267
     * @param string $styles
268
     *
269
     * @return $this
270
     */
271
    public function addStyleDeclaration($styles)
272
    {
273
        if (!isset($this->assets[$this->context]['styleDeclarations'])) {
274
            $this->assets[$this->context]['styleDeclarations'] = [];
275
        }
276
277
        if (!in_array($styles, $this->assets[$this->context]['styleDeclarations'])) {
278
            $this->assets[$this->context]['styleDeclarations'][] = $styles;
279
        }
280
281
        return $this;
282
    }
283
284
    /**
285
     * Adds a custom declaration to <head />.
286
     *
287
     * @param string $declaration
288
     * @param string $location
289
     *
290
     * @return $this
291
     */
292
    public function addCustomDeclaration($declaration, $location = 'head')
293
    {
294
        if ('head' == $location) {
295
            $this->assets[$this->context]['headDeclarations'][] = ['custom' => $declaration];
296
        } else {
297
            if (!isset($this->assets[$this->context]['customDeclarations'][$location])) {
298
                $this->assets[$this->context]['customDeclarations'][$location] = [];
299
            }
300
301
            if (!in_array($declaration, $this->assets[$this->context]['customDeclarations'][$location])) {
302
                $this->assets[$this->context]['customDeclarations'][$location][] = $declaration;
303
            }
304
        }
305
306
        return $this;
307
    }
308
309
    /**
310
     * Outputs the stylesheets and style declarations.
311
     */
312
    public function outputStyles()
313
    {
314
        echo $this->getStyles();
315
    }
316
317
    /**
318
     * Outputs the stylesheets and style declarations.
319
     *
320
     * @return string
321
     */
322
    public function getStyles()
323
    {
324
        $styles = '';
325
        if (isset($this->assets[$this->context]['stylesheets'])) {
326
            foreach (array_reverse($this->assets[$this->context]['stylesheets']) as $s) {
327
                $styles .= '<link rel="stylesheet" href="'.$this->getUrl($s).'" data-source="mautic" />'."\n";
328
            }
329
        }
330
331
        if (isset($this->assets[$this->context]['styleDeclarations'])) {
332
            $styles .= "<style data-source=\"mautic\">\n";
333
            foreach (array_reverse($this->assets[$this->context]['styleDeclarations']) as $d) {
334
                $styles .= "$d\n";
335
            }
336
            $styles .= "</style>\n";
337
        }
338
339
        return $styles;
340
    }
341
342
    /**
343
     * Outputs the script files and declarations.
344
     *
345
     * @param string $location
346
     */
347
    public function outputScripts($location)
348
    {
349
        if (isset($this->assets[$this->context]['scripts'][$location])) {
350
            foreach (array_reverse($this->assets[$this->context]['scripts'][$location]) as $s) {
351
                list($script, $async) = $s;
352
                echo '<script src="'.$this->getUrl($script).'"'.($async ? ' async' : '').' data-source="mautic"></script>'."\n";
353
            }
354
        }
355
356
        if (isset($this->assets[$this->context]['scriptDeclarations'][$location])) {
357
            echo "<script data-source=\"mautic\">\n";
358
            foreach (array_reverse($this->assets[$this->context]['scriptDeclarations'][$location]) as $d) {
359
                echo "$d\n";
360
            }
361
            echo "</script>\n";
362
        }
363
364
        if (isset($this->assets[$this->context]['customDeclarations'][$location])) {
365
            foreach (array_reverse($this->assets[$this->context]['customDeclarations'][$location]) as $d) {
366
                echo "$d\n";
367
            }
368
        }
369
    }
370
371
    /**
372
     * Output head scripts, stylesheets, and custom declarations.
373
     */
374
    public function outputHeadDeclarations()
375
    {
376
        echo $this->getHeadDeclarations();
377
    }
378
379
    /**
380
     * Returns head scripts, stylesheets, and custom declarations.
381
     *
382
     * @return string
383
     */
384
    public function getHeadDeclarations()
385
    {
386
        $headOutput = $this->getStyles();
387
        if (!empty($this->assets[$this->context]['headDeclarations'])) {
388
            $scriptOpen = false;
389
390
            foreach ($this->assets[$this->context]['headDeclarations'] as $declaration) {
391
                $type   = key($declaration);
392
                $output = $declaration[$type];
393
394
                switch ($type) {
395
                    case 'script':
396
                        if ($scriptOpen) {
397
                            $headOutput .= "\n</script>";
398
                            $scriptOpen = false;
399
                        }
400
                        list($script, $async) = $output;
401
402
                        $headOutput .= "\n".'<script src="'.$this->getUrl($script).'"'.($async ? ' async' : '').' data-source="mautic"></script>';
403
                        break;
404
                    case 'custom':
405
                    case 'declaration':
406
                        if ('custom' == $type && $scriptOpen) {
407
                            $headOutput .= "\n</script>";
408
                            $scriptOpen = false;
409
                        } elseif ('declaration' == $type && !$scriptOpen) {
410
                            $headOutput .= "\n<script data-source=\"mautic\">";
411
                            $scriptOpen = true;
412
                        }
413
                        $headOutput .= "\n$output";
414
                        break;
415
                }
416
            }
417
            if ($scriptOpen) {
418
                $headOutput .= "\n</script>\n\n";
419
            }
420
        }
421
422
        return $headOutput;
423
    }
424
425
    /**
426
     * Output system stylesheets.
427
     */
428
    public function outputSystemStylesheets()
429
    {
430
        $assets = $this->assetHelper->getAssets();
431
432
        if (isset($assets['css'])) {
433
            foreach ($assets['css'] as $url) {
434
                echo '<link rel="stylesheet" href="'.$this->getUrl($url).'" data-source="mautic" />'."\n";
435
            }
436
        }
437
    }
438
439
    /**
440
     * Output system scripts.
441
     *
442
     * @param bool|false $includeEditor
443
     */
444
    public function outputSystemScripts($includeEditor = false)
445
    {
446
        $assets = $this->assetHelper->getAssets();
447
448
        if ($includeEditor) {
449
            $assets['js'] = array_merge($assets['js'], $this->getFroalaScripts());
450
        }
451
452
        if (isset($assets['js'])) {
453
            foreach ($assets['js'] as $url) {
454
                echo '<script src="'.$this->getUrl($url).'" data-source="mautic"></script>'."\n";
455
            }
456
        }
457
    }
458
459
    /**
460
     * Fetch system scripts.
461
     *
462
     * @param bool $render        If true, a string will be returned of rendered script for header
463
     * @param bool $includeEditor
464
     *
465
     * @return array|string
466
     */
467
    public function getSystemScripts($render = false, $includeEditor = false)
468
    {
469
        $assets = $this->assetHelper->getAssets();
470
471
        if ($includeEditor) {
472
            $assets['js'] = array_merge($assets['js'], $this->getFroalaScripts());
473
        }
474
475
        if ($render) {
476
            $js = '';
477
            if (isset($assets['js'])) {
478
                foreach ($assets['js'] as $url) {
479
                    $js .= '<script src="'.$this->getUrl($url).'" data-source="mautic"></script>'."\n";
480
                }
481
            }
482
483
            return $js;
484
        }
485
486
        return $assets['js'];
487
    }
488
489
    /**
490
     * Load Froala JS source files.
491
     *
492
     * @return array
493
     */
494
    public function getFroalaScripts()
495
    {
496
        $base    = 'app/bundles/CoreBundle/Assets/js/libraries/froala/';
497
        $plugins = $base.'plugins/';
498
499
        return [
500
            $base.'froala_editor.js?v'.$this->version,
501
            $plugins.'align.js?v'.$this->version,
502
            $plugins.'code_beautifier.js?v'.$this->version,
503
            $plugins.'code_view.js?v'.$this->version,
504
            $plugins.'colors.js?v'.$this->version,
505
            // $plugins . 'file.js?v' . $this->version,  // @todo
506
            $plugins.'font_family.js?v'.$this->version,
507
            $plugins.'font_size.js?v'.$this->version,
508
            $plugins.'fullscreen.js?v'.$this->version,
509
            $plugins.'image.js?v'.$this->version,
510
            // $plugins . 'image_manager.js?v' . $this->version,
511
            $plugins.'filemanager.js?v'.$this->version,
512
            $plugins.'inline_style.js?v'.$this->version,
513
            $plugins.'line_breaker.js?v'.$this->version,
514
            $plugins.'link.js?v'.$this->version,
515
            $plugins.'lists.js?v'.$this->version,
516
            $plugins.'paragraph_format.js?v'.$this->version,
517
            $plugins.'paragraph_style.js?v'.$this->version,
518
            $plugins.'quick_insert.js?v'.$this->version,
519
            $plugins.'quote.js?v'.$this->version,
520
            $plugins.'table.js?v'.$this->version,
521
            $plugins.'url.js?v'.$this->version,
522
            //$plugins . 'video.js?v' . $this->version,
523
            $plugins.'gatedvideo.js?v'.$this->version,
524
            $plugins.'token.js?v'.$this->version,
525
            $plugins.'dynamic_content.js?v'.$this->version,
526
        ];
527
    }
528
529
    /**
530
     * Loads an addon script.
531
     *
532
     * @param string $assetFilePath         The path to the file location. Can use full path or relative to mautic web root
533
     * @param string $onLoadCallback        Mautic namespaced function to call for the script onload
534
     * @param string $alreadyLoadedCallback Mautic namespaced function to call if the script has already been loaded
535
     *
536
     * @return string
537
     */
538
    public function includeScript($assetFilePath, $onLoadCallback = '', $alreadyLoadedCallback = '')
539
    {
540
        return  '<script async="async" type="text/javascript" data-source="mautic">Mautic.loadScript(\''.$this->getUrl($assetFilePath)."', '$onLoadCallback', '$alreadyLoadedCallback');</script>";
541
    }
542
543
    /**
544
     * Include stylesheet.
545
     *
546
     * @param string $assetFilePath the path to the file location. Can use full path or relative to mautic web root
547
     *
548
     * @return string
549
     */
550
    public function includeStylesheet($assetFilePath)
551
    {
552
        return  '<script async="async" type="text/javascript" data-source="mautic">Mautic.loadStylesheet(\''.$this->getUrl($assetFilePath).'\');</script>';
553
    }
554
555
    /**
556
     * Turn all URLs in clickable links.
557
     *
558
     * @param string $text
559
     * @param array  $protocols http/https, ftp, mail, twitter
560
     *
561
     * @return string
562
     */
563
    public function makeLinks($text, $protocols = ['http', 'mail'], array $attributes = [])
564
    {
565
        // clear tags in text
566
        $text = InputHelper::url($text, false, $protocols);
567
568
        // Link attributes
569
        $attr = '';
570
        foreach ($attributes as $key => $val) {
571
            $attr = ' '.$key.'="'.htmlentities($val).'"';
572
        }
573
574
        $links = [];
575
576
        // Extract existing links and tags
577
        $text = preg_replace_callback('~(<a .*?>.*?</a>|<.*?>)~i', function ($match) use (&$links) {
578
            return '<'.array_push($links, $match[1]).'>';
579
        }, $text);
580
581
        // Extract text links for each protocol
582
        foreach ((array) $protocols as $protocol) {
583
            switch ($protocol) {
584
                case 'http':
585
                case 'https':
586
                    $text = preg_replace_callback('~(?:(https?)://([^\s<]+)|(www\.[^\s<]+?\.[^\s<]+))(?<![\.,:])~i', function ($match) use ($protocol, &$links, $attr) {
587
                        if ($match[1]) {
588
                            $protocol = $match[1];
589
                        }
590
                        $link = $this->escape($match[2] ?: $match[3]);
591
592
                        return '<'.array_push($links, "<a $attr href=\"$protocol://$link\">$link</a>").'>';
593
                    }, $text);
594
                    break;
595
                case 'mail':
596
                    $text = preg_replace_callback('~([^\s<]+?@[^\s<]+?\.[^\s<]+)(?<![\.,:])~', function ($match) use (&$links, $attr) {
597
                        $match[1] = $this->escape($match[1]);
598
599
                        return '<'.array_push($links, "<a $attr href=\"mailto:{$match[1]}\">{$match[1]}</a>").'>';
600
                    }, $text);
601
                    break;
602
                case 'twitter':
603
                    $text = preg_replace_callback('~(?<!\w)[@#](\w++)~', function ($match) use (&$links, $attr) {
604
                        $match[0] = $this->escape($match[0]);
605
                        $match[1] = $this->escape($match[1]);
606
607
                        return '<'.array_push($links, "<a $attr href=\"https://twitter.com/".('@' == $match[0][0] ? '' : 'search/%23').$match[1]."\">{$match[0]}</a>").'>';
608
                    }, $text);
609
                    break;
610
                default:
611
                    $text = preg_replace_callback('~'.preg_quote($protocol, '~').'://([^\s<]+?)(?<![\.,:])~i', function ($match) use ($protocol, &$links, $attr) {
612
                        $match[1] = $this->escape($match[1]);
613
614
                        return '<'.array_push($links, "<a $attr href=\"$protocol://{$match[1]}\">{$match[1]}</a>").'>';
615
                    }, $text);
616
                    break;
617
            }
618
        }
619
620
        // Insert all link
621
        return preg_replace_callback('/<(\d+)>/', function ($match) use (&$links) {
622
            return $links[$match[1] - 1];
623
        }, $text);
624
    }
625
626
    /**
627
     * Returns only first $charCount chars of the $text and adds "..." if it is shortened.
628
     *
629
     * @param string $text
630
     * @param int    $charCount
631
     *
632
     * @return string
633
     */
634
    public function shortenText($text, $charCount = null)
635
    {
636
        if ($charCount && strlen($text) > $charCount) {
637
            return mb_substr($text, 0, $charCount, 'utf-8').'...';
638
        }
639
640
        return $text;
641
    }
642
643
    /**
644
     * @param           $country
645
     * @param bool|true $urlOnly
646
     * @param string    $class
647
     *
648
     * @return string
649
     */
650
    public function getCountryFlag($country, $urlOnly = true, $class = '')
651
    {
652
        $flagPath = $this->pathsHelper->getSystemPath('assets', true).'/images/flags/';
653
        $relpath  = $this->pathsHelper->getSystemPath('assets').'/images/flags/';
654
        $country  = ucwords(str_replace(' ', '-', $country));
655
        $flagImg  = '';
656
        if (file_exists($flagPath.$country.'.png')) {
657
            if (file_exists($flagPath.$country.'.png')) {
658
                $flagImg = $this->getUrl($relpath.$country.'.png');
659
            }
660
        }
661
662
        if ($urlOnly) {
663
            return $flagImg;
664
        } else {
665
            return '<img src="'.$flagImg.'" class="'.$class.'" />';
666
        }
667
    }
668
669
    /**
670
     * Clear all the assets.
671
     */
672
    public function clear()
673
    {
674
        $this->assets = [];
675
    }
676
677
    /**
678
     * @return string
679
     */
680
    public function getName()
681
    {
682
        return 'assets';
683
    }
684
685
    /**
686
     * Not used.
687
     */
688
    public function setCharset()
689
    {
690
    }
691
692
    public function setAssetHelper(AssetGenerationHelper $helper)
693
    {
694
        $this->assetHelper = $helper;
695
    }
696
697
    /**
698
     * @param $siteUrl
699
     */
700
    public function setSiteUrl($siteUrl)
701
    {
702
        if ('/' === substr($siteUrl, -1)) {
703
            $siteUrl = substr($siteUrl, 0, -1);
704
        }
705
706
        $this->siteUrl = $siteUrl;
707
    }
708
709
    public function setPathsHelper(PathsHelper $pathsHelper)
710
    {
711
        $this->pathsHelper = $pathsHelper;
712
    }
713
714
    /**
715
     * @param $secretKey
716
     * @param $version
717
     */
718
    public function setVersion($secretKey, $version)
719
    {
720
        $this->version = substr(hash('sha1', $secretKey.$version), 0, 8);
721
    }
722
723
    /**
724
     * @param $string
725
     *
726
     * @return string
727
     */
728
    private function escape($string)
729
    {
730
        return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', false);
731
    }
732
}
733