Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 203
0.00% covered (danger)
0.00%
0 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
Piwik
0.00% covered (danger)
0.00%
0 / 203
0.00% covered (danger)
0.00%
0 / 20
3782
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 __invoke
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
56
 trackSearch
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 trackCombinedSearch
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 trackRecordPage
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 trackPageView
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getSearchResults
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
56
 getCombinedSearchResults
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 getRecordDriver
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 getSearchCustomVars
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
20
 getRecordPageCustomVars
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 getLightboxCustomVars
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGenericCustomVars
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getOpeningTrackingCode
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 getCustomUrl
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 getClosingTrackingCode
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getCustomVarsCode
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 getTrackSearchCode
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 getTrackCombinedSearchCode
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 getTrackPageViewCode
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * Piwik view helper
5 *
6 * PHP version 8
7 *
8 * Copyright (C) The National Library of Finland 2014-2018.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 *
23 * @category VuFind
24 * @package  View_Helpers
25 * @author   Ere Maijala <ere.maijala@helsinki.fi>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org Main Site
28 */
29
30namespace VuFind\View\Helper\Root;
31
32use function is_array;
33use function strlen;
34
35/**
36 * Piwik Web Analytics view helper
37 *
38 * @category VuFind
39 * @package  View_Helpers
40 * @author   Ere Maijala <ere.maijala@helsinki.fi>
41 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
42 * @link     https://vufind.org Main Site
43 */
44class Piwik extends \Laminas\View\Helper\AbstractHelper
45{
46    /**
47     * Piwik URL (false if disabled)
48     *
49     * @var string|bool
50     */
51    protected $url;
52
53    /**
54     * Piwik Site ID
55     *
56     * @var int
57     */
58    protected $siteId;
59
60    /**
61     * Search prefix (see config.ini for details)
62     *
63     * @var string
64     */
65    protected $searchPrefix;
66
67    /**
68     * Whether to disable cookies (see config.ini for details)
69     *
70     * @var bool
71     */
72    protected $disableCookies;
73
74    /**
75     * Whether to track use custom variables to track additional information
76     *
77     * @var bool
78     */
79    protected $customVars;
80
81    /**
82     * Request object
83     *
84     * @var \Laminas\Http\PhpEnvironment\Request
85     */
86    protected $request;
87
88    /**
89     * Router object
90     *
91     * @var \Laminas\Router\Http\RouteMatch
92     */
93    protected $router;
94
95    /**
96     * Whether the tracker was initialized from lightbox.
97     *
98     * @var bool
99     */
100    protected $lightbox;
101
102    /**
103     * Additional parameters
104     *
105     * @var array
106     */
107    protected $params;
108
109    /**
110     * A timestamp used to identify the init function to avoid name clashes when
111     * opening lightboxes.
112     *
113     * @var int
114     */
115    protected $timestamp;
116
117    /**
118     * Constructor
119     *
120     * @param string|bool                         $url        Piwik address
121     * (false if disabled)
122     * @param int|array                           $options    Options array (or,
123     * if a single value, the Piwik site ID -- for backward compatibility)
124     * @param bool                                $customVars Whether to track
125     * additional information in custom variables
126     * @param Laminas\Router\Http\RouteMatch      $router     Request
127     * @param Laminas\Http\PhpEnvironment\Request $request    Request
128     */
129    public function __construct($url, $options, $customVars, $router, $request)
130    {
131        $this->url = $url;
132        if ($url && !str_ends_with($url, '/')) {
133            $this->url .= '/';
134        }
135        if (is_array($options)) {
136            $this->siteId = $options['siteId'];
137            $this->searchPrefix = $options['searchPrefix'] ?? '';
138            $this->disableCookies = $options['disableCookies'] ?? '';
139        } else {
140            $this->siteId = $options;
141        }
142        $this->customVars = $customVars;
143        $this->router = $router;
144        $this->request = $request;
145        $this->timestamp = round(microtime(true) * 1000);
146    }
147
148    /**
149     * Returns Piwik code (if active) or empty string if not.
150     *
151     * @param array $params Parameters
152     *
153     * @return string
154     */
155    public function __invoke($params = null)
156    {
157        if (!$this->url) {
158            return '';
159        }
160
161        $this->params = $params;
162        if (isset($this->params['lightbox'])) {
163            $this->lightbox = $this->params['lightbox'];
164        }
165
166        $results = $this->getSearchResults();
167        if ($results && ($combinedResults = $this->getCombinedSearchResults())) {
168            $code = $this->trackCombinedSearch($results, $combinedResults);
169        } elseif ($results) {
170            $code = $this->trackSearch($results);
171        } elseif ($recordDriver = $this->getRecordDriver()) {
172            $code = $this->trackRecordPage($recordDriver);
173        } else {
174            $code = $this->trackPageView();
175        }
176
177        $inlineScript = $this->getView()->plugin('inlinescript');
178        return $inlineScript(\Laminas\View\Helper\HeadScript::SCRIPT, $code, 'SET');
179    }
180
181    /**
182     * Track a Search
183     *
184     * @param VuFind\Search\Base\Results $results Search Results
185     *
186     * @return string Tracking Code
187     */
188    protected function trackSearch($results)
189    {
190        $customVars = $this->lightbox
191            ? $this->getLightboxCustomVars()
192            : $this->getSearchCustomVars($results);
193
194        $code = $this->getOpeningTrackingCode();
195        $code .= $this->getCustomVarsCode($customVars);
196        $code .= $this->getTrackSearchCode($results);
197        $code .= $this->getClosingTrackingCode();
198
199        return $code;
200    }
201
202    /**
203     * Track a Combined Search
204     *
205     * @param VuFind\Search\Base\Results $results         Search Results
206     * @param array                      $combinedResults Combined Search Results
207     *
208     * @return string Tracking Code
209     */
210    protected function trackCombinedSearch($results, $combinedResults)
211    {
212        $customVars = $this->lightbox
213            ? $this->getLightboxCustomVars()
214            : $this->getSearchCustomVars($results);
215
216        $code = $this->getOpeningTrackingCode();
217        $code .= $this->getCustomVarsCode($customVars);
218        $code .= $this->getTrackCombinedSearchCode($results, $combinedResults);
219        $code .= $this->getClosingTrackingCode();
220
221        return $code;
222    }
223
224    /**
225     * Track a Record View
226     *
227     * @param VuFind\RecordDriver\AbstractBase $recordDriver Record Driver
228     *
229     * @return string Tracking Code
230     */
231    protected function trackRecordPage($recordDriver)
232    {
233        $customVars = $this->lightbox
234            ? $this->getLightboxCustomVars()
235            : $this->getRecordPageCustomVars($recordDriver);
236
237        $code = $this->getOpeningTrackingCode();
238        $code .= $this->getCustomVarsCode($customVars);
239        $code .= $this->getTrackPageViewCode();
240        $code .= $this->getClosingTrackingCode();
241
242        return $code;
243    }
244
245    /**
246     * Track a Generic Page View
247     *
248     * @return string Tracking Code
249     */
250    protected function trackPageView()
251    {
252        $customVars = $this->lightbox
253            ? $this->getLightboxCustomVars()
254            : $this->getGenericCustomVars();
255
256        $code = $this->getOpeningTrackingCode();
257        $code .= $this->getCustomVarsCode($customVars);
258        $code .= $this->getTrackPageViewCode();
259        $code .= $this->getClosingTrackingCode();
260
261        return $code;
262    }
263
264    /**
265     * Get Search Results if on a Results Page
266     *
267     * @return VuFind\Search\Base\Results|null Search results or null if not
268     * on a search page
269     */
270    protected function getSearchResults()
271    {
272        $viewModel = $this->getView()->plugin('view_model');
273        $current = $viewModel->getCurrent();
274        if (null === $current || 'layout/lightbox' === $current->getTemplate()) {
275            return null;
276        }
277        $children = $current->getChildren();
278        if (isset($children[0])) {
279            $template = $children[0]->getTemplate();
280            if (!strstr($template, '/home') && !strstr($template, 'facet-list')) {
281                $results = $children[0]->getVariable('results');
282                if ($results instanceof \VuFind\Search\Base\Results) {
283                    return $results;
284                }
285            }
286        }
287        return null;
288    }
289
290    /**
291     * Get Combined Search Results if on a Results Page
292     *
293     * @return array|null Array of search results or null if not on a combined search
294     * page
295     */
296    protected function getCombinedSearchResults()
297    {
298        $viewModel = $this->getView()->plugin('view_model');
299        $current = $viewModel->getCurrent();
300        if (null === $current) {
301            return null;
302        }
303        $children = $current->getChildren();
304        if (isset($children[0])) {
305            $results = $children[0]->getVariable('combinedResults');
306            if (is_array($results)) {
307                return $results;
308            }
309        }
310        return null;
311    }
312
313    /**
314     * Get Record Driver if on a Record Page
315     *
316     * @return VuFind\RecordDriver\AbstractBase|null Record driver or null if not
317     * on a record page
318     */
319    protected function getRecordDriver()
320    {
321        $view = $this->getView();
322        $viewModel = $view->plugin('view_model');
323        $current = $viewModel->getCurrent();
324        if (null === $current) {
325            $driver = $view->vars('driver');
326            if ($driver instanceof \VuFind\RecordDriver\AbstractBase) {
327                return $driver;
328            }
329            return null;
330        }
331        $children = $current->getChildren();
332        if (isset($children[0])) {
333            $driver = $children[0]->getVariable('driver');
334            if ($driver instanceof \VuFind\RecordDriver\AbstractBase) {
335                return $driver;
336            }
337        }
338        return null;
339    }
340
341    /**
342     * Get Custom Variables for Search Results
343     *
344     * @param VuFind\Search\Base\Results $results Search results
345     *
346     * @return array Associative array of custom variables
347     */
348    protected function getSearchCustomVars($results)
349    {
350        if (!$this->customVars) {
351            return [];
352        }
353
354        $facets = [];
355        $facetTypes = [];
356        $params = $results->getParams();
357        foreach ($params->getFilterList() as $filterType => $filters) {
358            $facetTypes[] = $filterType;
359            foreach ($filters as $filter) {
360                $facets[] = $filter['field'] . '|' . $filter['value'];
361            }
362        }
363        $facets = implode("\t", $facets);
364        $facetTypes = implode("\t", $facetTypes);
365
366        return [
367            'Facets' => $facets,
368            'FacetTypes' => $facetTypes,
369            'SearchType' => $params->getSearchType(),
370            'SearchBackend' => $params->getSearchClassId(),
371            'Sort' => $params->getSort(),
372            'Page' => $params->getPage(),
373            'Limit' => $params->getLimit(),
374            'View' => $params->getView(),
375        ];
376    }
377
378    /**
379     * Get Custom Variables for a Record Page
380     *
381     * @param VuFind\RecordDriver\AbstractBase $recordDriver Record driver
382     *
383     * @return array Associative array of custom variables
384     */
385    protected function getRecordPageCustomVars($recordDriver)
386    {
387        $id = $recordDriver->getUniqueID();
388        $formats = $recordDriver->tryMethod('getFormats');
389        if (is_array($formats)) {
390            $formats = implode(',', $formats);
391        }
392        $formats = $formats;
393        $author = $recordDriver->tryMethod('getPrimaryAuthor');
394        if (empty($author)) {
395            $author = '-';
396        }
397        // Use breadcrumb for title since it's guaranteed to return something
398        $title = $recordDriver->tryMethod('getBreadcrumb');
399        if (empty($title)) {
400            $title = '-';
401        }
402        $institutions = $recordDriver->tryMethod('getInstitutions');
403        if (is_array($institutions)) {
404            $institutions = implode(',', $institutions);
405        }
406        $institutions = $institutions;
407
408        return [
409            'RecordFormat' => $formats,
410            'RecordData' => "$id|$author|$title",
411            'RecordInstitution' => $institutions,
412        ];
413    }
414
415    /**
416     * Get Custom Variables for lightbox actions
417     *
418     * @return array Associative array of custom variables
419     */
420    protected function getLightboxCustomVars()
421    {
422        return [];
423    }
424
425    /**
426     * Get Custom Variables for a Generic Page View
427     *
428     * @return array Associative array of custom variables
429     */
430    protected function getGenericCustomVars()
431    {
432        return [];
433    }
434
435    /**
436     * Get the Initialization Part of the Tracking Code
437     *
438     * @return string JavaScript Code Fragment
439     */
440    protected function getOpeningTrackingCode()
441    {
442        $escape = $this->getView()->plugin('escapejs');
443        $code = <<<EOT
444
445            function initVuFindPiwikTracker{$this->timestamp}(){
446                var VuFindPiwikTracker = Piwik.getTracker();
447
448                VuFindPiwikTracker.setSiteId({$this->siteId});
449                VuFindPiwikTracker.setTrackerUrl('{$this->url}piwik.php');
450                VuFindPiwikTracker.setCustomUrl('{$escape($this->getCustomUrl())}');
451
452            EOT;
453        if ($this->disableCookies) {
454            $code .= <<<EOT
455                    VuFindPiwikTracker.disableCookies();
456
457                EOT;
458        }
459
460        return $code;
461    }
462
463    /**
464     * Get the custom URL of the Tracking Code
465     *
466     * @return string URL
467     */
468    protected function getCustomUrl()
469    {
470        $path = $this->request->getUri()->toString();
471        $routeMatch = $this->router->match($this->request);
472        if (
473            $routeMatch
474            && $routeMatch->getMatchedRouteName() == 'vufindrecord-ajaxtab'
475        ) {
476            // Replace 'AjaxTab' with tab name in record page URLs
477            $replace = 'AjaxTab';
478            $tab = $this->request->getPost('tab');
479            if (null !== ($pos = strrpos($path, $replace))) {
480                $path = substr_replace($path, $tab, $pos, $pos + strlen($replace));
481            }
482        }
483        return $path;
484    }
485
486    /**
487     * Get the Finalization Part of the Tracking Code
488     *
489     * @return string JavaScript Code Fragment
490     */
491    protected function getClosingTrackingCode()
492    {
493        return <<<EOT
494                VuFindPiwikTracker.enableLinkTracking();
495            };
496            (function(){
497                if (typeof Piwik === 'undefined') {
498                    var d=document, g=d.createElement('script'),
499                        s=d.getElementsByTagName('script')[0];
500                    g.type='text/javascript'; g.defer=true; g.async=true;
501                    g.src='{$this->url}piwik.js';
502                    g.onload=initVuFindPiwikTracker{$this->timestamp};
503                    s.parentNode.insertBefore(g,s);
504                } else {
505                    initVuFindPiwikTracker{$this->timestamp}();
506                }
507            })();
508            EOT;
509    }
510
511    /**
512     * Convert a Custom Variables Array to JavaScript Code
513     *
514     * @param array $customVars Custom Variables
515     *
516     * @return string JavaScript Code Fragment
517     */
518    protected function getCustomVarsCode($customVars)
519    {
520        $escape = $this->getView()->plugin('escapeHtmlAttr');
521        $code = '';
522        $i = 0;
523        foreach ($customVars as $key => $value) {
524            ++$i;
525
526            // Workaround to prevent overwriting of custom variables 4 and 5 by
527            // trackSiteSearch, see http://forum.piwik.org/read.php?2,115537,115538
528            if ($i === 4) {
529                $i = 6;
530            }
531
532            $value = $escape($value);
533            $code .= <<<EOT
534                    VuFindPiwikTracker.setCustomVariable($i, '$key', '$value', 'page');
535
536                EOT;
537        }
538        return $code;
539    }
540
541    /**
542     * Get Site Search Tracking Code
543     *
544     * @param VuFind\Search\Base\Results $results Search results
545     *
546     * @return string JavaScript Code Fragment
547     */
548    protected function getTrackSearchCode($results)
549    {
550        $escape = $this->getView()->plugin('escapeHtmlAttr');
551        $params = $results->getParams();
552        $searchTerms = $escape($params->getDisplayQuery());
553        $searchType = $escape($params->getSearchType());
554        $resultCount = $results->getResultTotal();
555        $backendId = $results->getOptions()->getSearchClassId();
556
557        // Use trackSiteSearch *instead* of trackPageView in searches
558        return <<<EOT
559                VuFindPiwikTracker.trackSiteSearch(
560                    '{$this->searchPrefix}$backendId|$searchTerms', '$searchType', $resultCount
561                );
562
563            EOT;
564    }
565
566    /**
567     * Get Site Search Tracking Code for Combined Search
568     *
569     * @param VuFind\Search\Base\Results $results         Search results
570     * @param array                      $combinedResults Combined Search Results
571     *
572     * @return string JavaScript Code Fragment
573     */
574    protected function getTrackCombinedSearchCode($results, $combinedResults)
575    {
576        $escape = $this->getView()->plugin('escapeHtmlAttr');
577        $params = $results->getParams();
578        $searchTerms = $escape($params->getDisplayQuery());
579        $searchType = $escape($params->getSearchType());
580        $resultCount = 0;
581        foreach ($combinedResults as $currentSearch) {
582            if (!empty($currentSearch['ajax'])) {
583                // Some results fetched via ajax, so report that we don't know the
584                // result count.
585                $resultCount = 'false';
586                break;
587            }
588            $resultCount += $currentSearch['view']->results
589                ->getResultTotal();
590        }
591
592        // Use trackSiteSearch *instead* of trackPageView in searches
593        return <<<EOT
594                VuFindPiwikTracker.trackSiteSearch(
595                    '{$this->searchPrefix}Combined|$searchTerms', '$searchType', $resultCount
596                );
597
598            EOT;
599    }
600
601    /**
602     * Get Page View Tracking Code
603     *
604     * @return string JavaScript Code Fragment
605     */
606    protected function getTrackPageViewCode()
607    {
608        return <<<EOT
609                VuFindPiwikTracker.trackPageView();
610
611            EOT;
612    }
613}