@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class PhabricatorFilesManagementIntegrityWorkflow
4 extends PhabricatorFilesManagementWorkflow {
5
6 protected function didConstruct() {
7 $arguments = $this->newIteratorArguments();
8
9 $arguments[] = array(
10 'name' => 'strip',
11 'help' => pht(
12 'DANGEROUS. Strip integrity hashes from files. This makes '.
13 'files vulnerable to corruption or tampering.'),
14 );
15
16 $arguments[] = array(
17 'name' => 'corrupt',
18 'help' => pht(
19 'Corrupt integrity hashes for given files. This is intended '.
20 'for debugging.'),
21 );
22
23 $arguments[] = array(
24 'name' => 'compute',
25 'help' => pht(
26 'Compute and update integrity hashes for files which do not '.
27 'yet have them.'),
28 );
29
30 $arguments[] = array(
31 'name' => 'overwrite',
32 'help' => pht(
33 'DANGEROUS. Recompute and update integrity hashes, overwriting '.
34 'invalid hashes. This may mark corrupt or dangerous files as '.
35 'valid.'),
36 );
37
38 $arguments[] = array(
39 'name' => 'force',
40 'short' => 'f',
41 'help' => pht(
42 'Execute dangerous operations without prompting for '.
43 'confirmation.'),
44 );
45
46
47 $this
48 ->setName('integrity')
49 ->setSynopsis(pht('Verify or recalculate file integrity hashes.'))
50 ->setArguments($arguments);
51 }
52
53 public function execute(PhutilArgumentParser $args) {
54 $modes = array();
55
56 $is_strip = $args->getArg('strip');
57 if ($is_strip) {
58 $modes[] = 'strip';
59 }
60
61 $is_corrupt = $args->getArg('corrupt');
62 if ($is_corrupt) {
63 $modes[] = 'corrupt';
64 }
65
66 $is_compute = $args->getArg('compute');
67 if ($is_compute) {
68 $modes[] = 'compute';
69 }
70
71 $is_overwrite = $args->getArg('overwrite');
72 if ($is_overwrite) {
73 $modes[] = 'overwrite';
74 }
75
76 $is_verify = !$modes;
77 if ($is_verify) {
78 $modes[] = 'verify';
79 }
80
81 if (count($modes) > 1) {
82 throw new PhutilArgumentUsageException(
83 pht(
84 'You have selected multiple operation modes (%s). Choose a '.
85 'single mode to operate in.',
86 implode(', ', $modes)));
87 }
88
89 $is_force = $args->getArg('force');
90 if (!$is_force) {
91 $prompt = null;
92 if ($is_strip) {
93 $prompt = pht(
94 'Stripping integrity hashes is dangerous and makes files '.
95 'vulnerable to corruption or tampering.');
96 }
97
98 if ($is_corrupt) {
99 $prompt = pht(
100 'Corrupting integrity hashes will prevent files from being '.
101 'accessed. This mode is intended only for development and '.
102 'debugging.');
103 }
104
105 if ($is_overwrite) {
106 $prompt = pht(
107 'Overwriting integrity hashes is dangerous and may mark files '.
108 'which have been corrupted or tampered with as safe.');
109 }
110
111 if ($prompt) {
112 $this->logWarn(pht('DANGEROUS'), $prompt);
113
114 if (!phutil_console_confirm(pht('Continue anyway?'))) {
115 throw new PhutilArgumentUsageException(pht('Aborted workflow.'));
116 }
117 }
118 }
119
120 $iterator = $this->buildIterator($args);
121
122 $failure_count = 0;
123 $total_count = 0;
124
125 foreach ($iterator as $file) {
126 $total_count++;
127 $display_name = $file->getMonogram();
128
129 $old_hash = $file->getIntegrityHash();
130
131 if ($is_strip) {
132 if ($old_hash === null) {
133 $this->logInfo(
134 pht('SKIPPED'),
135 pht(
136 'File "%s" does not have an integrity hash to strip.',
137 $display_name));
138 } else {
139 $file
140 ->setIntegrityHash(null)
141 ->save();
142
143 $this->logWarn(
144 pht('STRIPPED'),
145 pht(
146 'Stripped integrity hash for "%s".',
147 $display_name));
148 }
149
150 continue;
151 }
152
153 $need_hash = ($is_verify && $old_hash) ||
154 ($is_compute && ($old_hash === null)) ||
155 ($is_corrupt) ||
156 ($is_overwrite);
157 if ($need_hash) {
158 try {
159 $new_hash = $file->newIntegrityHash();
160 } catch (Exception $ex) {
161 $failure_count++;
162
163 $this->logFail(
164 pht('ERROR'),
165 pht(
166 'Unable to compute integrity hash for file "%s": %s',
167 $display_name,
168 $ex->getMessage()));
169
170 continue;
171 }
172 } else {
173 $new_hash = null;
174 }
175
176 // NOTE: When running in "corrupt" mode, we only corrupt the hash if
177 // we're able to compute a valid hash. Some files, like chunked files,
178 // do not support integrity hashing so corrupting them would create an
179 // unusual state.
180
181 if ($is_corrupt) {
182 if ($new_hash === null) {
183 $this->logInfo(
184 pht('IGNORED'),
185 pht(
186 'Storage for file "%s" does not support integrity hashing.',
187 $display_name));
188 } else {
189 $file
190 ->setIntegrityHash('<corrupted>')
191 ->save();
192
193 $this->logWarn(
194 pht('CORRUPTED'),
195 pht(
196 'Corrupted integrity hash for file "%s".',
197 $display_name));
198 }
199
200 continue;
201 }
202
203 if ($is_verify) {
204 if ($old_hash === null) {
205 $this->logInfo(
206 pht('NONE'),
207 pht(
208 'File "%s" has no stored integrity hash.',
209 $display_name));
210 } else if ($new_hash === null) {
211 $failure_count++;
212
213 $this->logWarn(
214 pht('UNEXPECTED'),
215 pht(
216 'Storage for file "%s" does not support integrity hashing, '.
217 'but the file has an integrity hash.',
218 $display_name));
219 } else if (phutil_hashes_are_identical($old_hash, $new_hash)) {
220 $this->logOkay(
221 pht('VALID'),
222 pht(
223 'File "%s" has a valid integrity hash.',
224 $display_name));
225 } else {
226 $failure_count++;
227
228 $this->logFail(
229 pht('MISMATCH'),
230 pht(
231 'File "%s" has an invalid integrity hash!',
232 $display_name));
233 }
234
235 continue;
236 }
237
238 if ($is_compute) {
239 if ($old_hash !== null) {
240 $this->logInfo(
241 pht('SKIP'),
242 pht(
243 'File "%s" already has an integrity hash.',
244 $display_name));
245 } else if ($new_hash === null) {
246 $this->logInfo(
247 pht('IGNORED'),
248 pht(
249 'Storage for file "%s" does not support integrity hashing.',
250 $display_name));
251 } else {
252 $file
253 ->setIntegrityHash($new_hash)
254 ->save();
255
256 $this->logOkay(
257 pht('COMPUTE'),
258 pht(
259 'Computed and stored integrity hash for file "%s".',
260 $display_name));
261 }
262
263 continue;
264 }
265
266 if ($is_overwrite) {
267 $same_hash = ($old_hash !== null) &&
268 ($new_hash !== null) &&
269 phutil_hashes_are_identical($old_hash, $new_hash);
270
271 if ($new_hash === null) {
272 $this->logInfo(
273 pht('IGNORED'),
274 pht(
275 'Storage for file "%s" does not support integrity hashing.',
276 $display_name));
277 } else if ($same_hash) {
278 $this->logInfo(
279 pht('UNCHANGED'),
280 pht(
281 'File "%s" already has the correct integrity hash.',
282 $display_name));
283 } else {
284 $file
285 ->setIntegrityHash($new_hash)
286 ->save();
287
288 $this->logOkay(
289 pht('OVERWRITE'),
290 pht(
291 'Overwrote integrity hash for file "%s".',
292 $display_name));
293 }
294
295 continue;
296 }
297 }
298
299 if ($failure_count) {
300 $this->logFail(
301 pht('FAIL'),
302 pht(
303 'Processed %s file(s), encountered %s error(s).',
304 new PhutilNumber($total_count),
305 new PhutilNumber($failure_count)));
306 } else {
307 $this->logOkay(
308 pht('DONE'),
309 pht(
310 'Processed %s file(s) with no errors.',
311 new PhutilNumber($total_count)));
312 }
313
314 return 0;
315 }
316
317}