Attachment::isExternal()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 11
rs 10
c 0
b 0
f 0
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\Entity\Attachments;
24
25
use App\Entity\Base\AbstractNamedDBElement;
26
use App\Validator\Constraints\Selectable;
27
use Doctrine\ORM\Mapping as ORM;
28
use Symfony\Component\Validator\Constraints as Assert;
29
use function in_array;
30
use InvalidArgumentException;
31
use LogicException;
32
33
/**
34
 * Class Attachment.
35
 *
36
 * @ORM\Entity(repositoryClass="App\Repository\AttachmentRepository")
37
 * @ORM\Table(name="`attachments`", indexes={
38
 *    @ORM\Index(name="attachments_idx_id_element_id_class_name", columns={"id", "element_id", "class_name"}),
39
 *    @ORM\Index(name="attachments_idx_class_name_id", columns={"class_name", "id"}),
40
 *    @ORM\Index(name="attachment_name_idx", columns={"name"}),
41
 *    @ORM\Index(name="attachment_element_idx", columns={"class_name", "element_id"})
42
 * })
43
 * @ORM\InheritanceType("SINGLE_TABLE")
44
 * @ORM\DiscriminatorColumn(name="class_name", type="string")
45
 * @ORM\DiscriminatorMap({
46
 *     "PartDB\Part" = "PartAttachment", "Part" = "PartAttachment",
47
 *     "PartDB\Device" = "ProjectAttachment", "Device" = "ProjectAttachment",
48
 *     "AttachmentType" = "AttachmentTypeAttachment", "Category" = "CategoryAttachment",
49
 *     "Footprint" = "FootprintAttachment", "Manufacturer" = "ManufacturerAttachment",
50
 *     "Currency" = "CurrencyAttachment", "Group" = "GroupAttachment",
51
 *     "MeasurementUnit" = "MeasurementUnitAttachment", "Storelocation" = "StorelocationAttachment",
52
 *     "Supplier" = "SupplierAttachment", "User" = "UserAttachment", "LabelProfile" = "LabelAttachment",
53
 * })
54
 * @ORM\EntityListeners({"App\EntityListeners\AttachmentDeleteListener"})
55
 */
