AttachmentPathResolver   A
last analyzed

Complexity

Total Complexity 25

Size/Duplication

Total Lines 346
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 182
c 1
b 0
f 0
dl 0
loc 346
rs 10
wmc 25

10 Methods

Rating   Name   Duplication   Size   Complexity  
A getMediaPath() 0 3 1
A convertOldFootprintPath() 0 8 2
A placeholderToRealPath() 0 31 5
A __construct() 0 24 3
A getFootprintsPath() 0 3 1
A parameterToAbsolutePath() 0 28 5
A realPathToPlaceholder() 0 27 4
A getModelsPath() 0 3 1
A getSecurePath() 0 3 1
A arrayToRegexArray() 0 10 2
1
<?php
2
/**
3
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4
 *
5
 * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
6
 *
7
 * This program is free software: you can redistribute it and/or modify
8
 * it under the terms of the GNU Affero General Public License as published
9
 * by the Free Software Foundation, either version 3 of the License, or
10
 * (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License
18
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19
 */
20
21
declare(strict_types=1);
22
23
namespace App\Services\Attachments;
24
25
use FontLib\Table\Type\maxp;
26
use const DIRECTORY_SEPARATOR;
27
use Symfony\Component\Filesystem\Filesystem;
28
29
/**
30
 * This service converts the relative pathes for attachments saved in database (like %MEDIA%/img.jpg) to real pathes
31
 * an vice versa.
32
 */
