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

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

Fix access control errors with results that are part of public galleries

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', 'Format');
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.