GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Issues (724)

app/Helpers/Attachments/AttachmentHelper.php (1 issue)

Severity
1
<?php
2
/**
3
 * AttachmentHelper.php
4
 * Copyright (c) 2019 [email protected]
5
 *
6
 * This file is part of Firefly III (https://github.com/firefly-iii).
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU Affero General Public License as
10
 * published by the Free Software Foundation, either version 3 of the
11
 * License, or (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU Affero General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU Affero General Public License
19
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
20
 */
21
declare(strict_types=1);
22
23
namespace FireflyIII\Helpers\Attachments;
24
25
use Crypt;
26
use FireflyIII\Exceptions\FireflyException;
27
use FireflyIII\Models\Attachment;
28
use FireflyIII\Models\PiggyBank;
29
use Illuminate\Contracts\Encryption\DecryptException;
30
use Illuminate\Contracts\Encryption\EncryptException;
31
use Illuminate\Contracts\Filesystem\FileNotFoundException;
32
use Illuminate\Contracts\Filesystem\Filesystem;
33
use Illuminate\Database\Eloquent\Model;
34
use Illuminate\Support\Collection;
35
use Illuminate\Support\Facades\Storage;
36
use Illuminate\Support\MessageBag;
37
use Log;
38
use Symfony\Component\HttpFoundation\File\UploadedFile;
39
40
/**
41
 * Class AttachmentHelper.
42
 */
