Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.20% covered (success)
98.20%
109 / 111
92.86% covered (success)
92.86%
13 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
Export
98.20% covered (success)
98.20%
109 / 111
92.86% covered (success)
92.86%
13 / 14
56
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getBulkUrl
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 getRedirectUrl
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
7
 needsRedirect
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 processGroup
91.67% covered (success)
91.67%
22 / 24
0.00% covered (danger)
0.00%
0 / 1
8.04
 recordSupportsFormat
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 getFormatsForRecord
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getFormatsForRecords
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 getHeaders
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLabelForFormat
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBulkExportType
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getActiveFormats
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
14
 getPostField
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getTargetWindow
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3/**
4 * Export support class
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  Export
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 Main Site
28 */
29
30namespace VuFind;
31
32use Laminas\Config\Config;
33use Laminas\View\Renderer\PhpRenderer;
34
35use function in_array;
36use function is_callable;
37
38/**
39 * Export support class
40 *
41 * @category VuFind
42 * @package  Export
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 Main Site
46 */
47class Export
48{
49    /**
50     * Main VuFind configuration
51     *
52     * @var Config
53     */
54    protected $mainConfig;
55
56    /**
57     * Export-specific configuration
58     *
59     * @var Config
60     */
61    protected $exportConfig;
62
63    /**
64     * Property to cache active formats
65     * (initialized to empty array , populated later)
66     *
67     * @var array
68     */
69    protected $activeFormats = [];
70
71    /**
72     * Constructor
73     *
74     * @param Config $mainConfig   Main VuFind configuration
75     * @param Config $exportConfig Export-specific configuration
76     */
77    public function __construct(Config $mainConfig, Config $exportConfig)
78    {
79        $this->mainConfig = $mainConfig;
80        $this->exportConfig = $exportConfig;
81    }
82
83    /**
84     * Get the URL for bulk export.
85     *
86     * @param PhpRenderer $view   View object (needed for URL generation)
87     * @param string      $format Export format being used
88     * @param array       $ids    Array of IDs to export (in source|id format)
89     *
90     * @return string
91     */
92    public function getBulkUrl($view, $format, $ids)
93    {
94        $params = ['f=' . urlencode($format)];
95        foreach ($ids as $id) {
96            $params[] = urlencode('i[]') . '=' . urlencode($id);
97        }
98        $serverUrlHelper = $view->plugin('serverurl');
99        $urlHelper = $view->plugin('url');
100        $url = $serverUrlHelper($urlHelper('cart-doexport'))
101            . '?' . implode('&', $params);
102
103        return $this->needsRedirect($format)
104            ? $this->getRedirectUrl($format, $url) : $url;
105    }
106
107    /**
108     * Build callback URL for export.
109     *
110     * @param string $format   Export format being used
111     * @param string $callback Callback URL for retrieving record(s)
112     *
113     * @return string
114     */
115    public function getRedirectUrl($format, $callback)
116    {
117        // Fill in special tokens in template:
118        $template = $this->exportConfig->$format->redirectUrl;
119        preg_match_all('/\{([^}]+)\}/', $template, $matches);
120        foreach ($matches[1] as $current) {
121            $parts = explode('|', $current);
122            switch ($parts[0]) {
123                case 'config':
124                case 'encodedConfig':
125                    if (isset($this->mainConfig->{$parts[1]}->{$parts[2]})) {
126                        $value = $this->mainConfig->{$parts[1]}->{$parts[2]};
127                    } else {
128                        $value = $parts[3];
129                    }
130                    if ($parts[0] == 'encodedConfig') {
131                        $value = urlencode($value);
132                    }
133                    $template = str_replace('{' . $current . '}', $value, $template);
134                    break;
135                case 'encodedCallback':
136                    $template = str_replace(
137                        '{' . $current . '}',
138                        urlencode($callback),
139                        $template
140                    );
141                    break;
142            }
143        }
144        return $template;
145    }
146
147    /**
148     * Does the requested format require a redirect?
149     *
150     * @param string $format Format to check
151     *
152     * @return bool
153     */
154    public function needsRedirect($format)
155    {
156        return !empty($this->exportConfig->$format->redirectUrl)
157            && 'link' === $this->getBulkExportType($format);
158    }
159
160    /**
161     * Convert an array of individual records into a single string for display.
162     *
163     * @param string $format Format of records to process
164     * @param array  $parts  Multiple records to process
165     *
166     * @return string
167     */
168    public function processGroup($format, $parts)
169    {
170        // If we're in XML mode, we need to do some special processing:
171        if (isset($this->exportConfig->$format->combineXpath)) {
172            $ns = isset($this->exportConfig->$format->combineNamespaces)
173                ? $this->exportConfig->$format->combineNamespaces->toArray()
174                : [];
175            $ns = array_map(
176                function ($current) {
177                    return explode('|', $current, 2);
178                },
179                $ns
180            );
181            if (empty($parts)) {
182                return '';
183            }
184            foreach ($parts as $part) {
185                // Convert text into XML object:
186                $current = simplexml_load_string($part);
187
188                // The first record gets kept as-is; subsequent records get merged
189                // in based on the configured XPath (currently only one level is
190                // supported)...
191                if (!isset($retVal)) {
192                    $retVal = $current;
193                } else {
194                    foreach ($ns as $n) {
195                        $current->registerXPathNamespace($n[0], $n[1]);
196                    }
197                    $matches = $current->xpath(
198                        $this->exportConfig->$format->combineXpath
199                    );
200                    foreach ($matches as $match) {
201                        SimpleXML::appendElement($retVal, $match);
202                    }
203                }
204            }
205            return $retVal->asXML();
206        } else {
207            // Not in XML mode -- just concatenate everything together:
208            return implode('', $parts);
209        }
210    }
211
212    /**
213     * Does the specified record support the specified export format?
214     *
215     * @param \VuFind\RecordDriver\AbstractBase $driver Record driver
216     * @param string                            $format Format to check
217     *
218     * @return bool
219     */
220    public function recordSupportsFormat($driver, $format)
221    {
222        // Check if the driver explicitly disallows the format:
223        if ($driver->tryMethod('exportDisabled', [$format])) {
224            return false;
225        }
226
227        // Check the requirements for export in the requested format:
228        if (isset($this->exportConfig->$format)) {
229            if (isset($this->exportConfig->$format->requiredMethods)) {
230                foreach ($this->exportConfig->$format->requiredMethods as $method) {
231                    // If a required method is missing, give up now:
232                    if (!is_callable([$driver, $method])) {
233                        return false;
234                    }
235                }
236            }
237            // If we got this far, we didn't encounter a problem, and the
238            // requested export format is valid, so we can report success!
239            return true;
240        }
241
242        // If we got this far, we couldn't find evidence of support:
243        return false;
244    }
245
246    /**
247     * Get an array of strings representing formats in which a specified record's
248     * data may be exported (empty if none). Legal values: "BibTeX", "EndNote",
249     * "MARC", "MARCXML", "RDF", "RefWorks".
250     *
251     * @param \VuFind\RecordDriver\AbstractBase $driver Record driver
252     *
253     * @return array Strings representing export formats.
254     */
255    public function getFormatsForRecord($driver)
256    {
257        // Get an array of enabled export formats (from config, or use defaults
258        // if nothing in config array).
259        $active = $this->getActiveFormats('record');
260
261        // Loop through all possible formats:
262        $formats = [];
263        foreach (array_keys($this->exportConfig->toArray()) as $format) {
264            if (
265                in_array($format, $active)
266                && $this->recordSupportsFormat($driver, $format)
267            ) {
268                $formats[] = $format;
269            }
270        }
271
272        // Send back the results:
273        return $formats;
274    }
275
276    /**
277     * Same return value as getFormatsForRecord(), but filtered to reflect bulk
278     * export configuration and to list only values supported by a set of records.
279     *
280     * @param array $drivers Array of record drivers
281     *
282     * @return array
283     */
284    public function getFormatsForRecords($drivers)
285    {
286        $formats = $this->getActiveFormats('bulk');
287        foreach ($drivers as $driver) {
288            // Filter out unsupported export formats:
289            $newFormats = [];
290            foreach ($formats as $current) {
291                if ($this->recordSupportsFormat($driver, $current)) {
292                    $newFormats[] = $current;
293                }
294            }
295            $formats = $newFormats;
296        }
297        return $formats;
298    }
299
300    /**
301     * Get headers for the requested format.
302     *
303     * @param string $format Selected export format
304     *
305     * @return array
306     */
307    public function getHeaders($format)
308    {
309        return $this->exportConfig->$format->headers ?? [];
310    }
311
312    /**
313     * Get the display label for the specified export format.
314     *
315     * @param string $format Format identifier
316     *
317     * @return string
318     */
319    public function getLabelForFormat($format)
320    {
321        return $this->exportConfig->$format->label ?? $format;
322    }
323
324    /**
325     * Get the bulk export type for the specified export format.
326     *
327     * @param string $format Format identifier
328     *
329     * @return string
330     */
331    public function getBulkExportType($format)
332    {
333        // if exportType is set on per-format basis in export.ini then use it
334        // else check if export type is set in config.ini
335        return $this->exportConfig->$format->bulkExportType
336            ?? $this->mainConfig->BulkExport->defaultType ?? 'link';
337    }
338
339    /**
340     * Get active export formats for the given context.
341     *
342     * @param string $context Export context (i.e. record, bulk)
343     *
344     * @return array
345     */
346    public function getActiveFormats($context = 'record')
347    {
348        if (!isset($this->activeFormats[$context])) {
349            $formatSettings = isset($this->mainConfig->Export)
350                ? $this->mainConfig->Export->toArray()
351                : ['RefWorks' => 'record,bulk', 'EndNote' => 'record,bulk'];
352
353            $active = [];
354            foreach ($formatSettings as $format => $allowedContexts) {
355                if (
356                    str_contains($allowedContexts, $context)
357                    || ($context == 'record' && $allowedContexts == 1)
358                ) {
359                    $active[] = $format;
360                }
361            }
362
363            // for legacy settings [BulkExport]
364            if (
365                $context == 'bulk'
366                && isset($this->mainConfig->BulkExport->enabled)
367                && $this->mainConfig->BulkExport->enabled
368                && isset($this->mainConfig->BulkExport->options)
369            ) {
370                $config = explode(':', $this->mainConfig->BulkExport->options);
371                foreach ($config as $option) {
372                    if (
373                        isset($this->mainConfig->Export->$option)
374                        && $this->mainConfig->Export->$option == true
375                    ) {
376                        $active[] = $option;
377                    }
378                }
379            }
380            $this->activeFormats[$context] = array_unique($active);
381        }
382        return $this->activeFormats[$context];
383    }
384
385    /**
386     * Get the export POST field name.
387     *
388     * @param string $format Format identifier
389     *
390     * @return string
391     */
392    public function getPostField($format)
393    {
394        return !empty($this->exportConfig->$format->postField)
395            ? $this->exportConfig->$format->postField : 'ImportData';
396    }
397
398    /**
399     * Get the export target window.
400     *
401     * @param string $format Format identifier
402     *
403     * @return string
404     */
405    public function getTargetWindow($format)
406    {
407        return !empty($this->exportConfig->$format->targetWindow)
408            ? $this->exportConfig->$format->targetWindow
409            : $format . 'Main';
410    }
411}