Passed
Pull Request — master (#58)
by Dmitriy
02:43
created

CodeFile::getId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Gii\Component\CodeFile;
6
7
use Diff;
8
use Diff_Renderer_Text_Unified;
9
use RuntimeException;
10
11
/**
12
 * CodeFile represents a code file to be generated.
13
 */
14
final class CodeFile
15
{
16
    /**
17
     * The new file mode
18
     */
19
    private const FILE_MODE = 0666;
20
    /**
21
     * The new directory mode
22
     */
23
    private const DIR_MODE = 0777;
24
25
    /**
26
     * @var string an ID that uniquely identifies this code file.
27
     */
28
    private string $id;
29
    /**
30
     * @var string the file path that the new code should be saved to.
31
     */
32
    private string $path;
33
    /**
34
     * @var CodeFileWriteOperationEnum the operation to be performed. This can be {@see OP_CREATE}, {@see OP_OVERWRITE}
35
     *     or {@see OP_SKIP}.
36
     */
37
    private CodeFileWriteOperationEnum $operation = CodeFileWriteOperationEnum::SAVE;
38
    /**
39
     * @var string the base path
40
     */
41
    private string $basePath = '';
42
    /**
43
     * @var int the permission to be set for newly generated code files.
44
     * This value will be used by PHP chmod function.
45
     * Defaults to 0666, meaning the file is read-writable by all users.
46
     */
47
    private int $newFileMode = self::FILE_MODE;
48
    /**
49
     * @var int the permission to be set for newly generated directories.
50
     * This value will be used by PHP chmod function.
51
     * Defaults to 0777, meaning the directory can be read, written and executed by all users.
52
     */
53
    private int $newDirMode = self::DIR_MODE;
54
    private CodeFileStateEnum $state;
55
56
    /**
57
     * Constructor.
58
     *
59
     * @param string $path the file path that the new code should be saved to.
60
     * @param string $content the newly generated code content.
61
     */
62 15
    public function __construct(string $path, private string $content)
63
    {
64 15
        $this->path = $this->preparePath($path);
65 15
        $this->id = dechex(crc32($this->path));
66 15
        $this->state = CodeFileStateEnum::NOT_EXIST;
67 15
        if (is_file($path)) {
68 5
            if (file_get_contents($path) === $content) {
69 2
                $this->operation = CodeFileWriteOperationEnum::SKIP;
70 2
                $this->state = CodeFileStateEnum::PRESENT_SAME;
71
            } else {
72 3
                $this->operation = CodeFileWriteOperationEnum::SAVE;
73 3
                $this->state = CodeFileStateEnum::PRESENT_DIFFERENT;
74
            }
75
        }
76
    }
77
78
    /**
79
     * Saves the code into the file specified by [[path]].
80
     *
81
     * @return CodeFileWriteStatusEnum the error occurred while saving the code file, or true if no error.
82
     */
83 3
    public function save(): CodeFileWriteStatusEnum
84
    {
85 3
        if ($this->operation === CodeFileWriteOperationEnum::SAVE && $this->state !== CodeFileStateEnum::PRESENT_SAME) {
86 3
            $dir = dirname($this->path);
87 3
            if (!is_dir($dir)) {
88 1
                if ($this->newDirMode !== self::DIR_MODE) {
89
                    $mask = @umask(0);
90
                    $result = @mkdir($dir, $this->newDirMode, true);
91
                    @umask($mask);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for umask(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

91
                    /** @scrutinizer ignore-unhandled */ @umask($mask);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
92
                } else {
93 1
                    $result = @mkdir($dir, 0777, true);
94
                }
95 1
                if (!$result) {
96
                    throw new RuntimeException("Unable to create the directory '$dir'.");
97
                }
98
            }
99
        }
100 3
        $status = match ($this->state) {
101 3
            CodeFileStateEnum::PRESENT_DIFFERENT => CodeFileWriteStatusEnum::OVERWROTE,
102 2
            CodeFileStateEnum::NOT_EXIST => CodeFileWriteStatusEnum::CREATED,
103
            default => CodeFileWriteStatusEnum::SKIPPED,
104
        };
105 3
        if (@file_put_contents($this->path, $this->content) === false) {
106
            throw new RuntimeException("Unable to write the file '{$this->path}'.");
107
        }
108
109 3
        if ($this->newFileMode !== self::FILE_MODE) {
110
            $mask = @umask(0);
111
            @chmod($this->path, $this->newFileMode);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

111
            /** @scrutinizer ignore-unhandled */ @chmod($this->path, $this->newFileMode);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
112
            @umask($mask);
113
        }
114
115 3
        return $status;
116
    }
117
118
    /**
119
     * @return string the code file path relative to the application base path.
120
     */
121 2
    public function getRelativePath(): string
122
    {
123 2
        if (!empty($this->basePath) && str_starts_with($this->path, $this->basePath)) {
124 1
            return substr($this->path, strlen($this->basePath) + 1);
125
        }
126
127 1
        return $this->path;
128
    }
129
130
    /**
131
     * @return string the code file extension (e.g. php, txt)
132
     */
133 1
    public function getType(): string
134
    {
135 1
        if (($pos = strrpos($this->path, '.')) !== false) {
136 1
            return substr($this->path, $pos + 1);
137
        }
138
139
        return 'unknown';
140
    }
141
142
    /**
143
     * Returns preview or false if it cannot be rendered
144
     */
145 3
    public function preview(): false|string
146
    {
147 3
        if (($pos = strrpos($this->path, '.')) !== false) {
148 2
            $type = substr($this->path, $pos + 1);
149
        } else {
150 1
            $type = 'unknown';
151
        }
152
153 3
        if ($type === 'php') {
154 1
            return highlight_string($this->content, true);
0 ignored issues
show
Bug Best Practice introduced by
The expression return highlight_string($this->content, true) could return the type true which is incompatible with the type-hinted return false|string. Consider adding an additional type-check to rule them out.
Loading history...
155
        }
156
157 2
        if (!in_array($type, ['jpg', 'gif', 'png', 'exe'])) {
158 1
            $content = htmlspecialchars(
159 1
                $this->content,
160 1
                ENT_NOQUOTES | ENT_SUBSTITUTE | ENT_HTML5,
161
                'UTF-8'
162
            );
163 1
            return nl2br($content);
164
        }
165
166 1
        return false;
167
    }
168
169
    /**
170
     * Returns diff or false if it cannot be calculated
171
     */
172 1
    public function diff(): false|string
173
    {
174 1
        $type = strtolower($this->getType());
175 1
        if (in_array($type, ['jpg', 'gif', 'png', 'exe'])) {
176
            return false;
177
        }
178
179 1
        if ($this->state === CodeFileStateEnum::PRESENT_DIFFERENT) {
180
            return $this->renderDiff(file($this->path), $this->content);
181
        }
182
183 1
        return '';
184
    }
185
186
    /**
187
     * Renders diff between two sets of lines
188
     */
189
    private function renderDiff(mixed $lines1, mixed $lines2): string
190
    {
191
        if (!is_array($lines1)) {
192
            $lines1 = explode("\n", $lines1);
193
        }
194
        if (!is_array($lines2)) {
195
            $lines2 = explode("\n", $lines2);
196
        }
197
        foreach ($lines1 as $i => $line) {
198
            $lines1[$i] = rtrim($line, "\r\n");
199
        }
200
        foreach ($lines2 as $i => $line) {
201
            $lines2[$i] = rtrim($line, "\r\n");
202
        }
203
204
        $renderer = new Diff_Renderer_Text_Unified();
205
        return (new Diff($lines1, $lines2))->render($renderer);
206
    }
207
208 1
    public function getId(): string
209
    {
210 1
        return $this->id;
211
    }
212
213 4
    public function getOperation(): CodeFileWriteOperationEnum
214
    {
215 4
        return $this->operation;
216
    }
217
218
    public function getState(): CodeFileStateEnum
219
    {
220
        return $this->state;
221
    }
222
223 1
    public function getPath(): string
224
    {
225 1
        return $this->path;
226
    }
227
228
    public function getContent(): string
229
    {
230
        return $this->content;
231
    }
232
233 2
    public function withBasePath(string $basePath): self
234
    {
235 2
        $new = clone $this;
236 2
        $new->basePath = $this->preparePath($basePath);
237
238 2
        return $new;
239
    }
240
241
    public function withNewFileMode(int $mode): self
242
    {
243
        $new = clone $this;
244
        $new->newFileMode = $mode;
245
246
        return $new;
247
    }
248
249
    public function withNewDirMode(int $mode): self
250
    {
251
        $new = clone $this;
252
        $new->newDirMode = $mode;
253
254
        return $new;
255
    }
256
257 15
    private function preparePath(string $path): string
258
    {
259 15
        return strtr($path, '/\\', DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR);
260
    }
261
}
262