Test Failed
Push — master ( 26c2a5...d708df )
by webdevetc
03:49
created

UploadsService::deletePostImage()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 13
rs 10
cc 4
nc 3
nop 1
1
<?php
2
3
namespace WebDevEtc\BlogEtc\Services;
4
5
use Auth;
6
use Carbon\Carbon;
7
use Exception;
8
use File;
9
use Illuminate\Contracts\Filesystem\Filesystem;
10
use Illuminate\Http\UploadedFile;
11
use Illuminate\Support\Str;
12
use Image;
13
use Intervention\Image\Constraint;
14
use RuntimeException;
15
use Storage;
16
use WebDevEtc\BlogEtc\Events\BlogPostAdded;
17
use WebDevEtc\BlogEtc\Events\BlogPostEdited;
18
use WebDevEtc\BlogEtc\Events\BlogPostWillBeDeleted;
19
use WebDevEtc\BlogEtc\Events\UploadedImage;
20
use WebDevEtc\BlogEtc\Helpers;
21
use WebDevEtc\BlogEtc\Interfaces\LegacyGetImageFileInterface;
22
use WebDevEtc\BlogEtc\Models\Post;
23
use WebDevEtc\BlogEtc\Models\UploadedPhoto;
24
use WebDevEtc\BlogEtc\Repositories\UploadedPhotosRepository;
25
use WebDevEtc\BlogEtc\Requests\CreateBlogEtcPostRequest;
26
use WebDevEtc\BlogEtc\Requests\DeleteBlogEtcPostRequest;
27
//use WebDevEtc\BlogEtc\Requests\PostRequest;
28
use WebDevEtc\BlogEtc\Requests\UpdateBlogEtcPostRequest;
29
use WebDevEtc\BlogEtc\Requests\UploadImageRequest;
30
31
/**
32
 * Class UploadsService.
33
 */
