Completed
Push — master ( 1b2800...4fe10b )
by Jan
04:09
created

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