Passed
Branch master (350f1b)
by Jan
04:53
created

Attachment::getIDString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
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
    /***********************************************************
120
     * Various function
121
     ***********************************************************/
122
123
    /**
124
     * Check if this attachment is a picture (analyse the file's extension).
125
     * If the link is external, it is assumed that this is true.
126
     *
127
     * @return bool * true if the file extension is a picture extension
128
     *              * otherwise false
129
     */
130
    public function isPicture(): bool
131
    {
132
        //We can not check if a external link is a picture, so just assume this is false
133
        if ($this->isExternal()) {
134
            return true;
135
        }
136
137
        $extension = pathinfo($this->getPath(), PATHINFO_EXTENSION);
138
139
        return in_array(strtolower($extension), static::PICTURE_EXTS, true);
140
    }
141
142
    /**
143
     * Check if this attachment is a 3D model and therefore can be directly shown to user.
144
     * If the attachment is external, false is returned (3D Models must be internal).
145
     */
146
    public function is3DModel(): bool
147
    {
148
        //We just assume that 3D Models are internally saved, otherwise we get problems loading them.
149
        if ($this->isExternal()) {
150
            return false;
151
        }
152
153
        $extension = pathinfo($this->getPath(), PATHINFO_EXTENSION);
154
155
        return in_array(strtolower($extension), static::MODEL_EXTS, true);
156
    }
157
158
    /**
159
     * Checks if the attachment file is externally saved (the database saves an URL).
160
     *
161
     * @return bool true, if the file is saved externally
162
     */
163
    public function isExternal(): bool
164
    {
165
        //When path is empty, this attachment can not be external
166
        if (empty($this->path)) {
167
            return false;
168
        }
169
170
        //After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode
171
        $tmp = explode('/', $this->path);
172
173
        if (empty($tmp)) {
174
            return true;
175
        }
176
177
        return ! in_array($tmp[0], array_merge(static::INTERNAL_PLACEHOLDER, static::BUILTIN_PLACEHOLDER), false);
178
    }
179
180
    /**
181
     * Check if this attachment is saved in a secure place.
182
     * This means that it can not be accessed directly via a web request, but must be viewed via a controller.
183
     *
184
     * @return bool true, if the file is secure
185
     */
186
    public function isSecure(): bool
187
    {
188
        //After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode
189
        $tmp = explode('/', $this->path);
190
191
        if (empty($tmp)) {
192
            return false;
193
        }
194
195
        return '%SECURE%' === $tmp[0];
196
    }
197
198
    /**
199
     * Checks if the attachment file is using a builtin file. (see BUILTIN_PLACEHOLDERS const for possible placeholders)
200
     * If a file is built in, the path is shown to user in url field (no sensitive infos are provided).
201
     *
202
     * @return bool true if the attachment is using an builtin file
203
     */
204
    public function isBuiltIn(): bool
205
    {
206
        return static::checkIfBuiltin($this->path);
207
    }
208
209
    /********************************************************************************
210
     *
211
     *   Getters
212
     *
213
     *********************************************************************************/
214
215
    /**
216
     * Returns the extension of the file referenced via the attachment.
217
     * For a path like %BASE/path/foo.bar, bar will be returned.
218
     * If this attachment is external null is returned.
219
     *
220
     * @return string|null the file extension in lower case
221
     */
222
    public function getExtension(): ?string
223
    {
224
        if ($this->isExternal()) {
225
            return null;
226
        }
227
228
        if (! empty($this->original_filename)) {
229
            return strtolower(pathinfo($this->original_filename, PATHINFO_EXTENSION));
230
        }
231
232
        return strtolower(pathinfo($this->getPath(), PATHINFO_EXTENSION));
233
    }
234
235
    /**
236
     * Get the element, associated with this Attachment (for example a "Part" object).
237
     *
238
     * @return AttachmentContainingDBElement the associated Element
239
     */
240
    public function getElement(): ?AttachmentContainingDBElement
241
    {
242
        return $this->element;
243
    }
244
245
    /**
246
     * The URL to the external file, or the path to the built in file.
247
     * Returns null, if the file is not external (and not builtin).
248
     */
249
    public function getURL(): ?string
250
    {
251
        if (! $this->isExternal() && ! $this->isBuiltIn()) {
252
            return null;
253
        }
254
255
        return $this->path;
256
    }
257
258
    /**
259
     * Returns the hostname where the external file is stored.
260
     * Returns null, if the file is not external.
261
     */
262
    public function getHost(): ?string
263
    {
264
        if (! $this->isExternal()) {
265
            return null;
266
        }
267
268
        return parse_url($this->getURL(), PHP_URL_HOST);
269
    }
270
271
    /**
272
     * Get the filepath, relative to %BASE%.
273
     *
274
     * @return string A string like %BASE/path/foo.bar
275
     */
276
    public function getPath(): string
277
    {
278
        return $this->path;
279
    }
280
281
    /**
282
     * Returns the filename of the attachment.
283
     * For a path like %BASE/path/foo.bar, foo.bar will be returned.
284
     *
285
     * If the path is a URL (can be checked via isExternal()), null will be returned.
286
     */
287
    public function getFilename(): ?string
