source: trunk/server/www/app/controllers/jobs_controller.php @ 291

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

Trach how long Factories have been active

File size: 16.4 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
21// We need to access the Request model statically for it's state constants
22App::import('Model', 'Request');
23
24/**
25 * The jobs controller
26 */
27class JobsController extends AppController
28{
29        /** @var array The components this controller uses */
30        public $components = array('AuthCert');
31       
32        /** @var array The helpers that will be available on the view */
33        public $helpers = array('Html', 'Form', 'JobModel');
34
35        /** @var array The models used by this controller */
36        public $uses = array('Job', 'Factory', 'Request');
37
38        /**
39         * You can't index jobs. Redirect to requests.
40         * @return void
41         */
42        public function index()
43        {
44                $this->redirect(array('controller' => 'requests', 'action' => 'index'));
45        }
46
47        /**
48         * View one of your own jobs
49         *
50         * @param string $id The job ID
51         * @return void
52         */
53        public function view($id = null)
54        {
55                if (!$id) {
56                        $this->Session->setFlash(__('Invalid Job.', true));
57                        $this->redirect(array('controller' => 'requests', 'action'=>'index'));
58                }
59               
60                $this->Job->contain(array(
61                        'Request',
62                        'Request.Gallery',
63                        'Platform',
64                        'Application',
65                        'Factory',
66                        'Result',
67                ));
68                $job = $this->Job->read(null, $id);
69               
70                $isValid = (
71                        !empty($job['Request']['Gallery']) ||
72                        $job['Request']['user_id'] == $this->AuthCert->user('id')
73                );
74
75                if (!$isValid) {
76                        $this->Session->setFlash(__('Invalid Job.', true));
77                        $this->redirect(array('controller' => 'requests', 'action'=>'index'));
78                }
79
80                $this->set('job', $job);
81        }
82
83        /**
84         * List all jobs
85         * @return void
86         */
87        public function admin_index()
88        {
89                $this->Job->recursive = 0;
90                $this->set('jobs', $this->paginate());
91        }
92
93        /**
94         * View a single job
95         *
96         * @param string $id The job ID
97         * @return void
98         */
99        public function admin_view($id = null)
100        {
101                if (!$id) {
102                        $this->Session->setFlash(__('Invalid Job.', true));
103                        $this->redirect(array('action'=>'index'));
104                }
105                $this->set('job', $this->Job->read(null, $id));
106        }
107
108        /**
109         * Add a new job manually
110         * @return void
111         */
112        public function admin_add()
113        {
114                if (!empty($this->data)) {
115                        $this->Job->create();
116                        if ($this->Job->save($this->data)) {
117                                $this->Session->setFlash(__('The Job has been saved', true));
118                                $this->redirect(array('action'=>'index'));
119                        } else {
120                                $this->Session->setFlash(__('The Job could not be saved. Please, try again.', true));
121                        }
122                }
123                $requests = $this->Job->Request->find('list');
124                $platforms = $this->Job->Platform->find('list');
125                $applications = $this->Job->Application->find('list');
126                $factories = $this->Job->Factory->find('list');
127                $results = $this->Job->Result->find('list');
128                $this->set(compact('requests', 'platforms', 'applications', 'factories', 'results'));
129        }
130
131        /**
132         * Edit a job
133         *
134         * @param string $id The job ID
135         * @return void
136         */
137        public function admin_edit($id = null)
138        {
139                if (!$id && empty($this->data)) {
140                        $this->Session->setFlash(__('Invalid Job', true));
141                        $this->redirect(array('action'=>'index'));
142                }
143                if (!empty($this->data)) {
144                        if ($this->Job->save($this->data)) {
145                                $this->Session->setFlash(__('The Job has been saved', true));
146                                $this->redirect(array('action'=>'index'));
147                        } else {
148                                $this->Session->setFlash(__('The Job could not be saved. Please, try again.', true));
149                        }
150                }
151                if (empty($this->data)) {
152                        $this->data = $this->Job->read(null, $id);
153                }
154                $requests = $this->Job->Request->find('list');
155                $platforms = $this->Job->Platform->find('list');
156                $applications = $this->Job->Application->find('list');
157                $factories = $this->Job->Factory->find('list');
158                $results = $this->Job->Result->find('list');
159                $this->set(compact('requests', 'platforms', 'applications', 'factories', 'results'));
160        }
161
162        /**
163         * Delete a job
164         *
165         * @param string $id The job ID
166         * @return void
167         */
168        public function admin_delete($id = null)
169        {
170                if (!$id) {
171                        $this->Session->setFlash(__('Invalid id for Job', true));
172                        $this->redirect(array('action'=>'index'));
173                }
174                if ($this->Job->del($id)) {
175                        $this->Session->setFlash(__('Job deleted', true));
176                        $this->redirect(array('action'=>'index'));
177                }
178        }
179
180        /**
181         * Find a job that matches any of the applications on a given factory and lock it
182         *
183         * Signature
184         * ~~~~~~~~~
185         * jobs.poll(factory_name)
186         *
187         * Arguments
188         * ~~~~~~~~~
189         * - factory_name (string): The name of the factory that you want to poll for
190         *
191         * Return value
192         * ~~~~~~~~~~~~
193         * When a job is found that matches any of the workers that are running on the
194         * requested factory then an array will be returned containing the following fields:
195         *
196         * - job (string): The GUID of the job
197         * - application (string): The name of the application to run, e.g. "OpenOffice.org Writer"
198         * - version (string): The version string of the application, e.g. "3.0" or "2003 SP2"
199         * - pageStart (int): The first page to render in the output
200         * - pageEnd (int): The last page to render in the output. If this is zero then it should be
201         *                  rendered to the end of the document.
202         * - format (string): 3-letter code specifying the desired output format (pdf, png or odf). If this
203         *                    is empty then the factory can decide.
204         * - filename (string): The original filename of the ODF document
205         * - doctype (string): 3-letter code specifying the input type (odt, ods, odp)
206         * - document (string): Base64-encoded contents of the document
207         *
208         * When no mathcing job has been found then an empty result is returned.
209         *
210         * Job locking
211         * ~~~~~~~~~~~
212         * The matching job is locked for five minutes. This is to make sure that no jobs are processed by two factories at
213         * the same time. If your factory takes longer to process a job, it is possible that somebody else will lock it. In
214         * this case, your upload will fail.
215         *
216         * Example request
217         * ~~~~~~~~~~~~~~~
218         * POST /xmlrpc HTTP/1.0
219         * Host: example.org
220         * Content-Type: text/xml
221         * Content-Length: 193
222         *
223         * <?xml version='1.0'?>
224         * <methodCall>
225         *   <methodName>jobs.finish</methodName>
226         *   <params>
227         *     <param><value><string>My factory</string></value></param>
228         *   </params>
229         * </methodCall>
230         *
231         * Example response
232         * ~~~~~~~~~~~~~~~~
233         * HTTP/1.1 200 OK
234         * Content-Type: application/xml
235         * Content-Length: 10164
236         * Connection: close
237         *
238         * <?xml version="1.0" encoding="iso-8859-1"?>
239         * <methodResponse>
240         *   <params>
241         *     <param>
242         *       <value>
243         *         <struct>
244         *           <member>
245         *             <name>job</name>
246         *             <value><string>4975c9c6-79a0-43a1-8134-0ba5c0a80105</string></value>
247         *           </member>
248         *           <member>
249         *             <name>application</name>
250         *             <value><string>OpenOffice.org Writer</string></value>
251         *           </member>
252         *           <member>
253         *             <name>version</name>
254         *             <value><string>3.0</string></value>
255         *           </member>
256         *           <member>
257         *             <name>pageStart</name>
258         *             <value><string>1</string></value>
259         *           </member>
260         *           <member>
261         *             <name>pageEnd</name>
262         *             <value><string>0</string></value>
263         *           </member>
264         *           <member>
265         *             <name>format</name>
266         *             <value><string>pdf</string></value>
267         *           </member>
268         *           <member>
269         *             <name>filename</name>
270         *             <value><string>mydocument.odt</string></value>
271         *           </member>
272         *           <member>
273         *             <name>doctype</name>
274         *             <value><string>odt</string></value>
275         *           </member>
276         *           <member>
277         *             <name>document</name>
278         *             <value><string>UEsDBBQA ... AAAAAA==</string></value>
279         *           </member>
280         *         </struct>
281         *       </value>
282         *     <param>
283         *   </params>
284         * </methodResponse>
285         *
286         * @param string $method The XMLRPC method name
287         * @param mixed $params The XMLRPC parameters
288         * @param mixed $userdata Data passed from the XMLRPC server
289         * @return array The XMLRPC result or a fault array
290         */
291        public function xmlrpc_poll($method, $params, $userdata = null)
292        {
293                if (!$factory = $this->Factory->findFactory($this->AuthCert->user('id'), $params[0])) {
294                        return array('faultCode' => 1, 'faultString' => 'Factory does not exist. Possible SSL authentication failure.');
295                }
296
297                $this->Factory->id = $factory['Factory']['id'];
298                $this->Factory->poll();
299
300                $jobs = array();
301                foreach ($factory['Worker'] as $worker) {
302                        $formats = array_merge(array(''), Set::extract('/Format/id', $worker));
303                        $doctypes = array_merge(array(''), Set::extract('/Application/Doctype/id', $worker));
304                        foreach ($formats as &$format) { $format = "'" . $format . "'"; }
305                        foreach ($doctypes as &$doctype) { $doctype = "'" . $doctype . "'"; }
306
307                        $jobSet = $this->Job->query("SELECT
308                                        `Job`.`id`,
309                                        `Job`.`version`,
310                                        `Application`.`name`,
311                                        `Request`.`id`,
312                                        `Request`.`page_start`,
313                                        `Request`.`page_end`,
314                                        `Request`.`filename`,
315                                        `Request`.`created`,
316                                        `Request`.`priority`,
317                                        `Request`.`own_factory`,
318                                        `Request`.`path`,
319                                        `Format`.`code`,
320                                        `Doctype`.`code`
321                                FROM `jobs` as `Job`
322                                        LEFT JOIN `requests` AS `Request` ON (`Job`.`request_id` = `Request`.`id`)
323                                        LEFT JOIN `applications` AS `Application` ON (`Job`.`application_id` = `Application`.`id`)
324                                        LEFT JOIN `formats` AS `Format` ON (`Request`.`format_id` = `Format`.`id`)
325                                        LEFT JOIN `mimetypes` AS `Mimetype` on (`Request`.`mimetype_id` = `Mimetype`.`id`)
326                                        LEFT JOIN `doctypes` AS `Doctype` on (`Mimetype`.`doctype_id` = `Doctype`.`id`)
327                                WHERE `Job`.`result_id` = ''
328                                        AND `Job`.`locked` < '" . date('Y-m-d H:i:s') . "'
329                                        AND `Job`.`platform_id` = '" . $factory['Operatingsystem']['platform_id'] . "'
330                                        AND `Job`.`application_id` = '" . $worker['application_id'] . "'
331                                        AND `Job`.`version` = '" . $worker['version'] . "'
332                                        AND `Request`.`state` = " . Request::STATE_QUEUED . "
333                                        AND `Request`.`expire` > '" . date('Y-m-d H:i:s') . "'
334                                        AND `Mimetype`.`doctype_id` IN (" . implode(', ', $doctypes) . ")
335                                        AND `Request`.`format_id` IN  (" . implode(', ', $formats) . ")
336                                        AND (`Request`.`own_factory` = 0
337                                                OR (`Request`.`own_factory` = 1 AND `Request`.`user_id` = '" . $this->AuthCert->user('id') . "')
338                                        )");
339                        $jobs = array_merge($jobs, $jobSet);
340                }
341
342                if (sizeof($jobs) == 0) {
343                        return null;
344                }
345
346                $jobs = Set::sort($jobs, '{n}.Request.created', SORT_ASC);
347                $jobs = Set::sort($jobs, '{n}.Request.priority', SORT_ASC);
348                $jobs = Set::sort($jobs, '{n}.Request.own_factory', SORT_DESC);
349                $job = array_shift($jobs);
350
351                $this->Request->id = $job['Request']['id'];
352                $file = $this->Request->getPath();
353                if (!file_exists($file) || !is_readable($file)) {
354                        // Something went wrong. Expire the entire request
355                        $this->Request->saveField('expire', date('Y-m-d H:i:s', time() -1));
356                        return array('faultCode' => 2, 'faultString' => 'Job document could not be read. Please poll again.');
357                }
358
359                $job['Job']['locked'] = date('Y-m-d H:i:s', time() + Configure::read('Job.locktime'));
360                $job['Job']['factory_id'] = $factory['Factory']['id'];
361                $this->Job->save($job);
362
363                $result = array(
364                        'job' => $job['Job']['id'],
365                        'application' => $job['Application']['name'],
366                        'version' => $job['Job']['version'],
367                        'pageStart' => $job['Request']['page_start'],
368                        'pageEnd' => $job['Request']['page_end'],
369                        'format' => ($job['Format'] ? $job['Format']['code'] : ''),
370                        'filename' => $job['Request']['filename'],
371                        'doctype' => $job['Doctype']['code'],
372                        'document' => base64_encode(file_get_contents($file))
373                );
374
375                return $result;
376        }
377
378        /**
379         * Finish a job by uploading the result
380         *
381         * Signature
382         * ~~~~~~~~~
383         * jobs.finish(factory_name, job, format, document)
384         *
385         * Arguments
386         * ~~~~~~~~~
387         * - factory_name (string): The name of the factory that processed the job.
388         * - job (string): The job ID that was returned by jobs.poll().
389         * - format (string): 3-letter code specifying the format of the result (pdf, png or odf).
390         * - document (string): Base64-encoded contents of the result document.
391         *
392         * Return value
393         * ~~~~~~~~~~~~
394         * - result (string): The ID of the result
395         *
396         * Example request
397         * ~~~~~~~~~~~~~~~
398         * POST /xmlrpc HTTP/1.0
399         * Host: example.org
400         * Content-Type: text/xml
401         * Content-Length: 10162
402         *
403         * <?xml version='1.0'?>
404         * <methodCall>
405         *   <methodName>jobs.finish</methodName>
406         *   <params>
407         *     <param><value><string>My factory</string></value></param>
408         *     <param><value><string>4975c9c6-79a0-43a1-8134-0ba5c0a80105</string></value></param>
409         *     <param><value><string>pdf</string></value></param>
410         *     <params><value><string>UEsDBBQA ... AAAAAA==</string></value></params>
411         *   </params>
412         * </methodCall>
413         *
414         * Example response
415         * ~~~~~~~~~~~~~~~~
416         * HTTP/1.1 200 OK
417         * Content-Type: application/xml
418         * Content-Length: 195
419         * Connection: close
420         *
421         * <?xml version="1.0" encoding="iso-8859-1"?>
422         * <methodResponse>
423         *   <params>
424         *     <param><value><string>497d9737-a2cc-4682-9d90-0136c0a80105</string></value></param>
425         *   </params>
426         * </methodResponse>
427         *
428         * @param string $method The XMLRPC method name
429         * @param mixed &$params The XMLRPC parameters
430         * @param mixed $userdata Data passed from the XMLRPC server
431         * @return array The XMLRPC result or a fault array
432         */
433        public function xmlrpc_finish($method, &$params, $userdata = null)
434        {
435                if (sizeof($params) != 4) {
436                        return array('faultCode' => 1, 'faultString' => 'Wrong parameter count.');
437                }
438                list($factory_name, $job_id, $format_code) = $params;
439
440                if (!$factory = $this->Factory->findFactory($this->AuthCert->user('id'), $factory_name)) {
441                        return array('faultCode' => 1, 'faultString' => 'Factory does not exist.');
442                }
443
444                // Check the job
445                $job = $this->Job->find('first', array(
446                        'conditions' => array('Job.id' => $job_id),
447                        'contain' => array(
448                                'Request',
449                                'Request.Format',
450                                'Application',
451                                'Platform',
452                        )
453                ));
454
455                if (!$job) {
456                        return array('faultCode' => 1, 'faultString' => 'Job does not exist.');
457                }
458
459                if ($job['Job']['factory_id'] != $factory['Factory']['id']) {
460                        return array('faultCode' => 1, 'faultString' => 'Job lock expired and the job is taken by another factory.');
461                }
462
463                // Check the return format
464                $format = $this->Job->Request->Format->find('first', array(
465                        'recursive' => -1,
466                        'conditions' => array('Format.code' => $format_code),
467                ));
468
469                if (!$format || ($job['Request']['format_id'] && $job['Request']['Format']['code'] != $format_code)) {
470                        return array('faultCode' => 1, 'faultString' => 'Wrong document format.');
471                }
472
473                // TODO: The file extension stuff is an ugly kludge. It should do this through the Mimetype model
474                $basename = basename($job['Request']['filename']);
475                $basename = substr($basename, 0, strrpos($basename, '.'));
476                if ($format_code == 'odf') {
477                        $filename = $basename . strrchr($job['Request']['filename'], '.');
478                } else {
479                        $filename = $basename . '.' . $format_code;
480                }
481
482                // Find out where to store the result
483                $path  = $job['Request']['root'] . DS;
484                $path .= Inflector::slug($job['Application']['name']) . '_';
485                $path .= Inflector::slug($job['Job']['version']) . '_';
486                $path .= Inflector::slug($job['Platform']['name']);
487
488                // Create the Result
489                $this->Job->Result->create();
490                $this->Job->Result->set(array(
491                        'format_id'  => $format['Format']['id'],
492                        'factory_id' => $factory['Factory']['id'],
493                        'path' => $path
494                ));
495
496                if (!$this->Job->Result->setFileBuffer($params[3], $filename, 'convert.base64-decode')) {
497                        $errors = $this->Job->Result->Behaviors->File->errors;
498                        array_unshift($errors, 'The result file could not be stored');
499                        return array('faultCode' => 1, 'faultString' => implode("\n", $errors));
500                }
501
502                if (!$this->Job->Result->save()) {
503                        return array('faultCode' => 1, 'faultString' => 'The result could not be saved');
504                }
505
506                // Update the Job with the new Result
507                $this->Job->id = $job['Job']['id'];
508                $this->Job->save(array('result_id' => $this->Job->Result->id));
509
510                return $this->Job->Result->id;
511        }
512}
513
514?>
Note: See TracBrowser for help on using the repository browser.