43
class AttachmentHelper implements AttachmentHelperInterface
44
{
45
    /** @var Collection All attachments */
46
    public $attachments;
47
    /** @var MessageBag All errors */
48
    public $errors;
49
    /** @var MessageBag All messages */
50
    public $messages;
51
    /** @var array Allowed mimes */
52
    protected $allowedMimes = [];
53
    /** @var int Max upload size. */
54
    protected $maxUploadSize = 0;
55
56
    /** @var Filesystem The disk where attachments are stored. */
57
    protected $uploadDisk;
58
59
60
    /**
61
     * AttachmentHelper constructor.
62
     *
63
     * @codeCoverageIgnore
64
     */
65
    public function __construct()
66
    {
67
        $this->maxUploadSize = (int) config('firefly.maxUploadSize');
68
        $this->allowedMimes  = (array) config('firefly.allowedMimes');
69
        $this->errors        = new MessageBag;
70
        $this->messages      = new MessageBag;
71
        $this->attachments   = new Collection;
72
        $this->uploadDisk    = Storage::disk('upload');
73
74
        if ('testing' === config('app.env')) {
75
            Log::warning(sprintf('%s should not be instantiated in the TEST environment!', get_class($this)));
76
        }
77
    }
78
79
80
    /**
81
     * Returns the content of an attachment.
82
     *
83
     * @codeCoverageIgnore
84
     *
85
     * @param Attachment $attachment
86
     *
87
     * @return string
88
     */
89
    public function getAttachmentContent(Attachment $attachment): string
90
    {
91
        $encryptedData = '';
0 ignored issues
show
The assignment to $encryptedData is dead and can be removed.
Loading history...
92
        try {
93
            $encryptedData = $this->uploadDisk->get(sprintf('at-%d.data', $attachment->id));
94
        } catch (FileNotFoundException $e) {
95
            Log::error($e->getMessage());
96
        }
97
        try {
98
            $unencryptedData = Crypt::decrypt($encryptedData); // verified
99
        } catch (DecryptException|FileNotFoundException $e) {
100
            Log::error(sprintf('Could not decrypt data of attachment #%d: %s', $attachment->id, $e->getMessage()));
101
            $unencryptedData = $encryptedData;
102
        }
103
104
        return $unencryptedData;
105
    }
106
107
    /**
108
     * Returns the file path relative to upload disk for an attachment,
109
     *
110
     * @param Attachment $attachment
111
     *
112
     * @codeCoverageIgnore
113
     * @return string
114
     */
115
    public function getAttachmentLocation(Attachment $attachment): string
116
    {
117
        return sprintf('%sat-%d.data', DIRECTORY_SEPARATOR, (int) $attachment->id);
118
    }
119
120
    /**
121
     * Get all attachments.
122
     *
123
     * @codeCoverageIgnore
124
     * @return Collection
125
     */
126
    public function getAttachments(): Collection
127
    {
128
        return $this->attachments;
129
    }
130
131
    /**
132
     * Get all errors.
133
     *
134
     * @return MessageBag
135
     * @codeCoverageIgnore
136
     */
137
    public function getErrors(): MessageBag
138
    {
139
        return $this->errors;
140
    }
141
142
    /**
143
     * Get all messages.
144
     *
145
     * @return MessageBag
146
     * @codeCoverageIgnore
147
     */
148
    public function getMessages(): MessageBag
149
    {
150
        return $this->messages;
151
    }
152
153
    /**
154
     * Uploads a file as a string.
155
     *
156
     * @param Attachment $attachment
157
     * @param string     $content
158
     *
159
     * @return bool
160
     */
161
    public function saveAttachmentFromApi(Attachment $attachment, string $content): bool
162
    {
163
        $resource = tmpfile();
164
        if (false === $resource) {
165
            // @codeCoverageIgnoreStart
166
            Log::error('Cannot create temp-file for file upload.');
167
168
            return false;
169
            // @codeCoverageIgnoreEnd
170
        }
171
172
        if ('' === $content) {
173
            Log::error('Cannot upload empty file.');
174
175
            return false;
176
        }
177
178
        $path = stream_get_meta_data($resource)['uri'];
179
        fwrite($resource, $content);
180
        $finfo       = finfo_open(FILEINFO_MIME_TYPE);
181
        $mime        = finfo_file($finfo, $path);
182
        $allowedMime = config('firefly.allowedMimes');
183
        if (!in_array($mime, $allowedMime, true)) {
184
            Log::error(sprintf('Mime type %s is not allowed for API file upload.', $mime));
185
186
            return false;
187
        }
188
        // is allowed? Save the file, without encryption.
189
        $this->uploadDisk->put($attachment->fileName(), $content);
190
191
        // update attachment.
192
        $attachment->md5      = md5_file($path);
193
        $attachment->mime     = $mime;
194
        $attachment->size     = strlen($content);
195
        $attachment->uploaded = true;
196
        $attachment->save();
197
198
        return true;
199
    }
200
201
    /**
202
     * Save attachments that get uploaded with models, through the app.
203
     *
204
     * @param object     $model
205
     * @param array|null $files
206
     *
207
     * @throws FireflyException
208
     * @return bool
209
     */
210
    public function saveAttachmentsForModel(object $model, ?array $files): bool
211
    {
212
        if (!($model instanceof Model)) {
213
            return false; // @codeCoverageIgnore
214
        }
215
216
        Log::debug(sprintf('Now in saveAttachmentsForModel for model %s', get_class($model)));
217
        if (is_array($files)) {
218
            Log::debug('$files is an array.');
219
            /** @var UploadedFile $entry */
220
            foreach ($files as $entry) {
221
                if (null !== $entry) {
222
                    $this->processFile($entry, $model);
223
                }
224
            }
225
            Log::debug('Done processing uploads.');
226
        }
227
        if (!is_array($files) || (is_array($files) && 0 === count($files))) {
228
            Log::debug('Array of files is not an array. Probably nothing uploaded. Will not store attachments.');
229
        }
230
231
        return true;
232
    }
233
234
    /**
235
     * Check if a model already has this file attached.
236
     *
237
     * @param UploadedFile $file
238
     * @param Model        $model
239
     *
240
     * @return bool
241
     */
242
    protected function hasFile(UploadedFile $file, Model $model): bool
243
    {
244
        $md5   = md5_file($file->getRealPath());
245
        $name  = $file->getClientOriginalName();
246
        $class = get_class($model);
247
        $count = 0;
248
        if (PiggyBank::class === $class) {
249
            $count = $model->account->user->attachments()->where('md5', $md5)->where('attachable_id', $model->id)->where('attachable_type', $class)->count();
250
        }
251
        if (PiggyBank::class !== $class) {
252
            $count = $model->user->attachments()->where('md5', $md5)->where('attachable_id', $model->id)->where('attachable_type', $class)->count();
253
        }
254
        $result = false;
255
        if ($count > 0) {
256
            $msg = (string) trans('validation.file_already_attached', ['name' => $name]);
257
            $this->errors->add('attachments', $msg);
258
            Log::error($msg);
259
            $result = true;
260
        }
261
262
        return $result;
263
    }
264
265
    /**
266
     * Process the upload of a file.
267
     *
268
     * @param UploadedFile $file
269
     * @param Model        $model
270
     *
271
     * @throws EncryptException
272
     * @throws FireflyException
273
     * @return Attachment|null
274
     */
275
    protected function processFile(UploadedFile $file, Model $model): ?Attachment
276
    {
277
        Log::debug('Now in processFile()');
278
        $validation = $this->validateUpload($file, $model);
279
        $attachment = null;
280
        if (false !== $validation) {
281
            $class = get_class($model);
282
            $user = $model->user;
283
            if (PiggyBank::class === $class) {
284
                $user = $model->account->user;
285
            }
286
287
            $attachment = new Attachment; // create Attachment object.
288
            /** @noinspection PhpUndefinedFieldInspection */
289
            $attachment->user()->associate($user);
290
            $attachment->attachable()->associate($model);
291
            $attachment->md5      = md5_file($file->getRealPath());
292
            $attachment->filename = $file->getClientOriginalName();
293
            $attachment->mime     = $file->getMimeType();
294
            $attachment->size     = $file->getSize();
295
            $attachment->uploaded = false;
296
            $attachment->save();
297
            Log::debug('Created attachment:', $attachment->toArray());
298
299
            $fileObject = $file->openFile();
300
            $fileObject->rewind();
301
302
            if (0 === $file->getSize()) {
303
                throw new FireflyException('Cannot upload empty or non-existent file.'); // @codeCoverageIgnore
304
            }
305
306
            $content = $fileObject->fread($file->getSize());
307
            Log::debug(sprintf('Full file length is %d and upload size is %d.', strlen($content), $file->getSize()));
308
309
            // store it:
310
            $this->uploadDisk->put($attachment->fileName(), $content);
311
            $attachment->uploaded = true; // update attachment
312
            $attachment->save();
313
            $this->attachments->push($attachment);
314
315
            $name = e($file->getClientOriginalName()); // add message:
316
            $msg  = (string) trans('validation.file_attached', ['name' => $name]);
317
            $this->messages->add('attachments', $msg);
318
        }
319
320
        return $attachment;
321
    }
322
323
    /**
324
     * Verify if the mime of a file is valid.
325
     *
326
     * @param UploadedFile $file
327
     *
328
     * @return bool
329
     */
330
    protected function validMime(UploadedFile $file): bool
331
    {
332
        Log::debug('Now in validMime()');
333
        $mime = e($file->getMimeType());
334
        $name = e($file->getClientOriginalName());
335
        Log::debug(sprintf('Name is %s, and mime is %s', $name, $mime));
336
        Log::debug('Valid mimes are', $this->allowedMimes);
337
        $result = true;
338
339
        if (!in_array($mime, $this->allowedMimes, true)) {
340
            $msg = (string) trans('validation.file_invalid_mime', ['name' => $name, 'mime' => $mime]);
341
            $this->errors->add('attachments', $msg);
342
            Log::error($msg);
343
344
            $result = false;
345
        }
346
347
        return $result;
348
    }
349
350
    /**
351
     * Verify if the size of a file is valid.
352
     *
353
     * @codeCoverageIgnore
354
     *
355
     * @param UploadedFile $file
356
     *
357
     * @return bool
358
     */
359
    protected function validSize(UploadedFile $file): bool
360
    {
361
        $size   = $file->getSize();
362
        $name   = e($file->getClientOriginalName());
363
        $result = true;
364
        if ($size > $this->maxUploadSize) {
365
            $msg = (string) trans('validation.file_too_large', ['name' => $name]);
366
            $this->errors->add('attachments', $msg);
367
            Log::error($msg);
368
369
            $result = false;
370
        }
371
372
        return $result;
373
    }
374
375
    /**
376
     * Verify if the file was uploaded correctly.
377
     *
378
     * @param UploadedFile $file
379
     * @param Model        $model
380
     *
381
     * @return bool
382
     */
383
    protected function validateUpload(UploadedFile $file, Model $model): bool
384
    {
385
        Log::debug('Now in validateUpload()');
386
        $result = true;
387
        if (!$this->validMime($file)) {
388
            $result = false;
389
        }
390
        if (0 === $file->getSize()) {
391
            Log::error('Cannot upload empty file.');
392
            $result = false;
393
        }
394
395
        // @codeCoverageIgnoreStart
396
        // can't seem to reach this point.
397
        if (true === $result && !$this->validSize($file)) {
398
            $result = false;
399
        }
400
        // @codeCoverageIgnoreEnd
401
        if (true === $result && $this->hasFile($file, $model)) {
402
            $result = false;
403
        }
404
405
        return $result;
406
    }
407
}
408