Passed
Push — master ( f0d0a7...1b0620 )
by Jan
10:32
created

Attachment::isURL()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 7
nc 5
nop 3
dl 0
loc 13
rs 9.6111
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 - 2020 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 function in_array;
29
use InvalidArgumentException;
30
use LogicException;
31
32
/**
33
 * Class Attachment.
34
 *
35
 * @ORM\Entity(repositoryClass="App\Repository\AttachmentRepository")
36
 * @ORM\Table(name="`attachments`")
37
 * @ORM\InheritanceType("SINGLE_TABLE")
38
 * @ORM\DiscriminatorColumn(name="class_name", type="string")
39
 * @ORM\DiscriminatorMap({
40
 *     "PartDB\Part" = "PartAttachment", "Part" = "PartAttachment",
41
 *     "PartDB\Device" = "DeviceAttachment", "Device" = "DeviceAttachment",
42
 *     "AttachmentType" = "AttachmentTypeAttachment", "Category" = "CategoryAttachment",
43
 *     "Footprint" = "FootprintAttachment", "Manufacturer" = "ManufacturerAttachment",
44
 *     "Currency" = "CurrencyAttachment", "Group" = "GroupAttachment",
45
 *     "MeasurementUnit" = "MeasurementUnitAttachment", "Storelocation" = "StorelocationAttachment",
46
 *     "Supplier" = "SupplierAttachment", "User" = "UserAttachment", "LabelProfile" = "LabelAttachment",
47
 * })
48
 * @ORM\EntityListeners({"App\EntityListeners\AttachmentDeleteListener"})
49
 */