56
abstract class Attachment extends AbstractNamedDBElement
57
{
58
    /**
59
     * A list of file extensions, that browsers can show directly as image.
60
     * Based on: https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types
61
     * It will be used to determine if a attachment is a picture and therefore will be shown to user as preview.
62
     */
63
    public const PICTURE_EXTS = ['apng', 'bmp', 'gif', 'ico', 'cur', 'jpg', 'jpeg', 'jfif', 'pjpeg', 'pjp', 'png',
64
        'svg', 'webp', ];
65
66
    /**
67
     * A list of extensions that will be treated as a 3D Model that can be shown to user directly in Part-DB.
68
     */
69
    public const MODEL_EXTS = ['x3d'];
70
71
    /**
72
     * When the path begins with one of this placeholders.
73
     */
74
    public const INTERNAL_PLACEHOLDER = ['%BASE%', '%MEDIA%', '%SECURE%'];
75
76
    /**
77
     * @var array placeholders for attachments which using built in files
78
     */
79
    public const BUILTIN_PLACEHOLDER = ['%FOOTPRINTS%', '%FOOTPRINTS3D%'];
80
81
    /**
82
     * @var string The class of the element that can be passed to this attachment. Must be overridden in subclasses.
83
     */
84
    public const ALLOWED_ELEMENT_CLASS = '';
85
86
    /**
87
     * @var string|null the original filename the file had, when the user uploaded it
88
     * @ORM\Column(type="string", nullable=true)
89
     */
90
    protected ?string $original_filename = null;
91
92
    /**
93
     * @var string The path to the file relative to a placeholder path like %MEDIA%
94
     * @ORM\Column(type="string", name="path")
95
     */
96
    protected string $path = '';
97
98
    /**
99
     * ORM mapping is done in sub classes (like PartAttachment).
100
     */
101
    protected ?AttachmentContainingDBElement $element = null;
102
103
    /**
104
     * @var bool
105
     * @ORM\Column(type="boolean")
106
     */
107
    protected bool $show_in_table = false;
108
109
    /**
110
     * @var AttachmentType
111
     * @ORM\ManyToOne(targetEntity="AttachmentType", inversedBy="attachments_with_type")
112
     * @ORM\JoinColumn(name="type_id", referencedColumnName="id", nullable=false)
113
     * @Selectable()
114
     * @Assert\NotNull(message="validator.attachment.must_not_be_null")
115
     */
116
    protected ?AttachmentType $attachment_type = null;
117
118
    public function __construct()
119
    {
120
        //parent::__construct();
121
        if ('' === static::ALLOWED_ELEMENT_CLASS) {
0 ignored issues
show
introduced by
The condition '' === static::ALLOWED_ELEMENT_CLASS is always true.
Loading history...
122
            throw new LogicException('An *Attachment class must override the ALLOWED_ELEMENT_CLASS const!');
123
        }
124
    }
125
126
    public function updateTimestamps(): void
127
    {
128
        parent::updateTimestamps();
129
        if ($this->element instanceof AttachmentContainingDBElement) {
130
            $this->element->updateTimestamps();
131
        }
132
    }
133
134
    /***********************************************************
135
     * Various function
136
     ***********************************************************/
137
138
    /**
139
     * Check if this attachment is a picture (analyse the file's extension).
140
     * If the link is external, it is assumed that this is true.
141
     *
142
     * @return bool * true if the file extension is a picture extension
143
     *              * otherwise false
144
     */
145
    public function isPicture(): bool
146
    {
147
        //We can not check if a external link is a picture, so just assume this is false
148
        if ($this->isExternal()) {
149
            return true;
150
        }
151
152
        $extension = pathinfo($this->getPath(), PATHINFO_EXTENSION);
153
154
        return in_array(strtolower($extension), static::PICTURE_EXTS, true);
0 ignored issues
show
Bug introduced by
It seems like $extension can also be of type array; however, parameter $string of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

154
        return in_array(strtolower(/** @scrutinizer ignore-type */ $extension), static::PICTURE_EXTS, true);
Loading history...
155
    }
156
157
    /**
158
     * Check if this attachment is a 3D model and therefore can be directly shown to user.
159
     * If the attachment is external, false is returned (3D Models must be internal).
160
     */
161
    public function is3DModel(): bool
162
    {
163
        //We just assume that 3D Models are internally saved, otherwise we get problems loading them.
164
        if ($this->isExternal()) {
165
            return false;
166
        }
167
168
        $extension = pathinfo($this->getPath(), PATHINFO_EXTENSION);
169
170
        return in_array(strtolower($extension), static::MODEL_EXTS, true);
0 ignored issues
show
Bug introduced by
It seems like $extension can also be of type array; however, parameter $string of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

170
        return in_array(strtolower(/** @scrutinizer ignore-type */ $extension), static::MODEL_EXTS, true);
Loading history...
171
    }
172
173
    /**
174
     * Checks if the attachment file is externally saved (the database saves an URL).
175
     *
176
     * @return bool true, if the file is saved externally
177
     */
178
    public function isExternal(): bool
179
    {
180
        //When path is empty, this attachment can not be external
181
        if (empty($this->path)) {
182
            return false;
183
        }
184
185
        //After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode
186
        $tmp = explode('/', $this->path);
187
188
        return !in_array($tmp[0], array_merge(static::INTERNAL_PLACEHOLDER, static::BUILTIN_PLACEHOLDER), true);
189
    }
190
191
    /**
192
     * Check if this attachment is saved in a secure place.
193
     * This means that it can not be accessed directly via a web request, but must be viewed via a controller.
194
     *
195
     * @return bool true, if the file is secure
196
     */
197
    public function isSecure(): bool
198
    {
199
        //After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode
200
        $tmp = explode('/', $this->path);
201
202
        return '%SECURE%' === $tmp[0];
203
    }
204
205
    /**
206
     * Checks if the attachment file is using a builtin file. (see BUILTIN_PLACEHOLDERS const for possible placeholders)
207
     * If a file is built in, the path is shown to user in url field (no sensitive infos are provided).
208
     *
209
     * @return bool true if the attachment is using an builtin file
210
     */
211
    public function isBuiltIn(): bool
212
    {
213
        return static::checkIfBuiltin($this->path);
214
    }
215
216
    /********************************************************************************
217
     *
218
     *   Getters
219
     *
220
     *********************************************************************************/
221
222
    /**
223
     * Returns the extension of the file referenced via the attachment.
224
     * For a path like %BASE/path/foo.bar, bar will be returned.
225
     * If this attachment is external null is returned.
226
     *
227
     * @return string|null the file extension in lower case
228
     */
229
    public function getExtension(): ?string
230
    {
231
        if ($this->isExternal()) {
232
            return null;
233
        }
234
235
        if (!empty($this->original_filename)) {
236
            return strtolower(pathinfo($this->original_filename, PATHINFO_EXTENSION));
0 ignored issues
show
Bug introduced by
It seems like pathinfo($this->original...nts\PATHINFO_EXTENSION) can also be of type array; however, parameter $string of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

236
            return strtolower(/** @scrutinizer ignore-type */ pathinfo($this->original_filename, PATHINFO_EXTENSION));
Loading history...
237
        }
238
239
        return strtolower(pathinfo($this->getPath(), PATHINFO_EXTENSION));
240
    }
241
242
    /**
243
     * Get the element, associated with this Attachment (for example a "Part" object).
244
     *
245
     * @return AttachmentContainingDBElement the associated Element
246
     */
247
    public function getElement(): ?AttachmentContainingDBElement
248
    {
249
        return $this->element;
250
    }
251
252
    /**
253
     * The URL to the external file, or the path to the built in file.
254
     * Returns null, if the file is not external (and not builtin).
255
     */
256
    public function getURL(): ?string
257
    {
258
        if (!$this->isExternal() && !$this->isBuiltIn()) {
259
            return null;
260
        }
261
262
        return $this->path;
263
    }
264
265
    /**
266
     * Returns the hostname where the external file is stored.
267
     * Returns null, if the file is not external.
268
     */
269
    public function getHost(): ?string
270
    {
271
        if (!$this->isExternal()) {
272
            return null;
273
        }
274
275
        return parse_url($this->getURL(), PHP_URL_HOST);
276
    }
277
278
    /**
279
     * Get the filepath, relative to %BASE%.
280
     *
281
     * @return string A string like %BASE/path/foo.bar
282
     */
283
    public function getPath(): string
284
    {
285
        return $this->path;
286
    }
287
288
    /**
289
     * Returns the filename of the attachment.
290
     * For a path like %BASE/path/foo.bar, foo.bar will be returned.
291
     *
292
     * If the path is a URL (can be checked via isExternal()), null will be returned.
293
     */
294
    public function getFilename(): ?string
295
    {
296
        if ($this->isExternal()) {
297
            return null;
298
        }
299
300
        //If we have a stored original filename, then use it
301
        if (!empty($this->original_filename)) {
302
            return $this->original_filename;
303
        }
304
305
        return pathinfo($this->getPath(), PATHINFO_BASENAME);
0 ignored issues
show
Bug Best Practice introduced by
The expression return pathinfo($this->g...ents\PATHINFO_BASENAME) could return the type array which is incompatible with the type-hinted return null|string. Consider adding an additional type-check to rule them out.
Loading history...
306
    }
307
308
    /**
309
     * Sets the filename that is shown for this attachment. Useful when the internal path is some generated value.
310
     *
311
     * @param string|null $new_filename The filename that should be shown.
312
     *                                  Set to null to generate the filename from path.
313
     *
314
     * @return Attachment
315
     */
316
    public function setFilename(?string $new_filename): self
317
    {
318
        if ('' === $new_filename) {
319
            $new_filename = null;
320
        }
321
        $this->original_filename = $new_filename;
322
323
        return $this;
324
    }
325
326
    /**
327
     * Get the show_in_table attribute.
328
     *
329
     * @return bool true means, this attachment will be listed in the "Attachments" column of the HTML tables
330
     *              false means, this attachment won't be listed in the "Attachments" column of the HTML tables
331
     */
332
    public function getShowInTable(): bool
333
    {
334
        return $this->show_in_table;
335
    }
336
337
    /**
338
     *  Get the type of this attachement.
339
     *
340
     * @return AttachmentType the type of this attachement
341
     */
342
    public function getAttachmentType(): ?AttachmentType
343
    {
344
        return $this->attachment_type;
345
    }
346
347
    /*****************************************************************************************************
348
     * Setters
349
     ***************************************************************************************************
350
     * @param  bool  $show_in_table
351
     * @return Attachment
352
     */
353
354
    public function setShowInTable(bool $show_in_table): self
355
    {
356
        $this->show_in_table = $show_in_table;
357
358
        return $this;
359
    }
360
361
    /**
362
     * Sets the element that is associated with this attachment.
363
     *
364
     * @return $this
365
     */
366
    public function setElement(AttachmentContainingDBElement $element): self
367
    {
368
        if (!is_a($element, static::ALLOWED_ELEMENT_CLASS)) {
369
            throw new InvalidArgumentException(sprintf('The element associated with a %s must be a %s!', static::class, static::ALLOWED_ELEMENT_CLASS));
370
        }
371
372
        $this->element = $element;
373
374
        return $this;
375
    }
376
377
    /**
378
     * Sets the filepath (with relative placeholder) for this attachment.
379
     *
380
     * @param string $path the new filepath of the attachment
381
     *
382
     * @return Attachment
383
     */
384
    public function setPath(string $path): self
385
    {
386
        $this->path = $path;
387
388
        return $this;
389
    }
390
391
    /**
392
     * @return $this
393
     */
394
    public function setAttachmentType(AttachmentType $attachement_type): self
395
    {
396
        $this->attachment_type = $attachement_type;
397
398
        return $this;
399
    }
400
401
    /**
402
     * Sets the url associated with this attachment.
403
     * If the url is empty nothing is changed, to not override the file path.
404
     *
405
     * @return Attachment
406
     */
407
    public function setURL(?string $url): self
408
    {
409
        //Only set if the URL is not empty
410
        if (!empty($url)) {
411
            if (false !== strpos($url, '%BASE%') || false !== strpos($url, '%MEDIA%')) {
412
                throw new InvalidArgumentException('You can not reference internal files via the url field! But nice try!');
413
            }
414
415
            $this->path = $url;
416
            //Reset internal filename
417
            $this->original_filename = null;
418
        }
419
420
        return $this;
421
    }
422
423
    /*****************************************************************************************************
424
     * Static functions
425
     *****************************************************************************************************/
426
427
    /**
428
     * Checks if the given path is a path to a builtin resource.
429
     *
430
     * @param string $path The path that should be checked
431
     *
432
     * @return bool true if the path is pointing to a builtin resource
433
     */
434
    public static function checkIfBuiltin(string $path): bool
435
    {
436
        //After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode
437
        $tmp = explode('/', $path);
438
        //Builtins must have a %PLACEHOLDER% construction
439
440
        return in_array($tmp[0], static::BUILTIN_PLACEHOLDER, false);
441
    }
442
443
    /**
444
     * Check if a string is a URL and is valid.
445
     *
446
     * @param string $string        The string which should be checked
447
     * @param bool   $path_required If true, the string must contain a path to be valid. (e.g. foo.bar would be invalid, foo.bar/test.php would be valid).
448
     * @param bool   $only_http     Set this to true, if only HTTPS or HTTP schemata should be allowed.
449
     *                              *Caution: When this is set to false, a attacker could use the file:// schema, to get internal server files, like /etc/passwd.*
450
     *
451
     * @return bool True if the string is a valid URL. False, if the string is not an URL or invalid.
452
     */
453
    public static function isValidURL(string $string, bool $path_required = true, bool $only_http = true): bool
454
    {
455
        if ($only_http) {   //Check if scheme is HTTPS or HTTP
456
            $scheme = parse_url($string, PHP_URL_SCHEME);
457
            if ('http' !== $scheme && 'https' !== $scheme) {
458
                return false;   //All other schemes are not valid.
459
            }
460
        }
461
        if ($path_required) {
462
            return (bool) filter_var($string, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED);
463
        }
464
465
        return (bool) filter_var($string, FILTER_VALIDATE_URL);
466
    }
467
}
468