]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - modules/SchedulersJobs/SchedulersJob.php
Release 6.5.15
[Github/sugarcrm.git] / modules / SchedulersJobs / SchedulersJob.php
1 <?php
2 if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
3 /*********************************************************************************
4  * SugarCRM Community Edition is a customer relationship management program developed by
5  * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc.
6  * 
7  * This program is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU Affero General Public License version 3 as published by the
9  * Free Software Foundation with the addition of the following permission added
10  * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
11  * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
12  * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
13  * 
14  * This program is distributed in the hope that it will be useful, but WITHOUT
15  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16  * FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
17  * details.
18  * 
19  * You should have received a copy of the GNU Affero General Public License along with
20  * this program; if not, see http://www.gnu.org/licenses or write to the Free
21  * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
22  * 02110-1301 USA.
23  * 
24  * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
25  * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
26  * 
27  * The interactive user interfaces in modified source and object code versions
28  * of this program must display Appropriate Legal Notices, as required under
29  * Section 5 of the GNU Affero General Public License version 3.
30  * 
31  * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
32  * these Appropriate Legal Notices must retain the display of the "Powered by
33  * SugarCRM" logo. If the display of the logo is not reasonably feasible for
34  * technical reasons, the Appropriate Legal Notices must display the words
35  * "Powered by SugarCRM".
36  ********************************************************************************/
37
38
39 /**
40  * Job queue job
41  * @api
42  */
43 class SchedulersJob extends Basic
44 {
45     const JOB_STATUS_QUEUED = 'queued';
46     const JOB_STATUS_RUNNING = 'running';
47     const JOB_STATUS_DONE = 'done';
48
49     const JOB_PENDING = 'queued';
50     const JOB_PARTIAL = 'partial';
51     const JOB_SUCCESS = 'success';
52     const JOB_FAILURE = 'failure';
53
54     // schema attributes
55         public $id;
56         public $name;
57         public $deleted;
58         public $date_entered;
59         public $date_modified;
60         public $scheduler_id;
61         public $execute_time; // when to execute
62     public $status;
63     public $resolution;
64     public $message;
65         public $target; // URL or function name
66     public $data; // Data set
67     public $requeue; // Requeue on failure?
68     public $retry_count;
69     public $failure_count;
70     public $job_delay=0; // Frequency to run it
71     public $assigned_user_id; // User under which the task is running
72     public $client; // Client ID that owns this job
73     public $execute_time_db;
74     public $percent_complete; // how much of the job is done
75
76         // standard SugarBean child attrs
77         var $table_name         = "job_queue";
78         var $object_name                = "SchedulersJob";
79         var $module_dir         = "SchedulersJobs";
80         var $new_schema         = true;
81         var $process_save_dates = true;
82         // related fields
83         var $job_name;  // the Scheduler's 'name' field
84         var $job;               // the Scheduler's 'job' field
85         // object specific attributes
86         public $user; // User object
87         var $scheduler; // Scheduler parent
88         public $min_interval = 30; // minimal interval for job reruns
89         protected $job_done = true;
90     protected $old_user;
91
92         /**
93          * Job constructor.
94          */
95         function SchedulersJob()
96         {
97         parent::Basic();
98         if(!empty($GLOBALS['sugar_config']['jobs']['min_retry_interval'])) {
99             $this->min_interval = $GLOBALS['sugar_config']['jobs']['min_retry_interval'];
100         }
101         }
102
103         public function check_date_relationships_load()
104         {
105         // Hack to work around the mess with dates being auto-converted to user format on retrieve
106             $this->execute_time_db = $this->db->fromConvert($this->execute_time, 'datetime');
107             parent::check_date_relationships_load();
108     }
109         /**
110      * handleDateFormat
111      *
112          * This function handles returning a datetime value.  It allows a user instance to be passed in, but will default to the
113      * user member variable instance if none is found.
114      *
115          * @param string $date String value of the date to calculate, defaults to 'now'
116          * @param object $user The User instance to use in calculating the time value, if empty, it will default to user member variable
117          * @param boolean $user_format Boolean indicating whether or not to convert to user's time format, defaults to false
118      *
119          * @return string Formatted datetime value
120          */
121         function handleDateFormat($date='now', $user=null, $user_format=false) {
122                 global $timedate;
123
124                 if(!isset($timedate) || empty($timedate))
125         {
126                         $timedate = new TimeDate();
127                 }
128
129                 // get user for calculation
130                 $user = (empty($user)) ? $this->user : $user;
131
132         if($date == 'now')
133         {
134             $dbTime = $timedate->asUser($timedate->getNow(), $user);
135         } else {
136             $dbTime = $timedate->asUser($timedate->fromString($date, $user), $user);
137         }
138
139         // if $user_format is set to true then just return as th user's time format, otherwise, return as database format
140         return $user_format ? $dbTime : $timedate->fromUser($dbTime, $user)->asDb();
141         }
142
143
144         ///////////////////////////////////////////////////////////////////////////
145         ////    SCHEDULERSJOB HELPER FUNCTIONS
146
147         /**
148          * This function takes a passed URL and cURLs it to fake multi-threading with another httpd instance
149          * @param       $job            String in URI-clean format
150          * @param       $timeout        Int value in secs for cURL to timeout. 30 default.
151          */
152         public function fireUrl($job, $timeout=30)
153         {
154         // TODO: figure out what error is thrown when no more apache instances can be spun off
155             // cURL inits
156                 $ch = curl_init();
157                 curl_setopt($ch, CURLOPT_URL, $job); // set url
158                 curl_setopt($ch, CURLOPT_FAILONERROR, true); // silent failure (code >300);
159                 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // do not follow location(); inits - we always use the current
160                 curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
161                 curl_setopt($ch, CURLOPT_DNS_USE_GLOBAL_CACHE, false);  // not thread-safe
162                 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // return into a variable to continue program execution
163                 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); // never times out - bad idea?
164                 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); // 5 secs for connect timeout
165                 curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);  // open brand new conn
166                 curl_setopt($ch, CURLOPT_HEADER, true); // do not return header info with result
167                 curl_setopt($ch, CURLOPT_NOPROGRESS, true); // do not have progress bar
168                 $urlparts = parse_url($job);
169                 if(empty($urlparts['port'])) {
170                     if($urlparts['scheme'] == 'https'){
171                                 $urlparts['port'] = 443;
172                         } else {
173                                 $urlparts['port'] = 80;
174                         }
175                 }
176                 curl_setopt($ch, CURLOPT_PORT, $urlparts['port']); // set port as reported by Server
177                 //TODO make the below configurable
178                 curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // most customers will not have Certificate Authority account
179                 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // most customers will not have Certificate Authority account
180
181                 curl_setopt($ch, CURLOPT_NOSIGNAL, true); // ignore any cURL signals to PHP (for multi-threading)
182                 $result = curl_exec($ch);
183                 $cInfo = curl_getinfo($ch);     //url,content_type,header_size,request_size,filetime,http_code
184                                                                         //ssl_verify_result,total_time,namelookup_time,connect_time
185                                                                         //pretransfer_time,size_upload,size_download,speed_download,
186                                                                         //speed_upload,download_content_length,upload_content_length
187                                                                         //starttransfer_time,redirect_time
188                 if(curl_errno($ch)) {
189                     $this->errors .= curl_errno($ch)."\n";
190                 }
191                 curl_close($ch);
192
193                 if($result !== FALSE && $cInfo['http_code'] < 400) {
194                         $GLOBALS['log']->debug("----->Firing was successful: $job");
195                         $GLOBALS['log']->debug('----->WTIH RESULT: '.strip_tags($result).' AND '.strip_tags(print_r($cInfo, true)));
196                         return true;
197                 } else {
198                         $GLOBALS['log']->fatal("Job failed: $job");
199                         return false;
200                 }
201         }
202         ////    END SCHEDULERSJOB HELPER FUNCTIONS
203         ///////////////////////////////////////////////////////////////////////////
204
205
206         ///////////////////////////////////////////////////////////////////////////
207         ////    STANDARD SUGARBEAN OVERRIDES
208         /**
209          * This function gets DB data and preps it for ListViews
210          */
211         function get_list_view_data()
212         {
213                 global $mod_strings;
214
215                 $temp_array = $this->get_list_view_array();
216                 $temp_array['JOB_NAME'] = $this->job_name;
217                 $temp_array['JOB']              = $this->job;
218
219         return $temp_array;
220         }
221
222         /** method stub for future customization
223          *
224          */
225         function fill_in_additional_list_fields()
226         {
227                 $this->fill_in_additional_detail_fields();
228         }
229
230
231         /**
232          * Mark this job as failed
233          * @param string $message
234          */
235     public function failJob($message = null)
236     {
237         return $this->resolveJob(self::JOB_FAILURE, $message);
238     }
239
240         /**
241          * Mark this job as success
242          * @param string $message
243          */
244     public function succeedJob($message = null)
245     {
246         return $this->resolveJob(self::JOB_SUCCESS, $message);
247     }
248
249     /**
250      * Called if job failed but will be retried
251      */
252     public function onFailureRetry()
253     {
254         // TODO: what we do if job fails, notify somebody?
255         $this->call_custom_logic("job_failure_retry");
256     }
257
258     /**
259      * Called if job has failed and will not be retried
260      */
261     public function onFinalFailure()
262     {
263         // TODO: what we do if job fails, notify somebody?
264         $this->call_custom_logic("job_failure");
265     }
266
267     /**
268      * Resolve job as success or failure
269      * @param string $resolution One of JOB_ constants that define job status
270      * @param string $message
271      * @return bool
272      */
273     public function resolveJob($resolution, $message = null)
274     {
275         $GLOBALS['log']->info("Resolving job {$this->id} as $resolution: $message");
276         if($resolution == self::JOB_FAILURE) {
277             $this->failure_count++;
278             if($this->requeue && $this->retry_count > 0) {
279                 // retry failed job
280                 $this->status = self::JOB_STATUS_QUEUED;
281                 if($this->job_delay < $this->min_interval) {
282                     $this->job_delay = $this->min_interval;
283                 }
284                 $this->execute_time = $GLOBALS['timedate']->getNow()->modify("+{$this->job_delay} seconds")->asDb();
285                 $this->retry_count--;
286                 $GLOBALS['log']->info("Will retry job {$this->id} at {$this->execute_time} ($this->retry_count)");
287                 $this->onFailureRetry();
288             } else {
289                 // final failure
290                 $this->status = self::JOB_STATUS_DONE;
291                 $this->onFinalFailure();
292             }
293         } else {
294             $this->status = self::JOB_STATUS_DONE;
295         }
296         $this->addMessages($message);
297         $this->resolution = $resolution;
298         $this->save();
299         if($this->status == self::JOB_STATUS_DONE && $this->resolution == self::JOB_SUCCESS) {
300             $this->updateSchedulerSuccess();
301         }
302         return true;
303     }
304
305     /**
306      * Update schedulers table on job success
307      */
308     protected function updateSchedulerSuccess()
309     {
310         if(empty($this->scheduler_id)) {
311             return;
312         }
313         $this->db->query("UPDATE schedulers SET last_run={$this->db->now()} WHERE id=".$this->db->quoted($this->scheduler_id));
314     }
315
316     /**
317      * Assemle job messages
318      * Takes messages in $this->message, errors & $message and assembles them into $this->message
319      * @param string $message
320      */
321     protected function addMessages($message)
322     {
323         if(!empty($this->errors)) {
324             $this->message .= $this->errors;
325             $this->errors = '';
326         }
327         if(!empty($message)) {
328             $this->message .= "$message\n";
329         }
330     }
331
332     /**
333      * Rerun this job again
334      * @param string $message
335      * @param string $delay how long to delay (default is job's delay)
336      * @return bool
337      */
338     public function postponeJob($message = null, $delay = null)
339     {
340         $this->status = self::JOB_STATUS_QUEUED;
341         $this->addMessages($message);
342         $this->resolution = self::JOB_PARTIAL;
343         if(empty($delay)) {
344             $delay = intval($this->job_delay);
345         }
346         $this->execute_time = $GLOBALS['timedate']->getNow()->modify("+$delay seconds")->asDb();
347         $GLOBALS['log']->info("Postponing job {$this->id} to {$this->execute_time}: $message");
348
349         $this->save();
350         return true;
351     }
352
353     /**
354      * Delete a job
355      * @see SugarBean::mark_deleted($id)
356      */
357     public function mark_deleted($id)
358     {
359         return $this->db->query("DELETE FROM {$this->table_name} WHERE id=".$this->db->quoted($id));
360     }
361
362     /**
363      * Shutdown handler to be called if something breaks in the middle of the job
364      */
365     public function unexpectedExit()
366     {
367         if(!$this->job_done) {
368             // Job wasn't properly finished, fail it
369             $this->resolveJob(self::JOB_FAILURE, translate('ERR_FAILED', 'SchedulersJobs'));
370         }
371     }
372
373     /**
374      * Run the job by ID
375      * @param string $id
376      * @param string $client Client that is trying to run the job
377      * @return bool|string true on success, false on job failure, error message on failure to run
378      */
379     public static function runJobId($id, $client)
380     {
381         $job = new self();
382         $job->retrieve($id);
383         if(empty($job->id)) {
384             $GLOBALS['log']->fatal("Job $id not found.");
385             return "Job $id not found.";
386         }
387         if($job->status != self::JOB_STATUS_RUNNING) {
388             $GLOBALS['log']->fatal("Job $id is not marked as running.");
389             return "Job $id is not marked as running.";
390         }
391         if($job->client != $client) {
392             $GLOBALS['log']->fatal("Job $id belongs to client {$job->client}, can not run as $client.");
393             return "Job $id belongs to another client, can not run as $client.";
394         }
395         $job->job_done = false;
396         register_shutdown_function(array($job, "unexpectedExit"));
397         $res = $job->runJob();
398         $job->job_done = true;
399         return $res;
400     }
401
402     /**
403      * Error handler, assembles the error messages
404      * @param int $errno
405      * @param string $errstr
406      * @param string $errfile
407      * @param int $errline
408      */
409     public function errorHandler($errno, $errstr, $errfile, $errline)
410     {
411         switch($errno)
412         {
413                 case E_USER_WARNING:
414                 case E_COMPILE_WARNING:
415                 case E_CORE_WARNING:
416                 case E_WARNING:
417                         $type = "Warning";
418                         break;
419                 case E_USER_ERROR:
420                 case E_COMPILE_ERROR:
421                 case E_CORE_ERROR:
422                 case E_ERROR:
423                         $type = "Fatal Error";
424                         break;
425                 case E_PARSE:
426                         $type = "Parse Error";
427                         break;
428                 case E_RECOVERABLE_ERROR:
429                         $type = "Recoverable Error";
430                         break;
431                     default:
432                         // Ignore errors we don't know about
433                         return;
434         }
435         $errstr = strip_tags($errstr);
436         $this->errors .= sprintf(translate('ERR_PHP', 'SchedulersJobs'), $type, $errno, $errstr, $errfile, $errline)."\n";
437     }
438
439     /**
440      * Change current user to given user
441      * @param User $user
442      */
443     protected function sudo($user)
444     {
445         $GLOBALS['current_user'] = $user;
446         // Reset the session
447         if(session_id()) {
448             session_destroy();
449         }
450         if(!headers_sent()) {
451                 session_start();
452             session_regenerate_id();
453         }
454         $_SESSION['is_valid_session']= true;
455         $_SESSION['user_id'] = $user->id;
456         $_SESSION['type'] = 'user';
457         $_SESSION['authenticated_user_id'] = $user->id;
458     }
459
460     /**
461      * Set environment to the user of this job
462      * @return boolean
463      */
464     protected function setJobUser()
465     {
466         // set up the current user and drop session
467         if(!empty($this->assigned_user_id)) {
468             $this->old_user = $GLOBALS['current_user'];
469             if(empty($this->user->id) || $this->assigned_user_id != $this->user->id) {
470                 $this->user = BeanFactory::getBean('Users', $this->assigned_user_id);
471                 if(empty($this->user->id)) {
472                     $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_NOSUCHUSER', 'SchedulersJobs'), $this->assigned_user_id));
473                     return false;
474                 }
475             }
476             $this->sudo($this->user);
477         } else {
478             $this->resolveJob(self::JOB_FAILURE, translate('ERR_NOUSER', 'SchedulersJobs'));
479             return false;
480         }
481         return true;
482     }
483
484     /**
485      * Restore previous user environment
486      */
487     protected function restoreJobUser()
488     {
489         if(!empty($this->old_user->id) && $this->old_user->id != $this->user->id) {
490             $this->sudo($this->old_user);
491         }
492     }
493
494     /**
495      * Run this job
496      * @return bool Was the job successful?
497      */
498     public function runJob()
499     {
500         require_once('modules/Schedulers/_AddJobsHere.php');
501
502         $this->errors = "";
503         $exJob = explode('::', $this->target, 2);
504         if($exJob[0] == 'function') {
505             // set up the current user and drop session
506             if(!$this->setJobUser()) {
507                 return false;
508             }
509                 $func = $exJob[1];
510                         $GLOBALS['log']->debug("----->SchedulersJob calling function: $func");
511             set_error_handler(array($this, "errorHandler"), E_ALL & ~E_NOTICE & ~E_STRICT);
512                         if(!is_callable($func)) {
513                             $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_CALL', 'SchedulersJobs'), $func));
514                         }
515                         $data = array($this);
516                         if(!empty($this->data)) {
517                             $data[] = $this->data;
518                         }
519             $res = call_user_func_array($func, $data);
520             restore_error_handler();
521             $this->restoreJobUser();
522                         if($this->status == self::JOB_STATUS_RUNNING) {
523                             // nobody updated the status yet - job function could do that
524                         if($res) {
525                             $this->resolveJob(self::JOB_SUCCESS);
526                                 return true;
527                         } else {
528                             $this->resolveJob(self::JOB_FAILURE);
529                             return false;
530                         }
531                         } else {
532                             return $this->resolution != self::JOB_FAILURE;
533                         }
534                 } elseif($exJob[0] == 'url') {
535                         if(function_exists('curl_init')) {
536                                 $GLOBALS['log']->debug('----->SchedulersJob firing URL job: '.$exJob[1]);
537                 set_error_handler(array($this, "errorHandler"), E_ALL & ~E_NOTICE & ~E_STRICT);
538                                 if($this->fireUrl($exJob[1])) {
539                     restore_error_handler();
540                     $this->resolveJob(self::JOB_SUCCESS);
541                                         return true;
542                                 } else {
543                     restore_error_handler();
544                                     $this->resolveJob(self::JOB_FAILURE);
545                                         return false;
546                                 }
547                         } else {
548                             $this->resolveJob(self::JOB_FAILURE, translate('ERR_CURL', 'SchedulersJobs'));
549                         }
550                 } elseif ($exJob[0] == 'class') {
551             $tmpJob = new $exJob[1]();
552             if($tmpJob instanceof RunnableSchedulerJob)
553             {
554                 // set up the current user and drop session
555                 if(!$this->setJobUser()) {
556                     return false;
557                 }
558                 $tmpJob->setJob($this);
559                 $result = $tmpJob->run($this->data);
560                 $this->restoreJobUser();
561                 if ($this->status == self::JOB_STATUS_RUNNING) {
562                     // nobody updated the status yet - job class could do that
563                     if ($result) {
564                         $this->resolveJob(self::JOB_SUCCESS);
565                         return true;
566                     } else {
567                         $this->resolveJob(self::JOB_FAILURE);
568                         return false;
569                     }
570                 } else {
571                     return $this->resolution != self::JOB_FAILURE;
572                 }
573             }
574             else {
575                 $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_JOBTYPE', 'SchedulersJobs'), strip_tags($this->target)));
576             }
577         }
578         else {
579                     $this->resolveJob(self::JOB_FAILURE, sprintf(translate('ERR_JOBTYPE', 'SchedulersJobs'), strip_tags($this->target)));
580                 }
581                 return false;
582     }
583
584 }  // end class Job
585
586 /**
587  * Runnable job queue job
588  *
589  */
590 interface RunnableSchedulerJob
591 {
592     /**
593      * @abstract
594      * @param SchedulersJob $job
595      */
596     public function setJob(SchedulersJob $job);
597
598     /**
599      * @abstract
600      *
601      */
602     public function run($data);
603 }