Passed
Pull Request — master (#58)
by Daniel
06:14
created

DataUriFile::decodeDataUri()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 2
dl 0
loc 10
ccs 0
cts 7
cp 0
crap 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the Silverback API Components Bundle Project
5
 *
6
 * (c) Daniel West <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace Silverback\ApiComponentsBundle\Model\Uploadable;
15
16
use Symfony\Component\HttpFoundation\File\Exception\FileException;
17
use Symfony\Component\HttpFoundation\File\File;
18
use Symfony\Component\Mime\MimeTypes;
19
20
/**
21
 * Based on https://github.com/hshn/base64-encoded-file/blob/master/src/HttpFoundation/File/Base64EncodedFile.php
22
 * Additional support for data uri - regular expression detection.
23
 *
24
 * @author Daniel West <[email protected]>
25
 */
26
class DataUriFile extends File
27
{
28
    /**
29
     * @param string $encoded
30
     * @param bool   $checkPath
31
     */
32
    public function __construct($encoded, $checkPath = true)
33
    {
34
        parent::__construct($this->restoreToTemporary($encoded), $checkPath);
35
    }
36
37
    /**
38
     * @param string $encoded
39
     *
40
     * @throws FileException
41
     */
42
    private function restoreToTemporary($encoded): string
43
    {
44
        preg_match('/^(?:(?:data:(?:\/\/)?([A-Za-z]+\/[A-Za-z.-]+)(?:;(base64))?,|)(?:(.+)|((?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?)))$/', $encoded, $matches);
45
46
        $mimeType = $matches[1];
47
        $base64Match = 'base64' === $matches[2];
48
        $isBase64 = '' === $mimeType || $base64Match;
49
        if ($isBase64 && !$mimeType) {
50
            return $this->decodePlainBase64($encoded);
51
        }
52
        if (!$isBase64) {
53
            return $this->decodeDataUri($matches[3], $mimeType);
54
        }
55
56
        return $this->decodeBase64Data($mimeType, $encoded);
57
    }
58
59
    private function decodePlainBase64(string $encoded): string
60
    {
61
        if (false === $decoded = base64_decode($encoded, true)) {
62
            throw new FileException('Unable to decode the string as base64');
63
        }
64
65
        $path = $this->getTempFileBasePath();
66
67
        if (false === file_put_contents($path, $decoded, FILE_BINARY)) {
68
            throw new FileException(sprintf('Unable to write the file "%s"', $path));
69
        }
70
71
        return $path;
72
    }
73
74
    private function decodeDataUri(string $dataUriMatch, string $mimeType): string
75
    {
76
        $target = $this->createTempFileTarget($mimeType);
77
        $content = urldecode($dataUriMatch);
78
        if (false === @fwrite($target->resource, $content)) {
79
            throw new FileException(sprintf('Unable to write the file "%s"', $target->path));
80
        }
81
        $this->closeFile($target);
82
83
        return $target->path;
84
    }
85
86
    private function decodeBase64Data(string $mimeType, string $fullMatch): string
87
    {
88
        $target = $this->createTempFileTarget($mimeType);
89
90
        $source = @fopen($fullMatch, 'r');
91
        if (false === $source) {
92
            throw new FileException('Unable to decode strings as base64');
93
        }
94
95
        if (false === stream_copy_to_stream($source, $target->resource)) {
96
            throw new FileException(sprintf('Unable to write the file "%s"', $target->path));
97
        }
98
99
        if (false === @fclose($source)) {
100
            throw new FileException(sprintf('Unable to close data stream'));
101
        }
102
103
        $this->closeFile($target);
104
105
        return $target->path;
106
    }
107
108
    private function getTempFileBasePath(): string
109
    {
110
        if (false === $path = tempnam($directory = sys_get_temp_dir(), 'DataUriFile')) {
111
            throw new FileException(sprintf('Unable to create a file into the "%s" directory: %s', $directory, $path));
112
        }
113
114
        return $path;
115
    }
116
117
    private function createTempFileTarget(string $mimeType): object
118
    {
119
        $path = $this->getTempFileBasePath();
120
121
        if (null !== $extension = (MimeTypes::getDefault()->getExtensions($mimeType)[0] ?? null)) {
122
            $path .= '.' . $extension;
123
        }
124
125
        if (false === $target = @fopen($path, 'wb+')) {
126
            throw new FileException(sprintf('Unable to open the file "%s"', $path));
127
        }
128
129
        $class = new class() {
130
            public string $path;
131
            public $resource;
132
        };
133
        $class->path = $path;
134
        $class->resource = $target;
135
136
        return $class;
137
    }
138
139
    private function closeFile(object $target): void
140
    {
141
        if (false === @fclose($target->resource)) {
142
            throw new FileException(sprintf('Unable to close the file "%s"', $target->path));
143
        }
144
    }
145
}
146