Passed
Push — master ( 21cfa8...7dda91 )
by
unknown
13:30
created

FileViewHelper::initializeArguments()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
c 1
b 0
f 0
dl 0
loc 11
rs 9.9666
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Fluid\ViewHelpers\Link;
19
20
use TYPO3\CMS\Core\Core\Environment;
21
use TYPO3\CMS\Core\Resource\File;
22
use TYPO3\CMS\Core\Resource\FileInterface;
23
use TYPO3\CMS\Core\Resource\FileReference;
24
use TYPO3\CMS\Core\Resource\ProcessedFile;
25
use TYPO3\CMS\Core\Utility\GeneralUtility;
26
use TYPO3\CMS\Core\Utility\PathUtility;
27
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
28
use TYPO3Fluid\Fluid\Core\ViewHelper\Exception;
29
30
/**
31
 * A ViewHelper for creating links to a file (FAL).
32
 *
33
 * Examples
34
 * ========
35
 *
36
 * Link to a file
37
 * --------------
38
 *
39
 * ::
40
 *
41
 *    <f:link.file file="{file}" target="_blank">See file</f:link.file>
42
 *
43
 * Output of a public file::
44
 *
45
 *    <a href="https://example.com/fileadmin/path/to/file.jpg" target="_blank">See file</a>
46
 *
47
 * Output of a non-public file::
48
 *
49
 *    <a href="https://example.com/index.php?eID=dumpFile&t=f&f=123&token=79bce812" target="_blank">See file</a>
50
 *
51
 * Link to download a file
52
 * -----------------------
53
 *
54
 * ::
55
 *
56
 *    <f:link.file file="{file}" download="true" filename="alternative-name.jpg">Download file</f:link.file>
57
 *
58
 * Output of a public file::
59
 *
60
 *    <a href="https://example.com/fileadmin/path/to/file.jpg" download="alternative-name.jpg">Download file</a>
61
 *
62
 * Output of a non-public file::
63
 *
64
 *    <a href="https://example.com/index.php?eID=dumpFile&t=f&f=123&dl=1&fn=alternative-name.jpg&token=79bce812">Download file</a>
65
 */
66
class FileViewHelper extends AbstractTagBasedViewHelper
67
{
68
    /**
69
     * @var string
70
     */
71
    protected $tagName = 'a';
72
73
    public function initializeArguments(): void
74
    {
75
        parent::initializeArguments();
76
        $this->registerArgument('file', FileInterface::class, 'Specifies the file to create a link to', true);
77
        $this->registerArgument('download', 'bool', 'Specifies if file should be downloaded instead of displayed');
78
        $this->registerArgument('filename', 'string', 'Specifies an alternative filename. If filename contains a file extension, this must be the same as from \'file\'.');
79
        $this->registerUniversalTagAttributes();
80
        $this->registerTagAttribute('name', 'string', 'Specifies the name of an anchor');
81
        $this->registerTagAttribute('rel', 'string', 'Specifies the relationship between the current document and the linked document');
82
        $this->registerTagAttribute('rev', 'string', 'Specifies the relationship between the linked document and the current document');
83
        $this->registerTagAttribute('target', 'string', 'Specifies where to open the linked document');
84
    }
85
86
    public function render(): string
87
    {
88
        $file = $this->arguments['file'];
89
90
        if (!($file instanceof FileInterface)) {
91
            throw new Exception('Argument \'file\' must be an instance of ' . FileInterface::class, 1621511632);
92
        }
93
94
        // Get the public URL. This url is either be defined by a GeneratePublicUrlForResourceEvent,
95
        // an OnlineMedia helper, the corresponding driver or using the file dump functionality.
96
        $publicUrl = $file->getPublicUrl();
97
98
        // Early return in case public url is null as this indicates the file is
99
        // not accessible, e.g. because the corresponding storage is offline.
100
        if ($publicUrl === null) {
101
            return '';
102
        }
103
104
        if (strpos($publicUrl, 'dumpFile') !== false) {
105
            // In case we deal with is a file dump URL, recreate the URL
106
            // by taking the defined view helper arguments into account.
107
            $publicUrl = $this->createFileDumpUrl($file);
108
        } elseif ($this->arguments['download'] ?? false) {
109
            // In case the URL directly links to the file (no eID) and
110
            // the file should be downloaded instead of displayed, this
111
            // must be set by the "download" tag attribute, which may
112
            // contain an alternative filename.
113
            $this->tag->addAttribute(
114
                'download',
115
                $this->getAlternativeFilename($file)
116
            );
117
        }
118
119
        $this->tag->addAttribute('href', $publicUrl);
120
        $this->tag->setContent($this->renderChildren() ?? htmlspecialchars($file->getName()));
121
        $this->tag->forceClosingTag(true);
122
123
        return $this->tag->render();
124
    }
125
126
    /**
127
     * Create a file dump URL, taking the view helper arguments into account
128
     */
129
    protected function createFileDumpUrl(FileInterface $file): string
130
    {
131
        $parameters = ['eID' => 'dumpFile'];
132
133
        if ($file instanceof File) {
134
            $parameters['t'] = 'f';
135
            $parameters['f'] = $file->getUid();
136
        } elseif ($file instanceof FileReference) {
137
            $parameters['t'] = 'r';
138
            $parameters['r'] = $file->getUid();
139
        } elseif ($file instanceof ProcessedFile) {
140
            $parameters['t'] = 'p';
141
            $parameters['p'] = $file->getUid();
142
        }
143
144
        if ($download = $this->arguments['download'] ?? false) {
145
            $parameters['dl'] = (int)$download;
146
        }
147
148
        if (($filename = $this->getAlternativeFilename($file)) !== '') {
149
            $parameters['fn'] = $filename;
150
        }
151
152
        $parameters['token'] = GeneralUtility::hmac(implode('|', $parameters), 'resourceStorageDumpFile');
153
154
        return  GeneralUtility::locationHeaderUrl(PathUtility::getAbsoluteWebPath(Environment::getPublicPath() . '/index.php'))
155
            . '?' . http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
156
    }
157
158
    protected function getAlternativeFilename(FileInterface $file): string
159
    {
160
        $alternativeFilename = $this->arguments['filename'] ?? '';
161
162
        // Return early if filename is empty or not valid
163
        if ($alternativeFilename === '' || !preg_match('/^[0-9a-z._\-]+$/i', $alternativeFilename)) {
164
            return '';
165
        }
166
167
        $extension = pathinfo($alternativeFilename, PATHINFO_EXTENSION);
168
        if ($extension === '') {
169
            // Add original extension in case alternative filename did not contain any
170
            $alternativeFilename = rtrim($alternativeFilename, '.') . '.' . $file->getExtension();
171
        }
172
173
        // Check if given or resolved extension matches the original one
174
        return $file->getExtension() === pathinfo($alternativeFilename, PATHINFO_EXTENSION)
175
            ? $alternativeFilename
176
            : '';
177
    }
178
}
179