1 | <?php |
---|
2 | /** |
---|
3 | * Officeshots.org - Test your office documents in different applications |
---|
4 | * Copyright (C) 2009 Stichting Lone Wolves |
---|
5 | * Written by Sander Marechal <s.marechal@jejik.com> |
---|
6 | * |
---|
7 | * This program is free software: you can redistribute it and/or modify |
---|
8 | * it under the terms of the GNU Affero General Public License as |
---|
9 | * published by the Free Software Foundation, either version 3 of the |
---|
10 | * License, or (at your option) any later version. |
---|
11 | * |
---|
12 | * This program is distributed in the hope that it will be useful, |
---|
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
---|
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
---|
15 | * GNU Affero General Public License for more details. |
---|
16 | * |
---|
17 | * You should have received a copy of the GNU Affero General Public License |
---|
18 | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
---|
19 | */ |
---|
20 | |
---|
21 | App::import('Core', 'Clamd.Clamd'); |
---|
22 | App::import('Behavior', 'Pipeline'); |
---|
23 | |
---|
24 | /** |
---|
25 | * The Request model |
---|
26 | */ |
---|
27 | class Request extends AppModel |
---|
28 | { |
---|
29 | /**#@+ |
---|
30 | * Request states |
---|
31 | */ |
---|
32 | const STATE_UPLOADING = 1; |
---|
33 | const STATE_PREPROCESSOR_QUEUED = 2; |
---|
34 | const STATE_SCAN_FOUND = 4; |
---|
35 | const STATE_PREPROCESSOR_FAILED = 8; |
---|
36 | const STATE_QUEUED = 16; |
---|
37 | const STATE_FINISHED = 32; |
---|
38 | const STATE_EXPIRED = 64; |
---|
39 | const STATE_CANCELLED = 128; |
---|
40 | /**#@-*/ |
---|
41 | |
---|
42 | /** @var array Every request belongs to a user and is associated with a document type and a desired output format */ |
---|
43 | public $belongsTo = array('User'); |
---|
44 | |
---|
45 | /** @var string A request consists of multiple jobs and has multiple ODF validators */ |
---|
46 | public $hasMany = array('Job', 'Validator' => array('foreignKey' => 'parent_id')); |
---|
47 | |
---|
48 | /** @var array Requests can belong to multiple galleries */ |
---|
49 | public $hasAndBelongsToMany = array('Gallery' => array('unique' => false)); |
---|
50 | |
---|
51 | /** @var array Use the file behaviour to associate ODF files with Requests */ |
---|
52 | public $actsAs = array( |
---|
53 | 'Containable', |
---|
54 | 'BeanStalk.Deferrable', |
---|
55 | 'File', |
---|
56 | 'Pipeline' => 'Preprocessor', |
---|
57 | ); |
---|
58 | |
---|
59 | /** @var string Use the filename as the distinguising name */ |
---|
60 | public $displayField = 'filename'; |
---|
61 | |
---|
62 | /** |
---|
63 | * Constructor |
---|
64 | * Build the pipelines |
---|
65 | */ |
---|
66 | public function __construct() |
---|
67 | { |
---|
68 | parent::__construct(); |
---|
69 | |
---|
70 | $this->Preprocessor->callback('scan'); |
---|
71 | $this->Preprocessor->callback('validateFile'); |
---|
72 | $this->Preprocessor->callback('saveField', 'state', self::STATE_QUEUED); |
---|
73 | $this->Preprocessor->errback('saveField', 'state', self::STATE_PREPROCESSOR_FAILED); |
---|
74 | } |
---|
75 | |
---|
76 | /** |
---|
77 | * Check access control for this request |
---|
78 | * @param string $user_id The user ID |
---|
79 | * @param string $type "read" or "write" |
---|
80 | * @param string $id The request ID |
---|
81 | * @return boolean True or False |
---|
82 | */ |
---|
83 | public function checkAccess($user_id, $type = 'read', $id = false) |
---|
84 | { |
---|
85 | if (!$id) { |
---|
86 | $id = $this->id; |
---|
87 | } |
---|
88 | |
---|
89 | if (!$id) { |
---|
90 | return false; |
---|
91 | } |
---|
92 | |
---|
93 | $request = $this->find('first', array( |
---|
94 | 'contain' => array('Gallery'), |
---|
95 | 'conditions' => array('Request.id' => $id), |
---|
96 | )); |
---|
97 | |
---|
98 | if ($request['Request']['user_id'] == $user_id) { |
---|
99 | return true; |
---|
100 | } |
---|
101 | |
---|
102 | if (isset($request['Gallery']) && !empty($request['Gallery'])) { |
---|
103 | foreach ($request['Gallery'] as $gallery) { |
---|
104 | if ($this->Gallery->checkAccess($user_id, $gallery['id'])) { |
---|
105 | return true; |
---|
106 | } |
---|
107 | } |
---|
108 | |
---|
109 | if ($type == 'read') { |
---|
110 | return true; |
---|
111 | } |
---|
112 | } |
---|
113 | |
---|
114 | if (Configure::read('Auth.allowAnonymous') && empty($request['Request']['user_id'])) { |
---|
115 | return true; |
---|
116 | } |
---|
117 | |
---|
118 | return false; |
---|
119 | } |
---|
120 | |
---|
121 | /** |
---|
122 | * Add an upload to the request |
---|
123 | * |
---|
124 | * If the add fails, &$errors contains the error messages |
---|
125 | * |
---|
126 | * @param array $data The data to save if not saving $this->data, including a file and the jobs |
---|
127 | * @param array &$errors The errors |
---|
128 | * @return boolean Success |
---|
129 | */ |
---|
130 | public function addUpload($data, $anonymise = false, &$errors = array()) |
---|
131 | { |
---|
132 | if (empty($data)) { |
---|
133 | $errors[] = __('The request was empty.', true); |
---|
134 | return false; |
---|
135 | } |
---|
136 | |
---|
137 | // Check that we have a file and jobs |
---|
138 | if ((!isset($data['Request']['FileUpload']) || !is_array($data['Request']['FileUpload']))) { |
---|
139 | $errors[] = __('You did not submit a file.', true); |
---|
140 | return false; |
---|
141 | } |
---|
142 | |
---|
143 | if (!$this->setFileUpload($data['Request']['FileUpload'])) { |
---|
144 | $errors[] = __('The file upload failed. Please try again.', true); |
---|
145 | $errors = array_merge($errors, $this->Behaviors->File->errors); |
---|
146 | return false; |
---|
147 | } |
---|
148 | |
---|
149 | // Run the ODF anonymiser if requested |
---|
150 | if ($data['Request']['anonymise']) { |
---|
151 | if (!$this->anonymise()) { |
---|
152 | $this->deleteFile(); |
---|
153 | $errors[] = __('The Anonymiser failed to run', true); |
---|
154 | return false; |
---|
155 | } |
---|
156 | } |
---|
157 | unset($data['Request']['anonymise']); |
---|
158 | |
---|
159 | return $this->save(); |
---|
160 | } |
---|
161 | |
---|
162 | /** |
---|
163 | * Add jobs to this request |
---|
164 | * |
---|
165 | * @param array $jobs An array of jobs |
---|
166 | * @param string $id The request ID |
---|
167 | * @return int The number of jobs created |
---|
168 | */ |
---|
169 | public function addJobs($jobs = array(), $id = null) |
---|
170 | { |
---|
171 | // Load the Application model. We need that below. |
---|
172 | $Application = ClassRegistry::init('Application'); |
---|
173 | |
---|
174 | // Load the request |
---|
175 | if (!$id) { |
---|
176 | $id = $this->id; |
---|
177 | } |
---|
178 | |
---|
179 | if (!$id) { |
---|
180 | return 0; |
---|
181 | } |
---|
182 | |
---|
183 | $request = $this->find('first', array( |
---|
184 | 'conditions' => array('Request.id' => $id), |
---|
185 | 'recursive' => -1, |
---|
186 | )); |
---|
187 | |
---|
188 | // Find the Mimetype associated with the request |
---|
189 | // We can use this to check the doctype on the submitted jobs below |
---|
190 | $mimetype = $this->Mimetype->find('first', array('conditions' => array( |
---|
191 | 'Mimetype.id' => $request['Request']['mimetype_id'], |
---|
192 | ))); |
---|
193 | |
---|
194 | $jobCount = 0; |
---|
195 | foreach ($jobs as $job) { |
---|
196 | $job['request_id'] = $id; |
---|
197 | |
---|
198 | // Check the request mimetype against the supported doctype |
---|
199 | $count = $Application->find('count', array( |
---|
200 | 'contain' => array( |
---|
201 | 'Application.id' => $job['application_id'], |
---|
202 | 'Doctype.code' => $mimetype['Doctype']['code'] |
---|
203 | ), |
---|
204 | )); |
---|
205 | |
---|
206 | if ($count == 0) { |
---|
207 | continue; |
---|
208 | } |
---|
209 | |
---|
210 | $this->Job->create(); |
---|
211 | if ($this->Job->save(array('Job' => $job))) { |
---|
212 | $jobCount++; |
---|
213 | } |
---|
214 | } |
---|
215 | |
---|
216 | // If this request had already finished and isn't expired yet, set it back to queued. |
---|
217 | $requeue = ( |
---|
218 | $request['Request']['state'] == self::STATE_FINISHED |
---|
219 | && ($request['Request']['expire'] == '0000-00-00 00:00:00' || strtotime($request['Request']['expire']) < time()) |
---|
220 | ); |
---|
221 | |
---|
222 | if ($requeue) { |
---|
223 | $this->saveField('state', self::STATE_QUEUED); |
---|
224 | } |
---|
225 | |
---|
226 | return $jobCount; |
---|
227 | } |
---|
228 | |
---|
229 | /** |
---|
230 | * Add ODF validators for the request |
---|
231 | */ |
---|
232 | public function addValidators() |
---|
233 | { |
---|
234 | // First remove any existing validators |
---|
235 | $this->Validator->deleteAll(array( |
---|
236 | 'Validator.parent_id' => $this->id, |
---|
237 | )); |
---|
238 | |
---|
239 | // Add new validators |
---|
240 | $validators = Configure::read('Validator'); |
---|
241 | foreach ($validators as $validator_name => $validator_config) { |
---|
242 | if (!$validator_config) { |
---|
243 | continue; |
---|
244 | } |
---|
245 | |
---|
246 | $this->Validator->create(); |
---|
247 | $this->Validator->save(array('Validator' => array( |
---|
248 | 'name' => $validator_name, |
---|
249 | 'parent_id' => $this->id, |
---|
250 | ))); |
---|
251 | } |
---|
252 | } |
---|
253 | |
---|
254 | /** |
---|
255 | * Cancel the request |
---|
256 | * |
---|
257 | * @return void |
---|
258 | */ |
---|
259 | public function cancel() |
---|
260 | { |
---|
261 | if (!$this->id) { |
---|
262 | return; |
---|
263 | } |
---|
264 | |
---|
265 | $this->save(array('Request' => array( |
---|
266 | 'state' => self::STATE_CANCELLED, |
---|
267 | 'expire' => date('Y-m-d H:i:s', time() - 1) |
---|
268 | ))); |
---|
269 | } |
---|
270 | |
---|
271 | /** |
---|
272 | * Set Expired state on all expired requests |
---|
273 | * |
---|
274 | * @return boolean Success |
---|
275 | */ |
---|
276 | public function expireAll() |
---|
277 | { |
---|
278 | return $this->updateAll( |
---|
279 | array('Request.state' => self::STATE_EXPIRED), |
---|
280 | array( |
---|
281 | 'Request.expire <=' => date('Y-m-d H:i:s'), |
---|
282 | 'Request.state' => array(self::STATE_UPLOADING, self::STATE_PREPROCESSOR_QUEUED, self::STATE_QUEUED) |
---|
283 | ) |
---|
284 | ); |
---|
285 | } |
---|
286 | |
---|
287 | /** |
---|
288 | * Delete all jobs and results belonging to the request |
---|
289 | */ |
---|
290 | public function deleteJobs($id = null) |
---|
291 | { |
---|
292 | if (!$id) { |
---|
293 | $id = $this->id; |
---|
294 | } |
---|
295 | |
---|
296 | if (!$id) { |
---|
297 | return; |
---|
298 | } |
---|
299 | |
---|
300 | $request = $this->find('first', array( |
---|
301 | 'contain' => array('Job'), |
---|
302 | 'conditions' => array('Request.id' => $id), |
---|
303 | )); |
---|
304 | |
---|
305 | if (!$request || !isset($request['Job']) || empty($request['Job'])) { |
---|
306 | return; |
---|
307 | } |
---|
308 | |
---|
309 | foreach ($request['Job'] as $job) { |
---|
310 | $this->Job->delete($job['id']); // Results are deleted in Job::beforeDelete |
---|
311 | } |
---|
312 | } |
---|
313 | |
---|
314 | /** |
---|
315 | * Generate a zipfile containing the original request and all results |
---|
316 | * |
---|
317 | * @return array an array containing the directory, id and filename of the zipfile to be used by the media view, or null |
---|
318 | */ |
---|
319 | public function createZip() |
---|
320 | { |
---|
321 | if (!isset($this->id)) { |
---|
322 | return null; |
---|
323 | } |
---|
324 | |
---|
325 | $root = FILES . $this->field('root'); |
---|
326 | $filename = $this->field('filename'); |
---|
327 | $filename = substr($filename, 0, strrpos($filename, '.')); |
---|
328 | $directory = TMP . 'zips'; |
---|
329 | $id = $this->id . '.zip'; |
---|
330 | |
---|
331 | if (file_exists($directory . DS . $id)) { |
---|
332 | unlink($directory . DS . $id); |
---|
333 | } |
---|
334 | |
---|
335 | $cwd = getcwd(); |
---|
336 | chdir($root); |
---|
337 | shell_exec('cd "' . $root . '" && zip -r "' . $directory . DS . $id . '" .'); |
---|
338 | chdir($cwd); |
---|
339 | |
---|
340 | return compact('id', 'directory', 'filename'); |
---|
341 | } |
---|
342 | |
---|
343 | /** |
---|
344 | * Convert the Markdown description to HTML before saving |
---|
345 | * @return boolean True to continue saving |
---|
346 | */ |
---|
347 | public function beforeSave() |
---|
348 | { |
---|
349 | if (isset($this->data['Request']['description'])) { |
---|
350 | App::import('Vendor', 'markdown'); |
---|
351 | App::import('Vendor', 'HTMLPurifier', array('file' => 'htmlpurifier/HTMLPurifier.standalone.php')); |
---|
352 | |
---|
353 | $config = HTMLPurifier_Config::createDefault(); |
---|
354 | $config->set('Cache.SerializerPath', CACHE . DS . 'htmlpurifier'); |
---|
355 | $purifier = new HTMLPurifier($config); |
---|
356 | |
---|
357 | $desc_html = Markdown($this->data['Request']['description']); |
---|
358 | $this->data['Request']['description_html'] = $purifier->purify($desc_html); |
---|
359 | } |
---|
360 | |
---|
361 | return true; |
---|
362 | } |
---|
363 | |
---|
364 | /** |
---|
365 | * Scan this request for viruses |
---|
366 | * This function is called from the Preprocessor pipeline |
---|
367 | * @return True to continue the pipeline, False to switch the pipeline to errback |
---|
368 | */ |
---|
369 | public function scan() |
---|
370 | { |
---|
371 | $clamd_config = Configure::read('Clamd'); |
---|
372 | if (!$clamd_config) { |
---|
373 | $this->log('Calmd not configued. Skipping scan...', LOG_DEBUG); |
---|
374 | return true; // Continue with the rest of the pipeline |
---|
375 | } |
---|
376 | |
---|
377 | $this->contain(); |
---|
378 | $this->read(); |
---|
379 | |
---|
380 | $clamd = new Clamd($clamd_config); |
---|
381 | $path = $this->getPath(); |
---|
382 | $result = ''; |
---|
383 | |
---|
384 | if (!$path) { |
---|
385 | $this->log('Request could not be scanned. ' . $path . ' (Request ID: ' . $this->id . ') does not exists. '); |
---|
386 | return false; |
---|
387 | } |
---|
388 | |
---|
389 | $status = $clamd->scan($path, $result); |
---|
390 | if ($status == Clamd::OK) { |
---|
391 | // Scan OK. Queue the Request for processing |
---|
392 | $this->log('Clamd scanned ' . $path . ' (Request ID: ' . $this->id . '): OK', LOG_DEBUG); |
---|
393 | } elseif ( $status == Clamd::FOUND ) { |
---|
394 | // There was a virus. Remove all jobs and alert user |
---|
395 | // Note that the file is *not* deleted. This was we can later check if there really was a virus |
---|
396 | $this->Job->deleteAll(array('Job.request_id' => $this->id)); |
---|
397 | $this->data['Request']['state'] = self::STATE_SCAN_FOUND; |
---|
398 | $this->data['Request']['state_info'] = $result; |
---|
399 | $this->data['Request']['expire'] = date('Y-m-d H:i:s'); |
---|
400 | $this->log('Clamd scanned ' . $path . ' (Request ID: ' . $this->id . '): FOUND ' . $result, LOG_DEBUG); |
---|
401 | return false; |
---|
402 | } else { |
---|
403 | // There was an error. Remove all jobs and alert user |
---|
404 | if ($status === false) { |
---|
405 | $result = $clamd->lastError(); |
---|
406 | $status = Clamd::ERROR; |
---|
407 | } |
---|
408 | |
---|
409 | $this->Job->deleteAll(array('Job.request_id' => $this->id)); |
---|
410 | $this->data['Request']['state_info'] = $result; |
---|
411 | $this->data['Request']['expire'] = date('Y-m-d H:i:s'); |
---|
412 | |
---|
413 | $this->log('Clamd error scanning ' . $path . ' (Request ID: ' . $this->id . '): ' . $result); |
---|
414 | return false; |
---|
415 | } |
---|
416 | |
---|
417 | return $this->save(); |
---|
418 | } |
---|
419 | |
---|
420 | /** |
---|
421 | * Run all the ODF validators associated with this request |
---|
422 | */ |
---|
423 | public function validateFile() |
---|
424 | { |
---|
425 | $this->contain('Validator'); |
---|
426 | $this->read(); |
---|
427 | |
---|
428 | $this->errors = array(); |
---|
429 | if (!is_array($this->data['Validator'])) { |
---|
430 | $this->log('No validators found for Request: ' . $this->id, LOG_DEBUG); |
---|
431 | return true; |
---|
432 | } |
---|
433 | |
---|
434 | foreach ($this->data['Validator'] as $validator) { |
---|
435 | $this->Validator->id = $validator['id']; |
---|
436 | $this->Validator->run(); |
---|
437 | } |
---|
438 | |
---|
439 | return true; // To continue the rest of the pipeline |
---|
440 | } |
---|
441 | |
---|
442 | /** |
---|
443 | * Run the request through the iTools greeking utility |
---|
444 | * @return boolean Success |
---|
445 | */ |
---|
446 | public function anonymise() |
---|
447 | { |
---|
448 | $bin = Configure::read('Anonymiser.path'); |
---|
449 | if (!$bin || !is_executable($bin)) { |
---|
450 | $this->log('Anonymiser not configured'); |
---|
451 | return false; |
---|
452 | } |
---|
453 | |
---|
454 | $path = $this->getPath(); |
---|
455 | $this->log('Anonymiser path: ' . $path, LOG_DEBUG); |
---|
456 | if (!is_readable($path) || !is_writeable($path)) { |
---|
457 | $this->log('Anonymiser target file not readable or writeable'); |
---|
458 | return false; |
---|
459 | } |
---|
460 | |
---|
461 | $spec = array( |
---|
462 | 0 => array('pipe', 'r'), // STDIN |
---|
463 | 1 => array('pipe', 'w'), // STDOUT |
---|
464 | 2 => array('pipe', 'w') // STDERR |
---|
465 | ); |
---|
466 | |
---|
467 | $proc = proc_open($bin . ' -o ' . $path . ' ' . $path, $spec, $pipes); |
---|
468 | if (!is_resource($proc)) { |
---|
469 | $this->log('Anonymiser failed to run'); |
---|
470 | return false; |
---|
471 | } |
---|
472 | |
---|
473 | $stdout = stream_get_contents($pipes[1]); |
---|
474 | $stderr = stream_get_contents($pipes[2]); |
---|
475 | fclose($pipes[0]); |
---|
476 | fclose($pipes[1]); |
---|
477 | fclose($pipes[2]); |
---|
478 | proc_close($proc); |
---|
479 | |
---|
480 | $this->log('Anonymiser STDOUT: ' . $stdout, LOG_DEBUG); |
---|
481 | $this->log('Anonymiser STDERR: ' . $stderr, LOG_DEBUG); |
---|
482 | |
---|
483 | if (trim($stderr)) { |
---|
484 | return false; |
---|
485 | } |
---|
486 | |
---|
487 | return true; |
---|
488 | } |
---|
489 | } |
---|
490 | |
---|
491 | ?> |
---|