Passed
Pull Request — master (#34)
by Anton
02:40
created

CodeFile::__construct()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

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

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

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