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

Last change on this file since 295 was 295, checked in by sander, 10 years ago

Link teh requested output formats to individual jobs, not the request as a whole

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