50
abstract class Attachment extends AbstractNamedDBElement
51
{
52
    /**
53
     * A list of file extensions, that browsers can show directly as image.
54
     * Based on: https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types
55
     * It will be used to determine if a attachment is a picture and therefore will be shown to user as preview.
56
     */
57
    public const PICTURE_EXTS = ['apng', 'bmp', 'gif', 'ico', 'cur', 'jpg', 'jpeg', 'jfif', 'pjpeg', 'pjp', 'png',
58
        'svg', 'webp', ];
59
60
    /**
61
     * A list of extensions that will be treated as a 3D Model that can be shown to user directly in Part-DB.
62
     */
63
    public const MODEL_EXTS = ['x3d'];
64
65
    /**
66
     * When the path begins with one of this placeholders.
67
     */
68
    public const INTERNAL_PLACEHOLDER = ['%BASE%', '%MEDIA%', '%SECURE%'];
69
70
    /**
71
     * @var array Placeholders for attachments which using built in files.
72
     */
73
    public const BUILTIN_PLACEHOLDER = ['%FOOTPRINTS%', '%FOOTPRINTS3D%'];
74
75
    /**
76
     * @var string The class of the element that can be passed to this attachment. Must be overridden in subclasses.
77
     */
78
    public const ALLOWED_ELEMENT_CLASS = '';
79
80
    /**
81
     * @var string|null the original filename the file had, when the user uploaded it
82
     * @ORM\Column(type="string", nullable=true)
83
     */
84
    protected $original_filename;
85
86
    /**
87
     * @var string The path to the file relative to a placeholder path like %MEDIA%
88
     * @ORM\Column(type="string", name="path")
89
     */
90
    protected $path = '';
91
92
    /**
93
     * ORM mapping is done in sub classes (like PartAttachment).
94
     */
95
    protected $element;
96
97
    /**
98
     * @var bool
99
     * @ORM\Column(type="boolean")
100
     */
101
    protected $show_in_table = false;
102
103
    /**
104
     * @var AttachmentType
105
     * @ORM\ManyToOne(targetEntity="AttachmentType", inversedBy="attachments_with_type")
106
     * @ORM\JoinColumn(name="type_id", referencedColumnName="id")
107
     * @Selectable()
108
     */
109
    protected $attachment_type;
110
111
    public function __construct()
112
    {
113
        //parent::__construct();
114
        if ('' === static::ALLOWED_ELEMENT_CLASS) {
0 ignored issues
show
introduced by
The condition '' === static::ALLOWED_ELEMENT_CLASS is always true.
Loading history...
115
            throw new LogicException('An *Attachment class must override the ALLOWED_ELEMENT_CLASS const!');
116
        }
117
    }
118
119
    public function updateTimestamps(): void
120
    {
121
        parent::updateTimestamps();
122
        if ($this->element instanceof AttachmentContainingDBElement) {
123
            $this->element->updateTimestamps();
124
        }
125
    }
126
127
    /***********************************************************
128
     * Various function
129
     ***********************************************************/
130
131
    /**
132
     * Check if this attachment is a picture (analyse the file's extension).
133
     * If the link is external, it is assumed that this is true.
134
     *
135
     * @return bool * true if the file extension is a picture extension
136
     *              * otherwise false
137
     */
138
    public function isPicture(): bool
139
    {
140
        //We can not check if a external link is a picture, so just assume this is false
141
        if ($this->isExternal()) {
142
            return true;
143
        }
144
145
        $extension = pathinfo($this->getPath(), PATHINFO_EXTENSION);
146
147
        return in_array(strtolower($extension), static::PICTURE_EXTS, true);
148
    }
149
150
    /**
151
     * Check if this attachment is a 3D model and therefore can be directly shown to user.
152
     * If the attachment is external, false is returned (3D Models must be internal).
153
     */
154
    public function is3DModel(): bool
155
    {
156
        //We just assume that 3D Models are internally saved, otherwise we get problems loading them.
157
        if ($this->isExternal()) {
158
            return false;
159
        }
160
161
        $extension = pathinfo($this->getPath(), PATHINFO_EXTENSION);
162
163
        return in_array(strtolower($extension), static::MODEL_EXTS, true);
164
    }
165
166
    /**
167
     * Checks if the attachment file is externally saved (the database saves an URL).
168
     *
169
     * @return bool true, if the file is saved externally
170
     */
171
    public function isExternal(): bool
172
    {
173
        //When path is empty, this attachment can not be external
174
        if (empty($this->path)) {
175
            return false;
176
        }
177
178
        //After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode
179
        $tmp = explode('/', $this->path);
180
181
        if (empty($tmp)) {
182
            return true;
183
        }
184
185
        return ! in_array($tmp[0], array_merge(static::INTERNAL_PLACEHOLDER, static::BUILTIN_PLACEHOLDER), false);
186
    }
187
188
    /**
189
     * Check if this attachment is saved in a secure place.
190
     * This means that it can not be accessed directly via a web request, but must be viewed via a controller.
191
     *
192
     * @return bool true, if the file is secure
193
     */
194
    public function isSecure(): bool
195
    {
196
        //After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode
197
        $tmp = explode('/', $this->path);
198
199
        if (empty($tmp)) {
200
            return false;
201
        }
202
203
        return '%SECURE%' === $tmp[0];
204
    }
205
206
    /**
207
     * Checks if the attachment file is using a builtin file. (see BUILTIN_PLACEHOLDERS const for possible placeholders)
208
     * If a file is built in, the path is shown to user in url field (no sensitive infos are provided).
209
     *
210
     * @return bool true if the attachment is using an builtin file
211
     */
212
    public function isBuiltIn(): bool
213
    {
214
        return static::checkIfBuiltin($this->path);
215
    }
216
217
    /********************************************************************************
218
     *
219
     *   Getters
220
     *
221
     *********************************************************************************/
222
223
    /**
224
     * Returns the extension of the file referenced via the attachment.
225
     * For a path like %BASE/path/foo.bar, bar will be returned.
226
     * If this attachment is external null is returned.
227
     *
228
     * @return string|null the file extension in lower case
229
     */
230
    public function getExtension(): ?string
231
    {
232
        if ($this->isExternal()) {
233
            return null;
234
        }
235
236
        if (! empty($this->original_filename)) {
237
            return strtolower(pathinfo($this->original_filename, PATHINFO_EXTENSION));
238
        }
239
240
        return strtolower(pathinfo($this->getPath(), PATHINFO_EXTENSION));
241
    }
242
243
    /**
244
     * Get the element, associated with this Attachment (for example a "Part" object).
245
     *
246
     * @return AttachmentContainingDBElement the associated Element
247
     */
248
    public function getElement(): ?AttachmentContainingDBElement
249
    {
250
        return $this->element;
251
    }
252
253
    /**
254
     * The URL to the external file, or the path to the built in file.
255
     * Returns null, if the file is not external (and not builtin).
256
     */
257
    public function getURL(): ?string
258
    {
259
        if (! $this->isExternal() && ! $this->isBuiltIn()) {
260
            return null;
261
        }
262
263
        return $this->path;
264
    }
265
266
    /**
267
     * Returns the hostname where the external file is stored.
268
     * Returns null, if the file is not external.
269
     */
270
    public function getHost(): ?string
271
    {
272
        if (! $this->isExternal()) {
273
            return null;
274
        }
275
276
        return parse_url($this->getURL(), PHP_URL_HOST);
277
    }
278
279
    /**
280
     * Get the filepath, relative to %BASE%.
281
     *
282
     * @return string A string like %BASE/path/foo.bar
283
     */
284
    public function getPath(): string
285
    {
286
        return $this->path;
287
    }
288
289
    /**
290
     * Returns the filename of the attachment.
291
     * For a path like %BASE/path/foo.bar, foo.bar will be returned.
292
     *
293
     * If the path is a URL (can be checked via isExternal()), null will be returned.
294
     */
295
    public function getFilename(): ?string
296
    {
297
        if ($this->isExternal()) {
298
            return null;
299
        }
300
301
        //If we have a stored original filename, then use it
302
        if (! empty($this->original_filename)) {
303
            return $this->original_filename;
304
        }
305
306
        return pathinfo($this->getPath(), PATHINFO_BASENAME);
307
    }
308
309
    /**
310
     * Sets the filename that is shown for this attachment. Useful when the internal path is some generated value.
311
     *
312
     * @param string|null $new_filename The filename that should be shown.
313
     *                                  Set to null to generate the filename from path.
314
     *
315
     * @return Attachment
316
     */
317
    public function setFilename(?string $new_filename): self
318
    {
319
        if ('' === $new_filename) {
320
            $new_filename = null;
321
        }
322
        $this->original_filename = $new_filename;
323
324
        return $this;
325
    }
326
327
    /**
328
     * Get the show_in_table attribute.
329
     *
330
     * @return bool true means, this attachment will be listed in the "Attachments" column of the HTML tables
331
     *              false means, this attachment won't be listed in the "Attachments" column of the HTML tables
332
     */
333
    public function getShowInTable(): bool
334
    {
335
        return (bool) $this->show_in_table;
336
    }
337
338
    /**
339
     *  Get the type of this attachement.
340
     *
341
     * @return AttachmentType the type of this attachement
342
     */
343
    public function getAttachmentType(): ?AttachmentType
344
    {
345
        return $this->attachment_type;
346
    }
347
348
349
    /*****************************************************************************************************
350
     * Setters
351
     ***************************************************************************************************
352
     * @param  bool  $show_in_table
353
     * @return Attachment
354
     */
355
356
    public function setShowInTable(bool $show_in_table): self
357
    {
358
        $this->show_in_table = $show_in_table;
359
360
        return $this;
361
    }
362
363
    /**
364
     * Sets the element that is associated with this attachment.
365
     *
366
     * @return $this
367
     */
368
    public function setElement(AttachmentContainingDBElement $element): self
369
    {
370
        if (! is_a($element, static::ALLOWED_ELEMENT_CLASS)) {
371
            throw new InvalidArgumentException(sprintf('The element associated with a %s must be a %s!', static::class, static::ALLOWED_ELEMENT_CLASS));
372
        }
373
374
        $this->element = $element;
375
376
        return $this;
377
    }
378
379
    /**
380
     * Sets the filepath (with relative placeholder) for this attachment.
381
     *
382
     * @param string $path the new filepath of the attachment
383
     *
384
     * @return Attachment
385
     */
386
    public function setPath(string $path): self
387
    {
388
        $this->path = $path;
389
390
        return $this;
391
    }
392
393
    /**
394
     * @return $this
395
     */
396
    public function setAttachmentType(AttachmentType $attachement_type): self
397
    {
398
        $this->attachment_type = $attachement_type;
399
400
        return $this;
401
    }
402
403
    /**
404
     * Sets the url associated with this attachment.
405
     * If the url is empty nothing is changed, to not override the file path.
406
     *
407
     * @return Attachment
408
     */
409
    public function setURL(?string $url): self
410
    {
411
        //Only set if the URL is not empty
412
        if (! empty($url)) {
413
            if (false !== strpos($url, '%BASE%') || false !== strpos($url, '%MEDIA%')) {
414
                throw new InvalidArgumentException('You can not reference internal files via the url field! But nice try!');
415
            }
416
417
            $this->path = $url;
418
            //Reset internal filename
419
            $this->original_filename = null;
420
        }
421
422
        return $this;
423
    }
424
425
    /*****************************************************************************************************
426
     * Static functions
427
     *****************************************************************************************************/
428
429
    /**
430
     * Checks if the given path is a path to a builtin resource.
431
     *
432
     * @param string $path The path that should be checked
433
     *
434
     * @return bool true if the path is pointing to a builtin resource
435
     */
436
    public static function checkIfBuiltin(string $path): bool
437
    {
438
        //After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode
439
        $tmp = explode('/', $path);
440
        //Builtins must have a %PLACEHOLDER% construction
441
        if (empty($tmp)) {
442
            return false;
443
        }
444
445
        return in_array($tmp[0], static::BUILTIN_PLACEHOLDER, false);
446
    }
447
448
    /**
449
     * Check if a string is a URL and is valid.
450
     *
451
     * @param string $string        The string which should be checked
452
     * @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).
453
     * @param bool   $only_http     Set this to true, if only HTTPS or HTTP schemata should be allowed.
454
     *                              *Caution: When this is set to false, a attacker could use the file:// schema, to get internal server files, like /etc/passwd.*
455
     *
456
     * @return bool True if the string is a valid URL. False, if the string is not an URL or invalid.
457
     */
458
    public static function isValidURL(string $string, bool $path_required = true, bool $only_http = true): bool
459
    {
460
        if ($only_http) {   //Check if scheme is HTTPS or HTTP
461
            $scheme = parse_url($string, PHP_URL_SCHEME);
462
            if ('http' !== $scheme && 'https' !== $scheme) {
463
                return false;   //All other schemes are not valid.
464
            }
465
        }
466
        if ($path_required) {
467
            return (bool) filter_var($string, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED);
468
        }
469
470
        return (bool) filter_var($string, FILTER_VALIDATE_URL);
471
    }
472
}
473