Completed
Push — master ( ba4bf4...97cb91 )
by Jan
05:00
created

Attachment::getExtension()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 0
dl 0
loc 11
rs 10
c 0
b 0
f 0
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
    const PICTURE_EXTS = ['apng', 'bmp', 'gif', 'ico', 'cur', 'jpg', 'jpeg', 'jfif', 'pjpeg', 'pjp', 'png',
57
                            'svg', 'webp'];
58
59
    /**
60
     * When the path begins with one of this placeholders
61
     */
62
    const INTERNAL_PLACEHOLDER = ['%BASE%', '%MEDIA%', '%FOOTPRINTS%', '%FOOTPRINTS3D%'];
63
64
    /**
65
     * @var bool
66
     * @ORM\Column(type="boolean")
67
     */
68
    protected $show_in_table = false;
69
70
    /**
71
     * @var string The path to the file relative to a placeholder path like %MEDIA%
72
     * @ORM\Column(type="string", name="path")
73
     */
74
    protected $path = '';
75
76
    /**
77
     * @var string The original filenamethe file had, when the user uploaded it.
78
     * @ORM\Column(type="string", nullable=true)
79
     */
80
    protected $original_filename;
81
82
    /**
83
     * ORM mapping is done in sub classes (like PartAttachment)
84
     */
85
    protected $element;
86
87
    /**
88
     * @var AttachmentType
89
     * @ORM\ManyToOne(targetEntity="AttachmentType", inversedBy="attachments_with_type")
90
     * @ORM\JoinColumn(name="type_id", referencedColumnName="id")
91
     * @Selectable()
92
     */
93
    protected $attachment_type;
94
95
    /***********************************************************
96
     * Various function
97
     ***********************************************************/
98
99
    /**
100
     * Check if this attachement is a picture (analyse the file's extension).
101
     * If the link is external, it is assumed that this is false.
102
     *
103
     * @return bool * true if the file extension is a picture extension
104
     *              * otherwise false
105
     */
106
    public function isPicture(): bool
107
    {
108
        //We can not check if a external link is a picture, so just assume this is false
109
        if ($this->isExternal()) {
110
            return true;
111
        }
112
113
        $extension = pathinfo($this->getPath(), PATHINFO_EXTENSION);
114
115
        return in_array(strtolower($extension), static::PICTURE_EXTS, true);
116
    }
117
118
    /**
119
     * Checks if the attachment file is externally saved (the database saves an URL)
120
     * @return bool true, if the file is saved externally
121
     */
122
    public function isExternal() : bool
123
    {
124
        //After the %PLACEHOLDER% comes a slash, so we can check if we have a placholder via explode
125
        $tmp = explode("/", $this->path);
126
127
        if (empty($tmp)) {
128
            return true;
129
        }
130
131
        return !in_array($tmp[0], static::INTERNAL_PLACEHOLDER, false);
132
    }
133
134
    /********************************************************************************
135
     *
136
     *   Getters
137
     *
138
     *********************************************************************************/
139
140
    /**
141
     * Returns the extension of the file referenced via the attachment.
142
     * For a path like %BASE/path/foo.bar, bar will be returned.
143
     * If this attachment is external null is returned.
144
     * @return string|null The file extension in lower case.
145
     */
146
    public function getExtension() : ?string
147
    {
148
        if ($this->isExternal()) {
149
            return null;
150
        }
151
152
        if (!empty($this->original_filename)) {
153
            return strtolower(pathinfo($this->original_filename, PATHINFO_EXTENSION));
154
        }
155
156
        return strtolower(pathinfo($this->getPath(), PATHINFO_EXTENSION));
157
    }
158
159
    /**
160
     * Get the element, associated with this Attachement (for example a "Part" object).
161
     *
162
     * @return AttachmentContainingDBElement The associated Element.
163
     */
164
    public function getElement(): ?AttachmentContainingDBElement
165
    {
166
        return $this->element;
167
    }
168
169
    /**
170
     * The URL to the external file.
171
     * Returns null, if the file is not external.
172
     * @return string|null
173
     */
174
    public function getURL(): ?string
175
    {
176
        if (!$this->isExternal()) {
177
            return null;
178
        }
179
180
        return $this->path;
181
    }
182
183
    /**
184
     * Returns the hostname where the external file is stored.
185
     * Returns null, if the file is not external.
186
     * @return string|null
187
     */
188
    public function getHost(): ?string
189
    {
190
        if (!$this->isExternal()) {
191
            return null;
192
        }
193
194
        return parse_url($this->getURL(), PHP_URL_HOST);
195
    }
196
197
    /**
198
     * Get the filepath, relative to %BASE%.
199
     *
200
     * @return string A string like %BASE/path/foo.bar
201
     */
202
    public function getPath(): string
203
    {
204
        return $this->path;
205
    }
206
207
208
209
    /**
210
     * Returns the filename of the attachment.
211
     * For a path like %BASE/path/foo.bar, foo.bar will be returned.
212
     *
213
     * If the path is a URL (can be checked via isExternal()), null will be returned.
214
     *
215
     * @return string|null
216
     */
