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

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

Fixed a very stupid typo in r298

File size: 12.6 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                // 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?>
Note: See TracBrowser for help on using the repository browser.