1 <?php
2
3 4 5 6 7 8 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
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
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