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
![]() |
|||
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
|
|||
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 |