288
    {
289
        if ($this->isExternal()) {
290
            return null;
291
        }
292
293
        //If we have a stored original filename, then use it
294
        if (! empty($this->original_filename)) {
295
            return $this->original_filename;
296
        }
297
298
        return pathinfo($this->getPath(), PATHINFO_BASENAME);
299
    }
300
301
    /**
302
     * Sets the filename that is shown for this attachment. Useful when the internal path is some generated value.
303
     *
304
     * @param string|null $new_filename The filename that should be shown.
305
     *                                  Set to null to generate the filename from path.
306
     *
307
     * @return Attachment
308
     */
309
    public function setFilename(?string $new_filename): self
310
    {
311
        if ('' === $new_filename) {
312
            $new_filename = null;
313
        }
314
        $this->original_filename = $new_filename;
315
316
        return $this;
317
    }
318
319
    /**
320
     * Get the show_in_table attribute.
321
     *
322
     * @return bool true means, this attachment will be listed in the "Attachments" column of the HTML tables
323
     *              false means, this attachment won't be listed in the "Attachments" column of the HTML tables
324
     */
325
    public function getShowInTable(): bool
326
    {
327
        return (bool) $this->show_in_table;
328
    }
329
330
    /**
331
     *  Get the type of this attachement.
332
     *
333
     * @return AttachmentType the type of this attachement
334
     */
335
    public function getAttachmentType(): ?AttachmentType
336
    {
337
        return $this->attachment_type;
338
    }
339
340
341
    /*****************************************************************************************************
342
     * Setters
343
     ***************************************************************************************************
344
     * @param  bool  $show_in_table
345
     * @return Attachment
346
     */
347
348
    public function setShowInTable(bool $show_in_table): self
349
    {
350
        $this->show_in_table = $show_in_table;
351
352
        return $this;
353
    }
354
355
    /**
356
     * Sets the element that is associated with this attachment.
357
     *
358
     * @return $this
359
     */
360
    public function setElement(AttachmentContainingDBElement $element): self
361
    {
362
        if (! is_a($element, static::ALLOWED_ELEMENT_CLASS)) {
363
            throw new InvalidArgumentException(sprintf('The element associated with a %s must be a %s!', static::class, static::ALLOWED_ELEMENT_CLASS));
364
        }
365
366
        $this->element = $element;
367
368
        return $this;
369
    }
370
371
    /**
372
     * Sets the filepath (with relative placeholder) for this attachment.
373
     *
374
     * @param string $path the new filepath of the attachment
375
     *
376
     * @return Attachment
377
     */
378
    public function setPath(string $path): self
379
    {
380
        $this->path = $path;
381
382
        return $this;
383
    }
384
385
    /**
386
     * @return $this
387
     */
388
    public function setAttachmentType(AttachmentType $attachement_type): self
389
    {
390
        $this->attachment_type = $attachement_type;
391
392
        return $this;
393
    }
394
395
    /**
396
     * Sets the url associated with this attachment.
397
     * If the url is empty nothing is changed, to not override the file path.
398
     *
399
     * @return Attachment
400
     */
401
    public function setURL(?string $url): self
402
    {
403
        //Only set if the URL is not empty
404
        if (! empty($url)) {
405
            if (false !== strpos($url, '%BASE%') || false !== strpos($url, '%MEDIA%')) {
406
                throw new InvalidArgumentException('You can not reference internal files via the url field! But nice try!');
407
            }
408
409
            $this->path = $url;
410
            //Reset internal filename
411
            $this->original_filename = null;
412
        }
413
414
        return $this;
415
    }
416
417
    /*****************************************************************************************************
418
     * Static functions
419
     *****************************************************************************************************/
420
421
    /**
422
     * Checks if the given path is a path to a builtin resource.
423
     *
424
     * @param string $path The path that should be checked
425
     *
426
     * @return bool true if the path is pointing to a builtin resource
427
     */
428
    public static function checkIfBuiltin(string $path): bool
429
    {
430
        //After the %PLACEHOLDER% comes a slash, so we can check if we have a placeholder via explode
431
        $tmp = explode('/', $path);
432
        //Builtins must have a %PLACEHOLDER% construction
433
        if (empty($tmp)) {
434
            return false;
435
        }
436
437
        return in_array($tmp[0], static::BUILTIN_PLACEHOLDER, false);
438
    }
439
440
    /**
441
     * Check if a string is a URL and is valid.
442
     *
443
     * @param string $string        The string which should be checked
444
     * @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).
445
     * @param bool   $only_http     Set this to true, if only HTTPS or HTTP schemata should be allowed.
446
     *                              *Caution: When this is set to false, a attacker could use the file:// schema, to get internal server files, like /etc/passwd.*
447
     *
448
     * @return bool True if the string is a valid URL. False, if the string is not an URL or invalid.
449
     */
450
    public static function isURL(string $string, bool $path_required = true, bool $only_http = true): bool
451
    {
452
        if ($only_http) {   //Check if scheme is HTTPS or HTTP
453
            $scheme = parse_url($string, PHP_URL_SCHEME);
454
            if ('http' !== $scheme && 'https' !== $scheme) {
455
                return false;   //All other schemes are not valid.
456
            }
457
        }
458
        if ($path_required) {
459
            return (bool) filter_var($string, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED);
460
        }
461
462
        return (bool) filter_var($string, FILTER_VALIDATE_URL);
463
    }
464
}
465