source: trunk/server/www/app/models/request.php @ 297

Last change on this file since 297 was 297, checked in by sander, 11 years ago

Generate and display jobs and results for test suites

File size: 12.3 KB
Line 
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
21App::import('Core', 'Clamd.Clamd');
22App::import('Behavior', 'Pipeline');
23
24/**
25 * The Request model
26 */
27class 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                return $jobCount;
217        }
218
219        /**
220         * Add ODF validators for the request
221         */
222        public function addValidators()
223        {
224                // First remove any existing validators
225                $this->Validator->deleteAll(array(
226                        'Validator.parent_id' => $this->id,
227                ));
228
229                // Add new validators
230                $validators = Configure::read('Validator');
231                foreach ($validators as $validator_name => $validator_config) {
232                        if (!$validator_config) {
233                                continue;
234                        }
235
236                        $this->Validator->create();
237                        $this->Validator->save(array('Validator' => array(
238                                'name' => $validator_name,
239                                'parent_id' => $this->id,
240                        )));
241                }
242        }
243
244        /**
245         * Cancel the request
246         *
247         * @return void
248         */
249        public function cancel()
250        {
251                if (!$this->id) {
252                        return;
253                }
254
255                $this->save(array('Request' => array(
256                        'state' => self::STATE_CANCELLED,
257                        'expire' => date('Y-m-d H:i:s', time() - 1)
258                )));
259        }
260
261        /**
262         * Set Expired state on all expired requests
263         *
264         * @return boolean Success
265         */
266        public function expireAll()
267        {
268                return $this->updateAll(
269                        array('Request.state' => self::STATE_EXPIRED),
270                        array(
271                                'Request.expire <=' => date('Y-m-d H:i:s'),
272                                'Request.state' => array(self::STATE_UPLOADING, self::STATE_PREPROCESSOR_QUEUED, self::STATE_QUEUED)
273                        )
274                );
275        }
276
277        /**
278         * Delete all jobs and results belonging to the request
279         */
280        public function deleteJobs($id = null)
281        {
282                if (!$id) {
283                        $id = $this->id;
284                }
285
286                if (!$id) {
287                        return;
288                }
289
290                $request = $this->find('first', array(
291                        'contain' => array('Job'),
292                        'conditions' => array('Request.id' => $id),
293                ));
294
295                if (!$request || !isset($request['Job']) || empty($request['Job'])) {
296                        return;
297                }
298
299                foreach ($request['Job'] as $job) {
300                        $this->Job->delete($job['id']); // Results are deleted in Job::beforeDelete
301                }
302        }
303
304        /**
305         * Generate a zipfile containing the original request and all results
306         *
307         * @return array an array containing the directory, id and filename of the zipfile to be used by the media view, or null
308         */
309        public function createZip()
310        {
311                if (!isset($this->id)) {
312                        return null;
313                }
314
315                $root = FILES . $this->field('root');
316                $filename = $this->field('filename');
317                $filename = substr($filename, 0, strrpos($filename, '.'));
318                $directory = TMP . 'zips';
319                $id = $this->id . '.zip';
320
321                if (file_exists($directory . DS . $id)) {
322                        unlink($directory . DS . $id);
323                }
324
325                $cwd = getcwd();
326                chdir($root);
327                shell_exec('cd "' . $root . '" && zip -r "' . $directory . DS . $id . '" .');
328                chdir($cwd);
329
330                return compact('id', 'directory', 'filename');
331        }
332
333        /**
334         * Convert the Markdown description to HTML before saving
335         * @return boolean True to continue saving
336         */
337        public function beforeSave()
338        {
339                if (isset($this->data['Request']['description'])) {
340                        App::import('Vendor', 'markdown');
341                        App::import('Vendor', 'HTMLPurifier', array('file' => 'htmlpurifier/HTMLPurifier.standalone.php'));
342
343                        $config = HTMLPurifier_Config::createDefault();
344                        $config->set('Cache.SerializerPath', CACHE . DS . 'htmlpurifier');
345                        $purifier = new HTMLPurifier($config);
346
347                        $desc_html = Markdown($this->data['Request']['description']);
348                        $this->data['Request']['description_html'] = $purifier->purify($desc_html);
349                }
350
351                return true;
352        }
353
354        /**
355         * Scan this request for viruses
356         * This function is called from the Preprocessor pipeline
357         * @return True to continue the pipeline, False to switch the pipeline to errback
358         */
359        public function scan()
360        {
361                $clamd_config = Configure::read('Clamd');
362                if (!$clamd_config) {
363                        $this->log('Calmd not configued. Skipping scan...', LOG_DEBUG);
364                        return true; // Continue with the rest of the pipeline
365                }
366
367                $this->contain();
368                $this->read();
369
370                $clamd = new Clamd($clamd_config);
371                $path = $this->getPath();
372                $result = '';
373
374                if (!$path) {
375                        $this->log('Request could not be scanned. ' . $path . ' (Request ID: ' . $this->id . ') does not exists. ');
376                        return false;
377                }
378
379                $status = $clamd->scan($path, $result);
380                if ($status == Clamd::OK) {
381                        // Scan OK. Queue the Request for processing
382                        $this->log('Clamd scanned ' . $path . ' (Request ID: ' . $this->id . '): OK', LOG_DEBUG);
383                } elseif ( $status == Clamd::FOUND ) {
384                        // There was a virus. Remove all jobs and alert user
385                        // Note that the file is *not* deleted. This was we can later check if there really was a virus
386                        $this->Job->deleteAll(array('Job.request_id' => $this->id));
387                        $this->data['Request']['state'] = self::STATE_SCAN_FOUND;
388                        $this->data['Request']['state_info'] = $result;
389                        $this->data['Request']['expire'] = date('Y-m-d H:i:s');
390                        $this->log('Clamd scanned ' . $path . ' (Request ID: ' . $this->id . '): FOUND ' . $result, LOG_DEBUG);
391                        return false;
392                } else {
393                        // There was an error. Remove all jobs and alert user
394                        if ($status === false) {
395                                $result = $clamd->lastError();
396                                $status = Clamd::ERROR;
397                        }
398
399                        $this->Job->deleteAll(array('Job.request_id' => $this->id));
400                        $this->data['Request']['state_info'] = $result;
401                        $this->data['Request']['expire'] = date('Y-m-d H:i:s');
402
403                        $this->log('Clamd error scanning ' . $path . ' (Request ID: ' . $this->id . '): ' . $result);
404                        return false;
405                }
406
407                return $this->save();
408        }
409
410        /**
411         * Run all the ODF validators associated with this request
412         */
413        public function validateFile()
414        {
415                $this->contain('Validator');
416                $this->read();
417
418                $this->errors = array();
419                if (!is_array($this->data['Validator'])) {
420                        $this->log('No validators found for Request: ' . $this->id, LOG_DEBUG);
421                        return true;
422                }
423
424                foreach ($this->data['Validator'] as $validator) {
425                        $this->Validator->id = $validator['id'];
426                        $this->Validator->run();
427                }
428
429                return true; // To continue the rest of the pipeline
430        }
431
432        /**
433         * Run the request through the iTools greeking utility
434         * @return boolean Success
435         */
436        public function anonymise()
437        {
438                $bin = Configure::read('Anonymiser.path');
439                if (!$bin || !is_executable($bin)) {
440                        $this->log('Anonymiser not configured');
441                        return false;
442                }
443
444                $path = $this->getPath();
445                $this->log('Anonymiser path: ' . $path, LOG_DEBUG);
446                if (!is_readable($path) || !is_writeable($path)) {
447                        $this->log('Anonymiser target file not readable or writeable');
448                        return false;
449                }
450
451                $spec = array(
452                        0 => array('pipe', 'r'), // STDIN
453                        1 => array('pipe', 'w'), // STDOUT
454                        2 => array('pipe', 'w')  // STDERR
455                );
456
457                $proc = proc_open($bin . ' -o ' . $path . ' ' . $path, $spec, $pipes);
458                if (!is_resource($proc)) {
459                        $this->log('Anonymiser failed to run');
460                        return false;
461                }
462
463                $stdout = stream_get_contents($pipes[1]);
464                $stderr = stream_get_contents($pipes[2]);
465                fclose($pipes[0]);
466                fclose($pipes[1]);
467                fclose($pipes[2]);
468                proc_close($proc);
469
470                $this->log('Anonymiser STDOUT: ' . $stdout, LOG_DEBUG);
471                $this->log('Anonymiser STDERR: ' . $stderr, LOG_DEBUG);
472
473                if (trim($stderr)) {
474                        return false;
475                }
476
477                return true;
478        }
479}
480
481?>
Note: See TracBrowser for help on using the repository browser.