CodeFile::renderDiff()   A
last analyzed

Complexity

Conditions 5
Paths 16

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 17
ccs 0
cts 11
cp 0
rs 9.6111
cc 5
nc 16
nop 2
crap 30
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 = CodeFileStateEnum::NOT_EXIST;
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 18
    public function __construct(string $path, private string $content)
63
    {
64 18
        $this->path = $this->preparePath($path);
65 18
        $this->id = dechex(crc32($this->path));
66 18
        if (is_file($path)) {
67 5
            if (file_get_contents($path) === $content) {
68 2
                $this->operation = CodeFileWriteOperationEnum::SKIP;
69 2
                $this->state = CodeFileStateEnum::PRESENT_SAME;
70
            } else {
71 3
                $this->operation = CodeFileWriteOperationEnum::SAVE;
72 3
                $this->state = CodeFileStateEnum::PRESENT_DIFFERENT;
73
            }
74
        }
75
    }
76
77
    /**
78
     * Saves the code into the file specified by [[path]].
79
     *
80
     * @return CodeFileWriteStatusEnum the error occurred while saving the code file, or true if no error.
81
     */
82 3
    public function save(): CodeFileWriteStatusEnum
83
    {
84 3
        if ($this->operation === CodeFileWriteOperationEnum::SAVE && $this->state !== CodeFileStateEnum::PRESENT_SAME) {
85 3
            $dir = dirname($this->path);
86 3
            if (!is_dir($dir)) {
87 1
                if ($this->newDirMode !== self::DIR_MODE) {
88
                    $mask = @umask(0);
89
                    $result = @mkdir($dir, $this->newDirMode, true);
90
                    @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

90
                    /** @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...
91
                } else {
92 1
                    $result = @mkdir($dir, 0777, true);
93
                }
94 1
                if (!$result) {
95
                    throw new RuntimeException("Unable to create the directory '$dir'.");
96
                }
97
            }
98
        }
99 3
        $status = match ($this->state) {
100 3
            CodeFileStateEnum::PRESENT_DIFFERENT => CodeFileWriteStatusEnum::OVERWROTE,
101 2
            CodeFileStateEnum::NOT_EXIST => CodeFileWriteStatusEnum::CREATED,
102
            default => CodeFileWriteStatusEnum::SKIPPED,
103 3
        };
104 3
        if (@file_put_contents($this->path, $this->content) === false) {
105
            throw new RuntimeException("Unable to write the file '{$this->path}'.");
106
        }
107
108 3
        if ($this->newFileMode !== self::FILE_MODE) {
109
            $mask = @umask(0);
110
            @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

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