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
|
|||||
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
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
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.');
}
![]() |
|||||
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
|
|||||
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 |
If you suppress an error, we recommend checking for the error condition explicitly: