Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
46.15% covered (danger)
46.15%
60 / 130
50.00% covered (danger)
50.00%
7 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
OpenUrl
46.15% covered (danger)
46.15%
60 / 130
50.00% covered (danger)
50.00%
7 / 14
682.65
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 __invoke
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 addImageBasedParams
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
42
 renderTemplate
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
210
 getImageBasedLinkingMode
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 imageBasedLinkingIsActive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isActive
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 checkContext
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 checkIfRulesApply
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
6.10
 checkExcludedRecordsRules
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 checkSupportedRecordsRules
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 hasNonEmptyValue
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 checkMethodRules
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
8
 checkRules
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
6.05
1<?php
2
3/**
4 * OpenUrl view helper
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2010.
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   Demian Katz <demian.katz@villanova.edu>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org/wiki/development Wiki
28 */
29
30namespace VuFind\View\Helper\Root;
31
32use VuFind\Resolver\Driver\PluginManager;
33
34use function count;
35use function in_array;
36use function is_callable;
37
38/**
39 * OpenUrl view helper
40 *
41 * @category VuFind
42 * @package  View_Helpers
43 * @author   Demian Katz <demian.katz@villanova.edu>
44 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
45 * @link     https://vufind.org/wiki/development Wiki
46 */
47class OpenUrl extends \Laminas\View\Helper\AbstractHelper
48{
49    /**
50     * Context helper
51     *
52     * @var \VuFind\View\Helper\Root\Context
53     */
54    protected $context;
55
56    /**
57     * VuFind OpenURL configuration
58     *
59     * @var \Laminas\Config\Config
60     */
61    protected $config;
62
63    /**
64     * OpenURL rules
65     *
66     * @var array
67     */
68    protected $openUrlRules;
69
70    /**
71     * Resolver plugin manager
72     *
73     * @var PluginManager
74     */
75    protected $resolverPluginManager;
76
77    /**
78     * Current RecordDriver
79     *
80     * @var \VuFind\RecordDriver
81     */
82    protected $recordDriver;
83
84    /**
85     * OpenURL context ('results', 'record' or 'holdings')
86     *
87     * @var string
88     */
89    protected $area;
90
91    /**
92     * Constructor
93     *
94     * @param Context                $context       Context helper
95     * @param array                  $openUrlRules  VuFind OpenURL rules
96     * @param PluginManager          $pluginManager Resolver plugin manager
97     * @param \Laminas\Config\Config $config        VuFind OpenURL config
98     */
99    public function __construct(
100        Context $context,
101        $openUrlRules,
102        PluginManager $pluginManager,
103        $config = null
104    ) {
105        $this->context = $context;
106        $this->openUrlRules = $openUrlRules;
107        $this->resolverPluginManager = $pluginManager;
108        $this->config = $config;
109    }
110
111    /**
112     * Set up context for helper
113     *
114     * @param \VuFind\RecordDriver $driver The current record driver
115     * @param string               $area   OpenURL context ('results', 'record'
116     *  or 'holdings'
117     *
118     * @return object
119     */
120    public function __invoke($driver, $area)
121    {
122        $this->recordDriver = $driver;
123        $this->area = $area;
124        return $this;
125    }
126
127    /**
128     * Support method for renderTemplate() -- process image based parameters.
129     *
130     * @param bool  $imagebased Indicates if an image based link
131     * should be displayed or not (null for system default)
132     * @param array $params     OpenUrl parameters set so far
133     *
134     * @return void
135     */
136    protected function addImageBasedParams($imagebased, &$params)
137    {
138        $params['openUrlImageBasedMode'] = $this->getImageBasedLinkingMode();
139        $params['openUrlImageBasedSrc'] = null;
140
141        if (null === $imagebased) {
142            $imagebased = $this->imageBasedLinkingIsActive();
143        }
144
145        if ($imagebased) {
146            if (!isset($this->config->dynamic_graphic)) {
147                // if imagebased linking is forced by the template, but it is not
148                // configured properly, throw an exception
149                throw new \Exception(
150                    'Template tries to display OpenURL as image based link, but
151                     Image based linking is not configured! Please set parameter
152                     dynamic_graphic in config file.'
153                );
154            }
155
156            // Check if we have an image-specific OpenURL to use to override
157            // the default value when linking the image.
158            $params['openUrlImageBasedOverride'] = $this->recordDriver
159                ->tryMethod('getImageBasedOpenUrl');
160
161            // Concatenate image based OpenUrl base and OpenUrl
162            // to a usable image reference
163            $base = $this->config->dynamic_graphic;
164            $imageOpenUrl = $params['openUrlImageBasedOverride']
165                ? $params['openUrlImageBasedOverride'] : $params['openUrl'];
166            $params['openUrlImageBasedSrc'] = $base
167                . ((!str_contains($base, '?')) ? '?' : '&')
168                . $imageOpenUrl;
169        }
170
171        return $params;
172    }
173
174    /**
175     * Public method to render the OpenURL template
176     *
177     * @param bool $imagebased Indicates if an image based link
178     * should be displayed or not (null for system default)
179     *
180     * @return string
181     */
182    public function renderTemplate($imagebased = null)
183    {
184        if (null !== $this->config && isset($this->config->url)) {
185            // Trim off any parameters (for legacy compatibility -- default config
186            // used to include extraneous parameters):
187            [$base] = explode('?', $this->config->url);
188        } else {
189            $base = false;
190        }
191
192        $embed = (isset($this->config->embed) && !empty($this->config->embed));
193
194        $embedAutoLoad = $this->config->embed_auto_load ?? false;
195        // ini values 'true'/'false' are provided via ini reader as 1/0
196        // only check embedAutoLoad for area if the current area passed checkContext
197        if (
198            !($embedAutoLoad === '1' || $embedAutoLoad === '0')
199            && !empty($this->area)
200        ) {
201            // embedAutoLoad is neither true nor false, so check if it contains an
202            // area string defining where exactly to use autoloading
203            $embedAutoLoad = in_array(
204                strtolower($this->area),
205                array_map(
206                    'trim',
207                    array_map(
208                        'strtolower',
209                        explode(',', $embedAutoLoad)
210                    )
211                )
212            );
213        }
214
215        // instantiate the resolver plugin to get a proper resolver link
216        $resolver = $this->config->resolver ?? 'other';
217        $openurl = $this->recordDriver->getOpenUrl();
218        if ($this->resolverPluginManager->has($resolver)) {
219            $resolverObj = new \VuFind\Resolver\Connection(
220                $this->resolverPluginManager->get($resolver)
221            );
222            $resolverUrl = $resolverObj->getResolverUrl($openurl);
223        } else {
224            $resolverUrl = empty($base) ? '' : $base . '?' . $openurl;
225        }
226
227        // Build parameters needed to display the control:
228        $params = [
229            'resolverUrl' => $resolverUrl,
230            'openUrl' => $openurl,
231            'openUrlBase' => empty($base) ? false : $base,
232            'openUrlWindow' => empty($this->config->window_settings)
233                ? false : $this->config->window_settings,
234            'openUrlGraphic' => empty($this->config->graphic)
235                ? false : $this->config->graphic,
236            'openUrlGraphicWidth' => empty($this->config->graphic_width)
237                ? false : $this->config->graphic_width,
238            'openUrlGraphicHeight' => empty($this->config->graphic_height)
239                ? false : $this->config->graphic_height,
240            'openUrlEmbed' => $embed,
241            'openUrlEmbedAutoLoad' => $embedAutoLoad,
242        ];
243        $this->addImageBasedParams($imagebased, $params);
244
245        // Render the subtemplate:
246        return ($this->context)($this->getView())
247            ->renderInContext('Helpers/openurl.phtml', $params);
248    }
249
250    /**
251     * Public method to check ImageBased Linking mode
252     *
253     * @return string|bool false if image based linking is not active,
254     * config image_based_linking_mode otherwise (default = 'both')
255     */
256    public function getImageBasedLinkingMode()
257    {
258        if (
259            $this->imageBasedLinkingIsActive()
260            && isset($this->config->image_based_linking_mode)
261        ) {
262            return $this->config->image_based_linking_mode;
263        }
264        return $this->imageBasedLinkingIsActive() ? 'both' : false;
265    }
266
267    /**
268     * Public method to check if ImageBased Linking is enabled
269     *
270     * @return bool
271     */
272    public function imageBasedLinkingIsActive()
273    {
274        return isset($this->config->dynamic_graphic);
275    }
276
277    /**
278     * Public method to check whether OpenURLs are active for current record
279     *
280     * @return bool
281     */
282    public function isActive()
283    {
284        // check first if OpenURLs are enabled for this RecordDriver
285        // check second if OpenURLs are enabled for this context
286        // check last if any rules apply
287        if (
288            !$this->recordDriver->getOpenUrl()
289            || !$this->checkContext()
290            || !$this->checkIfRulesApply()
291        ) {
292            return false;
293        }
294        return true;
295    }
296
297    /**
298     * Does the OpenURL configuration indicate that we should display OpenURLs in
299     * the specified context?
300     *
301     * @return bool
302     */
303    protected function checkContext()
304    {
305        // Doesn't matter the target area if no OpenURL resolver is specified:
306        if (empty($this->config->url)) {
307            return false;
308        }
309
310        // If a setting exists, return that:
311        $key = 'show_in_' . $this->area;
312        if (isset($this->config->$key)) {
313            return $this->config->$key;
314        }
315
316        // If we got this far, use the defaults -- true for results, false for
317        // everywhere else.
318        return $this->area == 'results';
319    }
320
321    /**
322     * Check if the rulesets found apply to the current record. First match counts.
323     *
324     * @return bool
325     */
326    protected function checkIfRulesApply()
327    {
328        // special case if no rules are defined at all assume that any record is
329        // valid for openUrls
330        if (!isset($this->openUrlRules) || count($this->openUrlRules) < 1) {
331            return true;
332        }
333        foreach ($this->openUrlRules as $rules) {
334            if (
335                !$this->checkExcludedRecordsRules($rules)
336                && $this->checkSupportedRecordsRules($rules)
337            ) {
338                return true;
339            }
340        }
341        return false;
342    }
343
344    /**
345     * Check if "exclude" rules from the OpenUrlRules.json file apply to
346     * the current record
347     *
348     * @param array $resolverDriverRules Array of rules for a specific resolverDriver
349     *
350     * @return bool
351     */
352    protected function checkExcludedRecordsRules($resolverDriverRules)
353    {
354        if (isset($resolverDriverRules['exclude'])) {
355            // No exclusion rules mean no exclusions -- return false
356            return count($resolverDriverRules['exclude'])
357                ? $this->checkRules($resolverDriverRules['exclude']) : false;
358        }
359        return false;
360    }
361
362    /**
363     * Check if "include" rules from the OpenUrlRules.json file apply to
364     * the current record
365     *
366     * @param array $resolverDriverRules Array of rules for a specific resolverDriver
367     *
368     * @return bool
369     */
370    protected function checkSupportedRecordsRules($resolverDriverRules)
371    {
372        if (isset($resolverDriverRules['include'])) {
373            // No inclusion rules mean include everything -- return true
374            return count($resolverDriverRules['include'])
375                ? $this->checkRules($resolverDriverRules['include']) : true;
376        }
377        return false;
378    }
379
380    /**
381     * Check if an array contains a non-empty value.
382     *
383     * @param array $in Array to check
384     *
385     * @return bool
386     */
387    protected function hasNonEmptyValue($in)
388    {
389        foreach ($in as $current) {
390            if (!empty($current)) {
391                return true;
392            }
393        }
394        return false;
395    }
396
397    /**
398     * Check if method rules match.
399     *
400     * @param array $rules Rules to check.
401     *
402     * @return bool
403     */
404    protected function checkMethodRules($rules)
405    {
406        $ruleMatchCounter = 0;
407        foreach ($rules as $key => $value) {
408            if (is_callable([$this->recordDriver, $key])) {
409                $value = (array)$value;
410                $recordValue = (array)$this->recordDriver->$key();
411
412                // wildcard present
413                if (in_array('*', $value)) {
414                    // Strip the wildcard out of the value list; what is left
415                    // is the set of values that MUST be found in the record.
416                    // If we subtract the record values from the required values
417                    // and still have something left behind, then the match fails
418                    // as long as SOME non-empty value was provided.
419                    $requiredValues = array_diff($value, ['*']);
420                    if (
421                        !count(array_diff($requiredValues, $recordValue))
422                        && $this->hasNonEmptyValue($recordValue)
423                    ) {
424                        $ruleMatchCounter++;
425                    }
426                } else {
427                    $valueCount = count($value);
428                    if (
429                        $valueCount == count($recordValue)
430                        && $valueCount == count(
431                            array_intersect($value, $recordValue)
432                        )
433                    ) {
434                        $ruleMatchCounter++;
435                    }
436                }
437            }
438        }
439
440        // Did all the rules match?
441        return $ruleMatchCounter == count($rules);
442    }
443
444    /**
445     * Checks if rules from the OpenUrlRules.json file apply to the current
446     * record
447     *
448     * @param array $ruleset Array of rules to be checked
449     *
450     * @return bool
451     */
452    protected function checkRules($ruleset)
453    {
454        // check each rule - first rule-match
455        foreach ($ruleset as $rule) {
456            // skip this rule if it's not relevant for the current RecordDriver
457            if (
458                isset($rule['recorddriver'])
459                && !($this->recordDriver instanceof $rule['recorddriver'])
460            ) {
461                continue;
462            }
463
464            // check if defined methods-rules apply for current record
465            if (isset($rule['methods'])) {
466                if ($this->checkMethodRules($rule['methods'])) {
467                    return true;
468                }
469            } else {
470                // no method rules? Then assume a match by default!
471                return true;
472            }
473        }
474        // no rule matched
475        return false;
476    }
477}