1 <?php
  2 
  3 /**
  4  * Cronjob Addon.
  5  *
  6  * @author gharlan[at]web[dot]de Gregor Harlan
  7  *
  8  * @package redaxo\cronjob
  9  */
 10 
 11 class rex_cronjob_manager_sql
 12 {
 13     private $sql;
 14     private $manager;
 15 
 16     private function __construct(rex_cronjob_manager $manager = null)
 17     {
 18         $this->sql = rex_sql::factory();
 19         // $this->sql->setDebug();
 20         $this->manager = $manager;
 21     }
 22 
 23     public static function factory(rex_cronjob_manager $manager = null)
 24     {
 25         return new self($manager);
 26     }
 27 
 28     public function getManager()
 29     {
 30         if (!is_object($this->manager)) {
 31             $this->manager = rex_cronjob_manager::factory();
 32         }
 33         return $this->manager;
 34     }
 35 
 36     public function hasManager()
 37     {
 38         return is_object($this->manager);
 39     }
 40 
 41     public function setMessage($message)
 42     {
 43         $this->getManager()->setMessage($message);
 44     }
 45 
 46     public function getMessage()
 47     {
 48         return $this->getManager()->getMessage();
 49     }
 50 
 51     public function hasMessage()
 52     {
 53         return $this->getManager()->hasMessage();
 54     }
 55 
 56     public function getName($id)
 57     {
 58         $this->sql->setQuery('
 59             SELECT  name
 60             FROM    ' . rex::getTable('cronjob') . '
 61             WHERE   id = ?
 62             LIMIT   1
 63         ', [$id]);
 64         if ($this->sql->getRows() == 1) {
 65             return $this->sql->getValue('name');
 66         }
 67         return null;
 68     }
 69 
 70     public function setStatus($id, $status)
 71     {
 72         $this->sql->setTable(rex::getTable('cronjob'));
 73         $this->sql->setWhere(['id' => $id]);
 74         $this->sql->setValue('status', $status);
 75         $this->sql->addGlobalUpdateFields();
 76         try {
 77             $this->sql->update();
 78             $success = true;
 79         } catch (rex_sql_exception $e) {
 80             $success = false;
 81         }
 82         $this->saveNextTime();
 83         return $success;
 84     }
 85 
 86     public function setExecutionStart($id, $reset = false)
 87     {
 88         $this->sql->setTable(rex::getTable('cronjob'));
 89         $this->sql->setWhere(['id' => $id]);
 90         $this->sql->setDateTimeValue('execution_start', $reset ? 0 : time());
 91         try {
 92             $this->sql->update();
 93             return true;
 94         } catch (rex_sql_exception $e) {
 95             return false;
 96         }
 97     }
 98 
 99     public function delete($id)
100     {
101         $this->sql->setTable(rex::getTable('cronjob'));
102         $this->sql->setWhere(['id' => $id]);
103         try {
104             $this->sql->delete();
105             $success = true;
106         } catch (rex_sql_exception $e) {
107             $success = false;
108         }
109         $this->saveNextTime();
110         return $success;
111     }
112 
113     public function check()
114     {
115         $env = rex_cronjob_manager::getCurrentEnvironment();
116         $script = 'script' === $env;
117 
118         $sql = rex_sql::factory();
119         // $sql->setDebug();
120 
121         $query = '
122             SELECT    id, name, type, parameters, `interval`, execution_moment
123             FROM      '.rex::getTable('cronjob').'
124             WHERE     status = 1
125                 AND   execution_start < ?
126                 AND   environment LIKE ?
127                 AND   nexttime <= ?
128             ORDER BY  nexttime ASC, execution_moment DESC, name ASC
129         ';
130 
131         if ($script) {
132             $minExecutionStartDiff = 6 * 60 * 60;
133         } else {
134             $query .= ' LIMIT 1';
135 
136             $minExecutionStartDiff = 2 * (ini_get('max_execution_time') ?: 60 * 60);
137         }
138 
139         $jobs = $sql->getArray($query, [rex_sql::datetime(time() - $minExecutionStartDiff), '%|' .$env. '|%', rex_sql::datetime()]);
140 
141         if (!$jobs) {
142             $this->saveNextTime();
143             return;
144         }
145 
146         ignore_user_abort(true);
147         register_shutdown_function(function () use (&$jobs) {
148             foreach ($jobs as $job) {
149                 if (isset($job['finished'])) {
150                     continue;
151                 }
152 
153                 if (!isset($job['started'])) {
154                     $this->setExecutionStart($job['id'], true);
155                     continue;
156                 }
157 
158                 $manager = $this->getManager();
159                 $manager->setCronjob(rex_cronjob::factory($job['type']));
160                 $manager->log(false, connection_status() != 0 ? 'Timeout' : 'Unknown error');
161                 $this->setNextTime($job['id'], $job['interval'], true);
162             }
163 
164             $this->saveNextTime();
165         });
166 
167         foreach ($jobs as $job) {
168             $this->setExecutionStart($job['id']);
169         }
170 
171         if ($script || 1 == $jobs[0]['execution_moment']) {
172             foreach ($jobs as &$job) {
173                 $job['started'] = true;
174                 $this->tryExecuteJob($job, true, true);
175                 $job['finished'] = true;
176             }
177             return;
178         }
179 
180         rex_extension::register('RESPONSE_SHUTDOWN', function () use (&$jobs) {
181             $job[0]['started'] = true;
182             $this->tryExecuteJob($jobs[0], true, true);
183             $job[0]['finished'] = true;
184         });
185     }
186 
187     public function tryExecute($id, $log = true)
188     {
189         $sql = rex_sql::factory();
190         $jobs = $sql->getArray('
191             SELECT    id, name, type, parameters, `interval`
192             FROM      ' . rex::getTable('cronjob') . '
193             WHERE     id = ? AND environment LIKE ?
194             LIMIT     1
195         ', [$id, '%|' . rex_cronjob_manager::getCurrentEnvironment() . '|%']);
196 
197         if (!$jobs) {
198             $this->getManager()->setMessage('Cronjob not found in database');
199             $this->saveNextTime();
200             return false;
201         }
202 
203         return $this->tryExecuteJob($jobs[0], $log);
204     }
205 
206     private function tryExecuteJob(array $job, $log = true, $resetExecutionStart = false)
207     {
208         $params = json_decode($job['parameters'], true);
209         $cronjob = rex_cronjob::factory($job['type']);
210 
211         $this->setNextTime($job['id'], $job['interval'], $resetExecutionStart);
212 
213         $success = $this->getManager()->tryExecute($cronjob, $job['name'], $params, $log, $job['id']);
214 
215         return $success;
216     }
217 
218     public function setNextTime($id, $interval, $resetExecutionStart = false)
219     {
220         $nexttime = self::calculateNextTime(json_decode($interval, true));
221         $nexttime = $nexttime ? rex_sql::datetime($nexttime) : null;
222         $add = $resetExecutionStart ? ', execution_start = 0' : '';
223         try {
224             $this->sql->setQuery('
225                 UPDATE  ' . rex::getTable('cronjob') . '
226                 SET     nexttime = ?' . $add . '
227                 WHERE   id = ?
228             ', [$nexttime, $id]);
229             $success = true;
230         } catch (rex_sql_exception $e) {
231             $success = false;
232         }
233         $this->saveNextTime();
234         return $success;
235     }
236 
237     public function getMinNextTime()
238     {
239         $this->sql->setQuery('
240             SELECT  MIN(nexttime) AS nexttime
241             FROM    ' . rex::getTable('cronjob') . '
242             WHERE   status = 1
243         ');
244 
245         if ($this->sql->getRows() == 1) {
246             return (int) $this->sql->getDateTimeValue('nexttime');
247         }
248         return null;
249     }
250 
251     public function saveNextTime($nexttime = null)
252     {
253         if ($nexttime === null) {
254             $nexttime = $this->getMinNextTime();
255         }
256         if ($nexttime === null) {
257             $nexttime = 0;
258         } else {
259             $nexttime = max(1, $nexttime);
260         }
261 
262         rex_config::set('cronjob', 'nexttime', $nexttime);
263         return true;
264     }
265 
266     public static function calculateNextTime(array $interval)
267     {
268         if (empty($interval['minutes']) || empty($interval['hours']) || empty($interval['days']) || empty($interval['weekdays']) || empty($interval['months'])) {
269             return null;
270         }
271 
272         $date = new \DateTime('+5 min');
273         $date->setTime($date->format('H'), floor($date->format('i') / 5) * 5, 0);
274 
275         $isValid = function ($value, $current) {
276             return 'all' === $value || in_array($current, $value);
277         };
278 
279         $validateTime = function () use ($interval, $date, $isValid) {
280             while (!$isValid($interval['hours'], $date->format('G'))) {
281                 $date->modify('+1 hour');
282                 $date->setTime($date->format('H'), 0, 0);
283             }
284 
285             while (!$isValid($interval['minutes'], (int) $date->format('i'))) {
286                 $date->modify('+5 min');
287 
288                 while (!$isValid($interval['hours'], $date->format('G'))) {
289                     $date->modify('+1 hour');
290                     $date->setTime($date->format('H'), 0, 0);
291                 }
292             }
293         };
294 
295         $validateTime();
296 
297         if (
298             !$isValid($interval['days'], $date->format('j')) ||
299             !$isValid($interval['weekdays'], $date->format('w')) ||
300             !$isValid($interval['months'], $date->format('n'))
301         ) {
302             $date->setTime(0, 0, 0);
303             $validateTime();
304 
305             while (!$isValid($interval['months'], $date->format('n'))) {
306                 $date->modify('first day of next month');
307             }
308 
309             while (!$isValid($interval['days'], $date->format('j')) || !$isValid($interval['weekdays'], $date->format('w'))) {
310                 $date->modify('+1 day');
311 
312                 while (!$isValid($interval['months'], $date->format('n'))) {
313                     $date->modify('first day of next month');
314                 }
315             }
316         }
317 
318         return $date->getTimestamp();
319     }
320 }
321