33
class AttachmentPathResolver
34
{
35
    protected string $project_dir;
36
37
    protected ?string $media_path;
38
    protected ?string $footprints_path;
39
    protected ?string $models_path;
40
    protected ?string $secure_path;
41
42
    protected array $placeholders;
43
    protected array $pathes;
44
    protected array $placeholders_regex;
45
    protected array $pathes_regex;
46
47
    /**
48
     * AttachmentPathResolver constructor.
49
     *
50
     * @param string      $project_dir     the kernel that should be used to resolve the project dir
51
     * @param string      $media_path      the path where uploaded attachments should be stored
52
     * @param string|null $footprints_path The path where builtin attachments are stored.
53
     *                                     Set to null if this ressource should be disabled.
54
     * @param string|null $models_path     set to null if this ressource should be disabled
55
     */
56
    public function __construct(string $project_dir, string $media_path, string $secure_path, ?string $footprints_path, ?string $models_path)
57
    {
58
        $this->project_dir = $project_dir;
59
60
        //Determine the path for our ressources
61
        $this->media_path = $this->parameterToAbsolutePath($media_path);
62
        $this->footprints_path = $this->parameterToAbsolutePath($footprints_path);
63
        $this->models_path = $this->parameterToAbsolutePath($models_path);
64
        $this->secure_path = $this->parameterToAbsolutePath($secure_path);
65
66
        //Here we define the valid placeholders and their replacement values
67
        $this->placeholders = ['%MEDIA%', '%BASE%/data/media', '%FOOTPRINTS%', '%FOOTPRINTS_3D%', '%SECURE%'];
68
        $this->pathes = [$this->media_path, $this->media_path, $this->footprints_path, $this->models_path, $this->secure_path];
69
70
        //Remove all disabled placeholders
71
        foreach ($this->pathes as $key => $path) {
72
            if (null === $path) {
73
                unset($this->placeholders[$key], $this->pathes[$key]);
74
            }
75
        }
76
77
        //Create the regex arrays
78
        $this->placeholders_regex = $this->arrayToRegexArray($this->placeholders);
79
        $this->pathes_regex = $this->arrayToRegexArray($this->pathes);
80
    }
81
82
    /**
83
     * Converts a path passed by parameter from services.yaml (which can be an absolute path or relative to project dir)
84
     * to an absolute path. When a relative path is passed, the directory must exist or null is returned.
85
     * Returns an absolute path with "/" no matter, what system is used.
86
     *
87
     * @internal
88
     *
89
     * @param string|null $param_path The parameter value that should be converted to a absolute path
90
     */
91
    public function parameterToAbsolutePath(?string $param_path): ?string
92
    {
93
        if (null === $param_path) {
94
            return null;
95
        }
96
97
        $fs = new Filesystem();
98
        //If current string is already an absolute path, then we have nothing to do
99
        if ($fs->isAbsolutePath($param_path)) {
100
            $tmp = realpath($param_path);
101
            //Disable ressource if path is not existing
102
            if (false === $tmp) {
103
                return null;
104
            }
105
106
            return $tmp;
107
        }
108
109
        //Otherwise prepend the project path
110
        $tmp = realpath($this->project_dir.DIRECTORY_SEPARATOR.$param_path);
111
112
        //If path does not exist then disable the placeholder
113
        if (false === $tmp) {
114
            return null;
115
        }
116
117
        //Normalize file path (use / instead of \)
118
        return str_replace('\\', '/', $tmp);
119
    }
120
121
    /**
122
     * Converts an relative placeholder filepath (with %MEDIA% or older %BASE%) to an absolute filepath on disk.
123
     * The directory separator is always /. Relative pathes are not realy possible (.. is striped).
124
     *
125
     * @param string $placeholder_path the filepath with placeholder for which the real path should be determined
126
     *
127
     * @return string|null The absolute real path of the file, or null if the placeholder path is invalid
128
     */
129
    public function placeholderToRealPath(string $placeholder_path): ?string
130
    {
131
        //The new attachments use %MEDIA% as placeholders, which is the directory set in media_directory
132
        //Older path entries are given via %BASE% which was the project root
133
134
        $count = 0;
135
136
        //When path is a footprint we have to first run the string through our lecagy german mapping functions
137
        if (strpos($placeholder_path, '%FOOTPRINTS%') !== false) {
138
            $placeholder_path = $this->convertOldFootprintPath($placeholder_path);
139
        }
140
141
        $placeholder_path = preg_replace($this->placeholders_regex, $this->pathes, $placeholder_path, -1, $count);
142
143
        //A valid placeholder can have only one
144
        if (1 !== $count) {
145
            return null;
146
        }
147
148
        //If we have now have a placeholder left, the string is invalid:
149
        if (preg_match('#%\w+%#', $placeholder_path)) {
150
            return null;
151
        }
152
153
        //Path is invalid if path is directory traversal
154
        if (false !== strpos($placeholder_path, '..')) {
155
            return null;
156
        }
157
158
        //Normalize path and remove .. (to prevent directory traversal attack)
159
        return str_replace(['\\'], ['/'], $placeholder_path);
160
    }
161
162
    /**
163
     * Converts an real absolute filepath to a placeholder version.
164
     *
165
     * @param string $real_path   the absolute path, for which the placeholder version should be generated
166
     * @param bool   $old_version By default the %MEDIA% placeholder is used, which is directly replaced with the
167
     *                            media directory. If set to true, the old version with %BASE% will be used, which is the project directory.
168
     *
169
     * @return string The placeholder version of the filepath
170
     */
171
    public function realPathToPlaceholder(string $real_path, bool $old_version = false): ?string
172
    {
173
        $count = 0;
174
175
        //Normalize path
176
        $real_path = str_replace('\\', '/', $real_path);
177
178
        if ($old_version) {
179
            //We need to remove the %MEDIA% placeholder (element 0)
180
            $pathes = $this->pathes_regex;
181
            $placeholders = $this->placeholders;
182
            unset($pathes[0], $placeholders[0]);
183
            $real_path = preg_replace($pathes, $placeholders, $real_path, -1, $count);
184
        } else {
185
            $real_path = preg_replace($this->pathes_regex, $this->placeholders, $real_path, -1, $count);
186
        }
187
188
        if (1 !== $count) {
189
            return null;
190
        }
191
192
        //If the new string does not begin with a placeholder, it is invalid
193
        if (!preg_match('#^%\w+%#', $real_path)) {
194
            return null;
195
        }
196
197
        return $real_path;
198
    }
199
200
    /**
201
     * The path where uploaded attachments is stored.
202
     *
203
     * @return string the absolute path to the media folder
204
     */
205
    public function getMediaPath(): string
206
    {
207
        return $this->media_path;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->media_path could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
208
    }
209
210
    /**
211
     * The path where secured attachments are stored. Must not be located in public/ folder, so it can only be accessed
212
     * via the attachment controller.
213
     *
214
     * @return string the absolute path to the secure path
215
     */
216
    public function getSecurePath(): string
217
    {
218
        return $this->secure_path;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->secure_path could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
219
    }
220
221
    /**
222
     * The string where the builtin footprints are stored.
223
     *
224
     * @return string|null The absolute path to the footprints folder. Null if built footprints were disabled.
225
     */
226
    public function getFootprintsPath(): ?string
227
    {
228
        return $this->footprints_path;
229
    }
230
231
    /**
232
     * The string where the builtin 3D models are stored.
233
     *
234
     * @return string|null The absolute path to the models folder. Null if builtin models were disabled.
235
     */
236
    public function getModelsPath(): ?string
237
    {
238
        return $this->models_path;
239
    }
240
241
    /**
242
     * Create an array usable for preg_replace out of an array of placeholders or pathes.
243
     * Slashes and other chars become escaped.
244
     * For example: '%TEST%' becomes '/^%TEST%/'.
245
     */
246
    protected function arrayToRegexArray(array $array): array
247
    {
248
        $ret = [];
249
250
        foreach ($array as $item) {
251
            $item = str_replace(['\\'], ['/'], $item);
252
            $ret[] = '/'.preg_quote($item, '/').'/';
253
        }
254
255
        return $ret;
256
    }
257
258
    private const OLD_FOOTPINT_PATH_REPLACEMENT = [
259
        'Aktiv' => 'Active',
260
        'Bedrahtet' => 'THT',
261
        'Dioden' => 'Diodes',
262
        'Gleichrichter' => 'Rectifier',
263
        'GLEICHRICHTER' => 'RECTIFIER',
264
        'Oszillatoren' => 'Oscillator',
265
        'Keramikresonatoren_SMD' => 'CeramicResonator_SMD',
266
        'Quarze_bedrahtet' => 'Crystals_THT',
267
        'QUARZ' => 'CRYSTAL',
268
        'Quarze_SMD' => 'Crystals_SMD',
269
        'Quarzoszillatoren_bedrahtet' =>  'CrystalOscillator_THT',
270
        'QUARZOSZILLATOR' => 'CRYSTAL_OSCILLATOR',
271
        'Quarzoszillatoren_SMD' => 'CrystalOscillator_SMD',
272
        'Schaltregler' => 'SwitchingRegulator',
273
        'SCHALTREGLER' => 'SWITCHING_REGULATOR',
274
        'Akustik' => 'Acoustics',
275
        'Elektromechanik' => 'Electromechanics',
276
        'Drahtbruecken' => 'WireJumpers',
277
        'DRAHTBRUECKE' => 'WIREJUMPER',
278
        'IC-Sockel' => 'IC-Socket',
279
        'SOCKEL' => 'SOCKET',
280
        'Kuehlkoerper' => 'Heatsinks',
281
        'KUEHLKOERPER' => 'HEATSINK',
282
        'Relais' => 'Relays',
283
        'RELAIS' => 'RELAY',
284
        'Schalter_Taster' => 'Switches_Buttons',
285
        'Drehschalter' => 'RotarySwitches',
286
        'DREHSCHALTER' => 'ROTARY_SWITCH',
287
        'Drucktaster' => 'Button',
288
        'TASTER' => 'BUTTON',
289
        'Kippschalter' => 'ToggleSwitch',
290
        'KIPPSCHALTER' => 'TOGGLE_SWITCH',
291
        'Gewinde' => 'Threaded',
292
        'abgewinkelt' => 'angled',
293
        'hochkant' => 'vertical',
294
        'stehend' => 'vertical',
295
        'liegend' => 'horizontal',
296
        '_WECHSLER' => '',
297
        'Schiebeschalter' => 'SlideSwitch',
298
        'SCHIEBESCHALTER' => 'SLIDE_SWITCH',
299
        'Sicherungshalter' => 'Fuseholder',
300
        'SICHERUNGSHALTER_Laengs' => 'FUSEHOLDER_Lenghtway',
301
        'SICHERUNGSHALTER_Quer' => 'FUSEHOLDER_Across',
302
        'Speicherkartenslots' => 'MemoryCardSlots',
303
        'KARTENSLOT' => 'CARD_SLOT',
304
        'SD-Karte' => 'SD_Card',
305
        'Rot' => 'Red',
306
        'Schwarz' => 'Black',
307
        'Verbinder' => 'Connectors',
308
        'BUCHSE' => 'SOCKET',
309
        'Buchsenleisten' => 'SocketStrips',
310
        'Reihig' => 'Row',
311
        'gerade' => 'straight',
312
        'flach' => 'flat',
313
        'praezisions' => 'precision',
314
        'praezision' => 'precision',
315
        'BUCHSENLEISTE' => 'SOCKET_STRIP',
316
        'GERADE' => 'STRAIGHT',
317
        'FLACH' => 'FLAT',
318
        'PRAEZISION' => 'PRECISION',
319
        'ABGEWINKELT' => 'ANGLED',
320
        'Federkraftklemmen' => 'SpringClamps',
321
        'SCHRAUBKLEMME' => 'SCREW_CLAMP',
322
        'KLEMME' => 'CLAMP',
323
        'VERBINDER' => 'CONNECTOR',
324
        'Loetoesen' => 'SolderingPads',
325
        'LOETOESE' => 'SOLDERING_PAD',
326
        'Rundsteckverbinder' => 'DINConnectors',
327
        'Schraubklemmen' => 'ScrewClamps',
328
        'Sonstiges' => 'Miscellaneous',
329
        'Stiftleisten' => 'PinHeaders',
330
        'STIFTLEISTE' => 'PIN_HEADER',
331
        'mit_Rahmen' => 'with_frame',
332
        'RAHMEN' => 'FRAME',
333
        'Maennlich' => 'Male',
334
        'Platinenmontage' => 'PCBMount',
335
        'PLATINENMONTAGE' => 'PCB_MOUNT',
336
        'Weiblich' => 'Female',
337
        'Optik' => 'Optics',
338
        'BLAU' => 'BLUE',
339
        'GELD' => 'YELLOW',
340
        'GRUEN' => 'GREEN',
341
        'ROT' => 'RED',
342
        'eckig' => 'square',
343
        'Passiv' => 'Passive',
344
        'EMV' => 'EMC',
345
        'Induktivitaeten' => 'Inductors',
346
        'SPULE' => 'COIL',
347
        'Kondensatoren' => 'Capacitors',
348
        'ELKO' => 'Electrolyte',
349
        'Elektrolyt' => 'Electrolyte',
350
        'Folie' => 'Film',
351
        'FOLIENKONDENSATOR' => 'FILM_CAPACITOR',
352
        'Keramik' => 'Ceramic',
353
        'KERKO' => 'Ceramic',
354
        'Tantal' => 'Tantalum',
355
        'TANTAL' => 'TANTALUM',
356
        'Trimmkondensatoren' => 'TrimmerCapacitors',
357
        'TRIMMKONDENSATOR' => 'TRIMMER_CAPACITOR',
358
        'KONDENSATOR' => 'CAPACITOR',
359
        'Transformatoren' => 'Transformers',
360
        'TRAFO' => 'TRANSFORMER',
361
        'Widerstaende' => 'Resistors',
362
        'WIDERSTAND' => 'RESISTOR',
363
        'Dickschicht' => 'ThickFilm',
364
        'DICKSCHICHT' => 'THICK_FILM',
365
        'KERAMIK' => 'CERAMIC',
366
        'Kohleschicht' => 'Carbon',
367
        'KOHLE' => 'CARBON',
368
        'Sonstige' => 'Miscellaneous', //Have to be last (after "Sonstiges")
369
    ];
370
371
    public function convertOldFootprintPath(string $old_path): string
372
    {
373
        //Only do the conversion if it contains a german string (meaning it has one of the four former base folders in its path)
374
        if (!preg_match('/%FOOTPRINTS%\/(Passiv|Aktiv|Akustik|Elektromechanik|Optik)\//', $old_path)) {
375
            return $old_path;
376
        }
377
378
        return strtr($old_path, self::OLD_FOOTPINT_PATH_REPLACEMENT);
379
    }
380
}
381