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