217
    public function getFilename(): ?string
218
    {
219
        if ($this->isExternal()) {
220
            return null;
221
        }
222
223
        //If we have a stored original filename, then use it
224
        if (!empty($this->original_filename)) {
225
            return $this->original_filename;
226
        }
227
228
        return pathinfo($this->getPath(), PATHINFO_BASENAME);
229
    }
230
231
    /**
232
     * Sets the filename that is shown for this attachment. Useful when the internal path is some generated value.
233
     * @param string|null $new_filename The filename that should be shown.
234
     * Set to null to generate the filename from path.
235
     * @return Attachment
236
     */
237
    public function setFilename(?string $new_filename): Attachment
238
    {
239
        $this->original_filename = $new_filename;
0 ignored issues
show
Bug Best Practice introduced by
In this branch, the function will implicitly return null which is incompatible with the type-hinted return App\Entity\Attachments\Attachment. Consider adding a return statement or allowing null as return value.

For hinted functions/methods where all return statements with the correct type are only reachable via conditions, ?null? gets implicitly returned which may be incompatible with the hinted type. Let?s take a look at an example:

interface ReturnsInt {
    public function returnsIntHinted(): int;
}

class MyClass implements ReturnsInt {
    public function returnsIntHinted(): int
    {
        if (foo()) {
            return 123;
        }
        // here: null is implicitly returned
    }
}
Loading history...
240
    }
241
242
    /**
243
     * Get the show_in_table attribute.
244
     *
245
     * @return bool true means, this attachement will be listed in the "Attachements" column of the HTML tables
246
     *              false means, this attachement won't be listed in the "Attachements" column of the HTML tables
247
     */
248
    public function getShowInTable(): bool
249
    {
250
        return (bool) $this->show_in_table;
251
    }
252
253
    /**
254
     *  Get the type of this attachement.
255
     *
256
     * @return AttachmentType the type of this attachement
257
     *
258
     */
259
    public function getAttachmentType(): ?AttachmentType
260
    {
261
        return $this->attachment_type;
262
    }
263
264
    /**
265
     * Returns the ID as an string, defined by the element class.
266
     * This should have a form like P000014, for a part with ID 14.
267
     *
268
     * @return string The ID as a string;
269
     */
270
    public function getIDString(): string
271
    {
272
        return 'A'.sprintf('%09d', $this->getID());
273
    }
274
275
    /*****************************************************************************************************
276
     * Setters
277
     ****************************************************************************************************/
278
279
    /**
280
     * @param bool $show_in_table
281
     *
282
     * @return self
283
     */
284
    public function setShowInTable(bool $show_in_table): self
285
    {
286
        $this->show_in_table = $show_in_table;
287
288
        return $this;
289
    }
290
291
    abstract public function setElement(AttachmentContainingDBElement $element) : Attachment;
292
293
    /**
294
     * @param string $path
295
     * @return Attachment
296
     */
297
    public function setPath(string $path): Attachment
298
    {
299
        $this->path = $path;
300
        return $this;
301
    }
302
303
    /**
304
     * @param AttachmentType $attachement_type
305
     * @return Attachment
306
     */
307
    public function setAttachmentType(AttachmentType $attachement_type): Attachment
308
    {
309
        $this->attachment_type = $attachement_type;
310
        return $this;
311
    }
312
313
    /**
314
     * Sets the url associated with this attachment.
315
     * If the url is empty nothing is changed, to not override the file path.
316
     * @param string|null $url
317
     * @return Attachment
318
     */
319
    public function setURL(?string $url) : Attachment
320
    {
321
        //Only set if the URL is not empty
322
        if (!empty($url)) {
323
            if (strpos($url, '%BASE%') !== false || strpos($url, '%MEDIA%') !== false) {
324
                throw new \InvalidArgumentException('You can not reference internal files via the url field! But nice try!');
325
            }
326
327
            $this->path = $url;
328
        }
329
330
        return $this;
331
    }
332
333
334
    /*****************************************************************************************************
335
     * Static functions
336
     *****************************************************************************************************/
337
338
    /**
339
     * Check if a string is a URL and is valid.
340
     * @param $string string The string which should be checked.
341
     * @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).
342
     * @param $only_http bool Set this to true, if only HTTPS or HTTP schemata should be allowed.
343
     *  *Caution: When this is set to false, a attacker could use the file:// schema, to get internal server files, like /etc/passwd.*
344
     * @return bool True if the string is a valid URL. False, if the string is not an URL or invalid.
345
     */
346
    public static function isURL(string $string, bool $path_required = true, bool $only_http = true) : bool
347
    {
348
        if ($only_http) {   //Check if scheme is HTTPS or HTTP
349
            $scheme = parse_url($string, PHP_URL_SCHEME);
350
            if ($scheme !== 'http' && $scheme !== 'https') {
351
                return false;   //All other schemes are not valid.
352
            }
353
        }
354
        if ($path_required) {
355
            return (bool) filter_var($string, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED);
356
        }
357
358
        return (bool) filter_var($string, FILTER_VALIDATE_URL);
359
    }
360
}
361