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

CodeFile::renderDiff()   A

Complexity

Conditions 5
Paths 16

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5.0187

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 17
ccs 10
cts 11
cp 0.9091
rs 9.6111
cc 5
nc 16
nop 2
crap 5.0187
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
    public const OPERATIONS = [
37
        self::OP_CREATE => 'Create',
38
        self::OP_OVERWRITE => 'Overwrite',
39
        self::OP_SKIP => 'Skip',
40
    ];
41
42
    /**
43
     * @var string an ID that uniquely identifies this code file.
44
     */
45
    private string $id;
46
    /**
47
     * @var string the file path that the new code should be saved to.
48
     */
49
    private string $path;
50
    /**
51
     * @var int the operation to be performed. This can be [[OP_CREATE]], [[OP_OVERWRITE]] or [[OP_SKIP]].
52
     */
53
    private int $operation = self::OP_CREATE;
54
    /**
55
     * @var string the base path
56
     */
57
    private string $basePath = '';
58
    /**
59
     * @var int the permission to be set for newly generated code files.
60
     * This value will be used by PHP chmod function.
61
     * Defaults to 0666, meaning the file is read-writable by all users.
62
     */
63
    private int $newFileMode = self::FILE_MODE;
64
    /**
65
     * @var int the permission to be set for newly generated directories.
66
     * This value will be used by PHP chmod function.
67
     * Defaults to 0777, meaning the directory can be read, written and executed by all users.
68
     */
69
    private int $newDirMode = self::DIR_MODE;
70
71
    /**
72
     * Constructor.
73
     *
74
     * @param string $path the file path that the new code should be saved to.
75
     * @param string $content the newly generated code content.
76
     */
77 22
    public function __construct(string $path, private string $content)
78
    {
79 22
        $this->path = $this->preparePath($path);
80 22
        $this->id = dechex(crc32($this->path));
81 22
        if (is_file($path)) {
82 6
            $this->operation = file_get_contents($path) === $content ? self::OP_SKIP : self::OP_OVERWRITE;
83
        }
84
    }
85
86
    /**
87
     * Saves the code into the file specified by [[path]].
88
     *
89
     * @return bool the error occurred while saving the code file, or true if no error.
90
     */
91 3
    public function save(): bool
92
    {
93 3
        if ($this->operation === self::OP_CREATE) {
94 2
            $dir = dirname($this->path);
95 2
            if (!is_dir($dir)) {
96 1
                if ($this->newDirMode !== self::DIR_MODE) {
97
                    $mask = @umask(0);
98
                    $result = @mkdir($dir, $this->newDirMode, true);
99
                    @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

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

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