Passed
Pull Request — master (#56)
by Rustam
02:34
created

CodeFile   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 247
Duplicated Lines 0 %

Test Coverage

Coverage 74.7%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 35
eloc 87
c 1
b 0
f 0
dl 0
loc 247
ccs 62
cts 83
cp 0.747
rs 9.6

15 Methods

Rating   Name   Duplication   Size   Complexity  
A getPath() 0 3 1
A renderDiff() 0 17 5
A withBasePath() 0 6 1
B save() 0 28 7
A preview() 0 22 4
A getId() 0 3 1
A withNewFileMode() 0 6 1
A withNewDirMode() 0 6 1
A __construct() 0 6 3
A getContent() 0 3 1
A preparePath() 0 3 1
A getType() 0 7 2
A getOperation() 0 3 1
A diff() 0 12 3
A getRelativePath() 0 7 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Gii;
6
7
use Diff;
8
use RuntimeException;
9
use Yiisoft\Yii\Gii\Component\DiffRendererHtmlInline;
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
     * The code file is new.
26
     */
27
    public const OP_CREATE = 0;
28
    /**
29
     * The code file already exists, and the new one may need to overwrite it.
30
     */
31
    public const OP_OVERWRITE = 1;
32
    /**
33
     * The new code file and the existing one are identical.
34
     */
35
    public const OP_SKIP = 2;
36
    /**
37
     * Operations map to be performed.
38
     */
39
    public const OPERATIONS_MAP = [
40
        self::OP_CREATE => 'Create',
41
        self::OP_OVERWRITE => 'Overwrite',
42
        self::OP_SKIP => 'Skip',
43
    ];
44
45
    /**
46
     * @var string an ID that uniquely identifies this code file.
47
     */
48
    private string $id;
49
    /**
50
     * @var string the file path that the new code should be saved to.
51
     */
52
    private string $path;
53
    /**
54
     * @var int the operation to be performed. This can be {@see OP_CREATE}, {@see OP_OVERWRITE} or {@see OP_SKIP}.
55
     */
56
    private int $operation = self::OP_CREATE;
57
    /**
58
     * @var string the base path
59
     */
60
    private string $basePath = '';
61
    /**
62
     * @var int the permission to be set for newly generated code files.
63
     * This value will be used by PHP chmod function.
64
     * Defaults to 0666, meaning the file is read-writable by all users.
65
     */
66
    private int $newFileMode = self::FILE_MODE;
67
    /**
68
     * @var int the permission to be set for newly generated directories.
69
     * This value will be used by PHP chmod function.
70
     * Defaults to 0777, meaning the directory can be read, written and executed by all users.
71
     */
72
    private int $newDirMode = self::DIR_MODE;
73
74
    /**
75
     * Constructor.
76
     *
77
     * @param string $path the file path that the new code should be saved to.
78
     * @param string $content the newly generated code content.
79
     */
80 22
    public function __construct(string $path, private string $content)
81
    {
82 22
        $this->path = $this->preparePath($path);
83 22
        $this->id = dechex(crc32($this->path));
84 22
        if (is_file($path)) {
85 6
            $this->operation = file_get_contents($path) === $content ? self::OP_SKIP : self::OP_OVERWRITE;
86
        }
87
    }
88
89
    /**
90
     * Saves the code into the file specified by [[path]].
91
     *
92
     * @return bool the error occurred while saving the code file, or true if no error.
93
     */
94 3
    public function save(): bool
95
    {
96 3
        if ($this->operation === self::OP_CREATE) {
97 2
            $dir = dirname($this->path);
98 2
            if (!is_dir($dir)) {
99 1
                if ($this->newDirMode !== self::DIR_MODE) {
100
                    $mask = @umask(0);
101
                    $result = @mkdir($dir, $this->newDirMode, true);
102
                    @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

102
                    /** @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...
103
                } else {
104 1
                    $result = @mkdir($dir, 0777, true);
105
                }
106 1
                if (!$result) {
107
                    throw new RuntimeException("Unable to create the directory '$dir'.");
108
                }
109
            }
110
        }
111 3
        if (@file_put_contents($this->path, $this->content) === false) {
112
            throw new RuntimeException("Unable to write the file '{$this->path}'.");
113
        }
114
115 3
        if ($this->newFileMode !== self::FILE_MODE) {
116
            $mask = @umask(0);
117
            @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

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