34
class UploadsService
35
{
36
    /**
37
     * How many iterations to find an available filename, before exception.
38
     *
39
     * @var int
40
     */
41
    private static $availableFilenameAttempts = 10;
42
    /**
43
     * @var UploadedPhotosRepository
44
     */
45
    private $repository;
46
47
    public function __construct(UploadedPhotosRepository $repository)
48
    {
49
        $this->repository = $repository;
50
    }
51
52
    /**
53
     * Save a new post.
54
     */
55
    public function legacyStorePost(CreateBlogEtcPostRequest $request)
56
    {
57
        $new_blog_post = new Post($request->all());
58
59
        $this->legacyProcessUploadedImages($request, $new_blog_post);
60
61
        if (!$new_blog_post->posted_at) {
62
            $new_blog_post->posted_at = Carbon::now();
63
        }
64
65
        $new_blog_post->user_id = Auth::user()->id;
66
        $new_blog_post->save();
67
68
        $new_blog_post->categories()->sync($request->categories());
69
70
        Helpers::flashMessage('Added post');
71
        event(new BlogPostAdded($new_blog_post));
72
73
        return $new_blog_post->editUrl();
74
    }
75
76
    /**
77
     * This uses some legacy code. This will get refactored soon into something nicer.
78
     */
79
    public function legacyUpdatePost(UpdateBlogEtcPostRequest $request, $blogPostId)
80
    {
81
        /** @var Post $post */
82
        $post = Post::findOrFail($blogPostId);
83
        $post->fill($request->all());
84
85
        $this->legacyProcessUploadedImages($request, $post);
86
87
        $post->save();
88
        $post->categories()->sync($request->categories());
89
90
        Helpers::flashMessage('Updated post');
91
        event(new BlogPostEdited($post));
92
93
        return $post->editUrl();
94
    }
95
96
    /**
97
     * Legacy method - will get updated soon.
98
     *
99
     * Process any uploaded images (for featured image).
100
     *
101
     * @throws Exception
102
     *
103
     * @return array returns an array of details about each file resized
104
     *
105
     * @todo - This class was added after the other main features, so this duplicates some code from the main blog post admin controller (BlogEtcAdminController). For next full release this should be tided up.
106
     */
107
    public function legacyProcessUploadedImagesSingle(UploadImageRequest $request)
108
    {
109
        $this->increaseMemoryLimit();
110
        $photo = $request->file('upload');
111
112
        $uploaded_image_details = [];
113
114
        $sizes_to_upload = $request->get('sizes_to_upload');
115
116
        // now upload a full size - this is a special case, not in the config file. We only store full size images in this class, not as part of the featured blog image uploads.
117
        if (isset($sizes_to_upload['blogetc_full_size']) && 'true' === $sizes_to_upload['blogetc_full_size']) {
118
            $uploaded_image_details['blogetc_full_size'] = $this->legacyUploadAndResize(null, $request->get('image_title'),
119
                'fullsize', $photo);
120
        }
121
122
        foreach ((array) config('blogetc.image_sizes') as $size => $image_size_details) {
123
            if (!isset($sizes_to_upload[$size]) || !$sizes_to_upload[$size] || !$image_size_details['enabled']) {
124
                continue;
125
            }
126
127
            $uploaded_image_details[$size] = $this->legacyUploadAndResize(null, $request->get('image_title'),
128
                $image_size_details, $photo);
129
        }
130
131
        UploadedPhoto::create([
132
            'image_title'     => $request->get('image_title'),
133
            'source'          => 'ImageUpload',
134
            'uploader_id'     => (int) Auth::id(),
135
            'uploaded_images' => $uploaded_image_details,
136
        ]);
137
138
        return $uploaded_image_details;
139
    }
140
141
    /**
142
     * Process any uploaded images (for featured image).
143
     *
144
     * @param $new_blog_post
145
     *
146
     * @throws Exception
147
     *
148
     * @todo - next full release, tidy this up!
149
     */
150
    public function legacyProcessUploadedImages(LegacyGetImageFileInterface $request, Post $new_blog_post)
151
    {
152
        if (!config('blogetc.image_upload_enabled')) {
153
            return;
154
        }
155
156
        $this->increaseMemoryLimit();
157
158
        $uploaded_image_details = [];
159
160
        foreach ((array) config('blogetc.image_sizes') as $size => $image_size_details) {
161
            if ($image_size_details['enabled'] && $photo = $request->get_image_file($size)) {
162
                $uploaded_image = $this->legacyUploadAndResize($new_blog_post, $new_blog_post->title, $image_size_details,
163
                    $photo);
164
165
                $new_blog_post->$size = $uploaded_image['filename'];
166
                $uploaded_image_details[$size] = $uploaded_image;
167
            }
168
        }
169
170
        // todo: link this to the blogetc_post row.
171
        if (count(array_filter($uploaded_image_details)) > 0) {
172
            UploadedPhoto::create([
173
                'source'          => 'BlogFeaturedImage',
174
                'uploaded_images' => $uploaded_image_details,
175
            ]);
176
        }
177
    }
178
179
    /**
180
     * @param Post $new_blog_post
181
     * @param $suggested_title string - used to help generate the filename
182
     * @param $image_size_details mixed - either an array (with 'w' and 'h') or a string (and it'll be uploaded at full size, no size reduction, but will use this string to generate the filename)
183
     * @param $photo
184
     *
185
     * @throws Exception
186
     *
187
     * @return array
188
     */
189
    protected function legacyUploadAndResize(Post $new_blog_post = null, $suggested_title, $image_size_details, $photo)
190
    {
191
        $image_filename = $this->legacyGetImageFilename($suggested_title, $image_size_details, $photo);
192
        $destinationPath = $this->image_destination_path();
193
194
        $resizedImage = Image::make($photo->getRealPath());
195
196
        if (is_array($image_size_details)) {
197
            $w = $image_size_details['w'];
198
            $h = $image_size_details['h'];
199
200
            if (isset($image_size_details['crop']) && $image_size_details['crop']) {
201
                $resizedImage = $resizedImage->fit($w, $h);
202
            } else {
203
                $resizedImage = $resizedImage->resize($w, $h, static function ($constraint) {
204
                    $constraint->aspectRatio();
205
                });
206
            }
207
        } elseif ('fullsize' === $image_size_details) {
208
            $w = $resizedImage->width();
209
            $h = $resizedImage->height();
210
        } else {
211
            throw new Exception('Invalid image_size_details value');
212
        }
213
214
        $resizedImage->save($destinationPath.'/'.$image_filename, config('blogetc.image_quality', 80));
215
216
        event(new UploadedImage($image_filename, $resizedImage, $new_blog_post, __METHOD__));
217
218
        return [
219
            'filename' => $image_filename,
220
            'w'        => $w,
221
            'h'        => $h,
222
        ];
223
    }
224
225
    /**
226
     * @throws RuntimeException
227
     *
228
     * @return string
229
     */
230
    public function image_destination_path()
231
    {
232
        $path = public_path('/'.config('blogetc.blog_upload_dir'));
233
        $this->check_image_destination_path_is_writable($path);
234
235
        return $path;
236
    }
237
238
    /**
239
     * Legacy - will be removed
240
     * Check if the image destination directory is writable.
241
     * Throw an exception if it was not writable.
242
     *
243
     * @param $path
244
     *
245
     * @throws RuntimeException
246
     */
247
    protected function check_image_destination_path_is_writable($path)
248
    {
249
        if (!is_writable($path)) {
250
            throw new RuntimeException("Image destination path is not writable ($path)");
251
        }
252
    }
253
254
    /**
255
     * Legacy function, will get refactored soon into something nicer!
256
     * Get a filename (that doesn't exist) on the filesystem.
257
     *
258
     * Todo: support multiple filesystem locations.
259
     *
260
     * @param $image_size_details - either an array (with w/h attributes) or a string
0 ignored issues
show
Documentation Bug introduced by
The doc comment - at position 0 could not be parsed: Unknown type name '-' at position 0 in -.
Loading history...
261
     *
262
     * @throws RuntimeException
263
     *
264
     * @return string
265
     */
266
    public function legacyGetImageFilename(string $suggested_title, $image_size_details, UploadedFile $photo)
267
    {
268
        $base = $this->generate_base_filename($suggested_title);
269
270
        // $wh will be something like "-1200x300"
271
        $wh = $this->getDimensions($image_size_details);
272
        $ext = '.'.$photo->getClientOriginalExtension();
273
274
        for ($i = 1; $i <= 10; $i++) {
275
            // add suffix if $i>1
276
            $suffix = $i > 1 ? '-'.str_random(5) : '';
277
278
            $attempt = str_slug($base.$suffix.$wh).$ext;
279
280
            if (!File::exists($this->image_destination_path().'/'.$attempt)) {
281
                // filename doesn't exist, let's use it!
282
                return $attempt;
283
            }
284
        }
285
286
        // too many attempts...
287
        throw new RuntimeException("Unable to find a free filename after $i attempts - aborting now.");
288
    }
289
290
    /**
291
     * @return string
292
     */
293
    protected function generate_base_filename(string $suggested_title)
294
    {
295
        $base = substr($suggested_title, 0, 100);
296
        if (!$base) {
297
            // if we have an empty string then we should use a random one:
298
            $base = 'image-'.str_random(5);
299
300
            return $base;
301
        }
302
303
        return $base;
304
    }
305
306
    /**
307
     * Given a filename, return a public url for that asset on the filesystem as defined in the config.
308
     */
309
    public static function publicUrl(string $filename): string
310
    {
311
        return self::disk()->url(config('blogetc.blog_upload_dir').'/'.$filename);
312
    }
313
314
    /**
315
     * Disk for filesystem storage.
316
     *
317
     * Set the relevant config file to use things such as S3.
318
     */
319
    public static function disk(): Filesystem
320
    {
321
        return Storage::disk(config('blogetc.image_upload_disk', 'public'));
322
    }
323
324
//    /**
325
//     * Handle an image upload via the upload image section (not blog post featured image).
326
//     *
327
//     * @param $uploadedImage
328
//     *
329
//     * @throws Exception
330
//     */
331
//    public function processUpload($uploadedImage, string $imageTitle): array
332
//    {
333
//        // to save in db later
334
//        $uploadedImageDetails = [];
335
//        $this->increaseMemoryLimit();
336
//
337
//        if (config('blogetc.image_store_full_size')) {
338
//            // Store as full size
339
//            $uploadedImageDetails['blogetc_full_size'] = $this->uploadAndResize(
340
//                null,
341
//                $imageTitle,
342
//                'fullsize',
343
//                $uploadedImage
344
//            );
345
//        }
346
//
347
//        foreach ((array) config('blogetc.image_sizes') as $size => $imageSizeDetails) {
348
//            $uploadedImageDetails[$size] = $this->uploadAndResize(
349
//                null,
350
//                $imageTitle,
351
//                $imageSizeDetails,
352
//                $uploadedImage
353
//            );
354
//        }
355
//
356
//        // Store the image data in db:
357
//        $this->storeInDatabase(
358
//            null,
359
//            $imageTitle,
360
//            UploadedPhoto::SOURCE_IMAGE_UPLOAD,
361
//            (int) Auth::id(),
362
//            $uploadedImageDetails
363
//        );
364
//
365
//        return $uploadedImageDetails;
366
//    }
367
368
    /**
369
     * Small method to increase memory limit.
370
     * This can be defined in the config file. If blogetc.memory_limit is false/null then it won't do anything.
371
     * This is needed though because if you upload a large image it'll not work.
372
     */
373
    public function increaseMemoryLimit(): void
374
    {
375
        // increase memory - change this setting in config file
376
        if (config('blogetc.memory_limit')) {
377
            ini_set('memory_limit', config('blogetc.memory_limit'));
378
        }
379
    }
380
381
    /**
382
     * Resize and store an image.
383
     *
384
     * @param Post $new_blog_post
385
     * @param $suggested_title - used to help generate the filename
0 ignored issues
show
Documentation Bug introduced by
The doc comment - at position 0 could not be parsed: Unknown type name '-' at position 0 in -.
Loading history...
386
     * @param array|string $imageSizeDetails - either an array (with 'w' and 'h') or a string (and it'll be uploaded at full size,
387
     *                                       no size reduction, but will use this string to generate the filename)
388
     * @param $photo
389
     *
390
     * @throws Exception
391
     */
392
    protected function uploadAndResize(
393
        ?Post $new_blog_post,
394
        $suggested_title,
395
        $imageSizeDetails,
396
        UploadedFile $photo
397
    ): array {
398
        // get the filename/filepath
399
        $image_filename = $this->getImageFilename($suggested_title, $imageSizeDetails, $photo);
400
        $destinationPath = $this->imageDestinationPath();
401
402
        // make image
403
        $resizedImage = Image::make($photo->getRealPath());
404
405
        if (is_array($imageSizeDetails)) {
406
            // resize to these dimensions:
407
            $w = $imageSizeDetails['w'];
408
            $h = $imageSizeDetails['h'];
409
410
            if (isset($imageSizeDetails['crop']) && $imageSizeDetails['crop']) {
411
                $resizedImage = $resizedImage->fit($w, $h);
412
            } else {
413
                $resizedImage = $resizedImage->resize($w, $h, static function (Constraint $constraint) {
414
                    $constraint->aspectRatio();
415
                });
416
            }
417
        } elseif ('fullsize' === $imageSizeDetails) {
418
            // nothing to do here - no resizing needed.
419
            // We just need to set $w/$h with the original w/h values
420
            $w = $resizedImage->width();
421
            $h = $resizedImage->height();
422
        } else {
423
            throw new RuntimeException('Invalid image_size_details value of '.$imageSizeDetails);
424
        }
425
426
        $imageQuality = config('blogetc.image_quality', 80);
427
        $format = pathinfo($image_filename, PATHINFO_EXTENSION);
428
        $resizedImageData = $resizedImage->encode($format, $imageQuality);
429
        $this::disk()->put($destinationPath.'/'.$image_filename, $resizedImageData);
430
431
        event(new UploadedImage($image_filename, $resizedImage, $new_blog_post, __METHOD__));
432
433
        return [
434
            'filename' => $image_filename,
435
            'w'        => $w,
436
            'h'        => $h,
437
        ];
438
    }
439
440
    /**
441
     * Get a filename (that doesn't exist) on the filesystem.
442
     *
443
     * @param $image_size_details - either an array (with w/h attributes) or a string
0 ignored issues
show
Documentation Bug introduced by
The doc comment - at position 0 could not be parsed: Unknown type name '-' at position 0 in -.
Loading history...
444
     *
445
     * @throws RuntimeException
446
     */
447
    protected function getImageFilename(string $suggested_title, $image_size_details, UploadedFile $photo): string
448
    {
449
        $base = $this->baseFilename($suggested_title);
450
451
        // $wh will be something like "-1200x300"
452
        $wh = $this->getDimensions($image_size_details);
453
        $ext = '.'.$photo->getClientOriginalExtension();
454
455
        for ($i = 1; $i <= self::$availableFilenameAttempts; $i++) {
456
            // add suffix if $i>1
457
            $suffix = $i > 1 ? '-'.Str::random(5) : '';
458
459
            $attempt = Str::slug($base.$suffix.$wh).$ext;
460
461
            if (!$this::disk()->exists($this->imageDestinationPath().'/'.$attempt)) {
462
                return $attempt;
463
            }
464
        }
465
466
        // too many attempts...
467
        throw new RuntimeException("Unable to find a free filename after $i attempts - aborting now.");
468
    }
469
470
    protected function baseFilename(string $suggestedTitle): string
471
    {
472
        $base = substr($suggestedTitle, 0, 100);
473
474
        return $base ?: 'image-'.Str::random(5);
475
    }
476
477
    /**
478
     * Get the width and height as a string, with x between them
479
     * (123x456).
480
     *
481
     * It will always be prepended with '-'
482
     *
483
     * Example return value: -123x456
484
     *
485
     * $image_size_details should either be an array with two items ([$width, $height]),
486
     * or a string.
487
     *
488
     * If an array is given:
489
     * getWhForFilename([123,456]) it will return "-123x456"
490
     *
491
     * If a string is given:
492
     * getWhForFilename("some string") it will return -some-string". (max len: 30)
493
     *
494
     * @param array|string $imageSize
495
     *
496
     * @throws RuntimeException
497
     */
498
    protected function getDimensions($imageSize): string
499
    {
500
        if (is_array($imageSize)) {
501
            return '-'.$imageSize['w'].'x'.$imageSize['h'];
502
        }
503
504
        return '-'.Str::slug(substr($imageSize, 0, 30));
505
    }
506
507
    /**
508
     * @deprecated - use getDimensions()
509
     */
510
    protected function getWhForFilename($image_size_details)
511
    {
512
        return $this->getDimensions($image_size_details);
513
    }
514
515
    /**
516
     * @throws RuntimeException
517
     */
518
    protected function imageDestinationPath(): string
519
    {
520
        return config('blogetc.blog_upload_dir');
521
    }
522
523
    /**
524
     * Store new image upload meta data in database.
525
     */
526
    protected function storeInDatabase(
527
        ?int $blogPostID,
528
        string $imageTitle,
529
        string $source,
530
        ?int $uploaderID,
531
        array $uploadedImages
532
    ): UploadedPhoto {
533
        // store the image upload.
534
        return $this->repository->create([
535
            'blog_etc_post_id' => $blogPostID,
536
            'image_title'      => $imageTitle,
537
            'source'           => $source,
538
            'uploader_id'      => $uploaderID,
539
            'uploaded_images'  => $uploadedImages,
540
        ]);
541
    }
542
543
//    /**
544
//     * Process any uploaded images (for featured image).
545
//     *
546
//     * @throws Exception
547
//     *
548
//     * @todo - next full release, tidy this up!
549
//     */
550
//    public function processFeaturedUpload(PostRequest $request, Post $new_blog_post): ?array
551
//    {
552
//        if (! config('blogetc.image_upload_enabled')) {
553
//            // image upload was disabled
554
//            return null;
555
//        }
556
//
557
//        $newSizes = [];
558
//        $this->increaseMemoryLimit();
559
//
560
//        // to save in db later
561
//        $uploaded_image_details = [];
562
//
563
//        $enabledImageSizes = collect((array) config('blogetc.image_sizes'))
564
//            ->filter(function ($size) {
565
//                return ! empty($size['enabled']);
566
//            });
567
//
568
//        foreach ($enabledImageSizes as $size => $image_size_details) {
569
//            $photo = $request->getImageSize($size);
570
//
571
//            if (! $photo) {
572
//                continue;
573
//            }
574
//
575
//            $uploaded_image = $this->uploadAndResize(
576
//                $new_blog_post,
577
//                $new_blog_post->title,
578
//                $image_size_details,
579
//                $photo
580
//            );
581
//
582
//            $newSizes[$size] = $uploaded_image['filename'];
583
//
584
//            $uploaded_image_details[$size] = $uploaded_image;
585
//        }
586
//
587
//        // store the image upload.
588
//        if (empty($newSizes)) {
589
//            // Nothing to do if there were no sizes in config.
590
//            return null;
591
//        }
592
//
593
//        // todo: link this to the blogetc_post row.
594
//        $this->storeInDatabase(
595
//            $new_blog_post->id,
596
//            $new_blog_post->title,
597
//            UploadedPhoto::SOURCE_FEATURED_IMAGE,
598
//            Auth::id(),
599
//            $uploaded_image_details
600
//        );
601
//
602
//        return $newSizes;
603
//    }
604
605
    /**
606
     * Legacy function, will be refactored soon.
607
     *
608
     * @param $blogPostId
609
     *
610
     * @return mixed
611
     */
612
    public function legacyDestroyPost(/* @scrutinizer ignore-unused */ DeleteBlogEtcPostRequest $request, $blogPostId)
613
    {
614
        $post = Post::findOrFail($blogPostId);
615
        event(new BlogPostWillBeDeleted($post));
616
617
        $post->delete();
618
619
        // todo - delete the featured images?
620
        // At the moment it just issues a warning saying the images are still on the server.
621
622
        return $post;
623
    }
624
625
    public function deletePostImage(Post $post):array{
626
        $deletedImageSizes = [];
627
        foreach(array_keys(config('blogetc.image_sizes')) as $size) {
628
            $imageFilename = $post->$size;
629
            $path = $this->image_destination_path() .'/'. $imageFilename;
630
631
            if($imageFilename && file_exists($path) ){
632
                unlink($path);
633
                $deletedImageSizes[] = $size;
634
            }
635
        }
636
637
        return $deletedImageSizes;
638
    }
639
}
640
641