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

CodeFile   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 235
Duplicated Lines 0 %

Test Coverage

Coverage 74.07%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 35
eloc 80
c 1
b 0
f 0
dl 0
loc 235
ccs 60
cts 81
cp 0.7407
rs 9.6

15 Methods

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

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

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