1<?php
2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
3//
4// All Rights Reserved. See copyright.txt for details and a complete list of authors.
5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
6// $Id$
7
8use Psr\Log\LoggerInterface;
9
10class Scheduler_Item
11{
12
13	public $id;
14	public $name;
15	public $description;
16	public $task;
17	public $params;
18	public $run_time;
19	public $status;
20	public $re_run;
21	public $run_only_once;
22	public $creation_date;
23	private $logger;
24
25	const STATUS_ACTIVE = 'active';
26	const STATUS_INACTIVE = 'inactive';
27
28	public static $availableTasks = [
29		'ConsoleCommandTask' => 'ConsoleCommand',
30		'ShellCommandTask' => 'ShellCommand',
31		'HTTPGetCommandTask' => 'HTTPGetCommand',
32		'TikiCheckerCommandTask' => 'TikiCheckerCommand',
33	];
34
35	public function __construct($id, $name, $description, $task, $params, $run_time, $status, $re_run, $run_only_once, LoggerInterface $logger)
36	{
37		$this->id = $id;
38		$this->name = $name;
39		$this->description = $description;
40		$this->task = $task;
41		$this->params = $params;
42		$this->run_time = $run_time;
43		$this->status = $status;
44		$this->re_run = $re_run;
45		$this->run_only_once = $run_only_once;
46		$this->logger = $logger;
47	}
48
49	public static function getAvailableTasks()
50	{
51		return self::$availableTasks;
52	}
53
54	/**
55	 * Save Scheduler
56	 */
57	public function save()
58	{
59		$schedLib = TikiLib::lib('scheduler');
60		$id = $schedLib->set_scheduler(
61			$this->name,
62			$this->description,
63			$this->task,
64			$this->params,
65			$this->run_time,
66			$this->status,
67			$this->re_run,
68			$this->run_only_once,
69			$this->id,
70			$this->creation_date
71		);
72
73		if ($id) {
74			$this->id = $id;
75		}
76	}
77
78	/**
79	 * check if the scheduler is running
80	 *
81	 * @return int return scheduler id if it is matched and return -1 otherwise
82	 */
83	public function isRunning()
84	{
85		$lastRun = $this->getLastRun();
86		return ! empty($lastRun) ? $lastRun['status'] == 'running' : false;
87	}
88
89	/**
90	 * Check if scheduler is stalled (running for long time)
91	 *
92	 * @param bool|null	$notify	Send tiki admins an email notification if scheduler is marked as stalled, if null uses tiki stored preferences
93	 *
94	 * @return int|bool	The scheduler run id if is stalled, false otherwise
95	 */
96	public function isStalled($notify = null)
97	{
98		global $tikilib;
99
100		$threshold = $tikilib->get_preference('scheduler_stalled_timeout', 15);
101
102		if ($threshold == 0) {
103			return false;
104		}
105
106		$lastRun = $this->getLastRun();
107
108		if (empty($lastRun) || $lastRun['status'] != 'running') {
109			return false;
110		}
111
112		if ($lastRun['stalled']) {
113			return true;
114		}
115
116		$startTime = $lastRun['start_time'];
117		$now = time();
118
119		if ($now < ($startTime + $threshold * 60)) {
120			return false;
121		}
122
123		$this->setStalled($lastRun['id'], $notify);
124
125		return $lastRun['id'];
126	}
127
128	/**
129	 * Sets last run as stalled
130	 *
131	 * @param int		$runId	The run id to mark as stalled
132	 * @param bool|null	$notify	Send tiki admins an email notification, if null uses tiki stored preferences
133	 */
134	protected function setStalled($runId, $notify = null)
135	{
136		global $tikilib;
137
138		$schedLib = TikiLib::lib('scheduler');
139		$schedLib->setSchedulerRunStalled($this->id, $runId);
140
141		if (is_null($notify)) {
142			$notify = $tikilib->get_preference('scheduler_notify_on_stalled', 'y') === 'y';
143		}
144
145		if (! $notify) {
146			return;
147		}
148
149		$users = Scheduler_Utils::getSchedulerNotificationUsers('scheduler_users_to_notify_on_stalled');
150
151		Tiki\Notifications\Email::sendSchedulerNotification('scheduler_stalled_notification_subject.tpl', 'scheduler_stalled_notification.tpl', $this, $users);
152	}
153
154	/**
155	 * Mark last run as healed
156	 *
157	 * @param string	$message	The output message when healed
158	 * @param bool|null	$notify		Send email notification to tiki admins when scheduler is marked as healed, if null uses tiki stored preferences
159	 * @param bool		$force		Force heal even if not in the timeframe
160	 *
161	 * @return bool	True if healed, false otherwise.
162	 */
163	public function heal($message, $notify = null, $force = false)
164	{
165		global $tikilib;
166
167		$threshold = $tikilib->get_preference('scheduler_healing_timeout', 30);
168
169		if ($threshold == 0 && ! $force) {
170			return false;
171		}
172
173		$lastRun = $this->getLastRun();
174
175		if (empty($lastRun) || $lastRun['status'] != 'running' || $lastRun['healed']) {
176			return false;
177		}
178
179		$startTime = $lastRun['start_time'];
180		$now = time();
181
182		if ($now < ($startTime + $threshold * 60) && ! $force) {
183			return false;
184		}
185
186		$schedLib = TikiLib::lib('scheduler');
187		$schedLib->setSchedulerRunHealed($this->id, $lastRun['id'], $message);
188
189		if (is_null($notify)) {
190			$notify = $tikilib->get_preference('scheduler_notify_on_healing', 'y') === 'y';
191		}
192
193		if ($notify) {
194			$users = Scheduler_Utils::getSchedulerNotificationUsers('scheduler_users_to_notify_on_healed');
195
196			Tiki\Notifications\Email::sendSchedulerNotification('scheduler_healed_notification_subject.tpl', 'scheduler_healed_notification.tpl', $this, $users);
197		}
198
199		$this->reduceLogs();
200
201		return true;
202	}
203
204	/**
205	 * Remove old logs
206	 *
207	 * @param int|null $numberLogs THe number of logs to keep
208	 */
209	public function reduceLogs($numberLogs = null)
210	{
211		global $tikilib;
212
213		if (is_null($numberLogs) || ! is_numeric($numberLogs)) {
214			$numberLogs = $tikilib->get_preference('scheduler_keep_logs');
215		}
216
217		if (empty($numberLogs)) {
218			return;
219		}
220
221		$schedLib = TikiLib::lib('scheduler');
222		$count = $schedLib->countRuns($this->id);
223
224		if ($count > $numberLogs) {
225			// Get the older run to keep
226			$schedulers = $schedLib->get_scheduler_runs($this->id, 1, $numberLogs - 1);
227			$runId = $schedulers[0]['id'];
228
229			$schedLib->removeLogs($this->id, $runId);
230		}
231	}
232
233	/**
234	 * @param bool	$userTriggered	True if user triggered the execution, false otherwise.
235	 *
236	 * @return	array	The execution status and output message
237	 */
238	public function execute($userTriggered = false)
239	{
240		global $user, $prefs;
241
242		$schedlib = TikiLib::lib('scheduler');
243		$status = $schedlib->get_run_status($this->id);
244
245		$this->logger->info('Scheduler last run status: ' . $status);
246
247		if ($status == 'running') {
248			if ($this->isStalled()) {
249				return [
250					'status' => 'failed',
251					'message' => tr('Scheduler task is stalled.')
252				];
253			}
254
255			return [
256				'status' => 'failed',
257				'message' => tr('Scheduler task is already running.')
258			];
259		}
260
261		$this->logger->info('Task: ' . $this->task);
262
263		$class = 'Scheduler_Task_' . $this->task;
264		if (! class_exists($class)) {
265			return [
266				'status' => 'failed',
267				'message' => $class . ' not found.',
268			];
269		}
270
271		if($this->run_only_once) {
272			$schedlib->setInactive($this->id);
273		}
274
275		list('run_id' => $runId, 'start_time' => $startTime) = $schedlib->start_scheduler_run($this->id);
276		$this->logger->debug("Start time: " . $startTime);
277
278		$params = json_decode($this->params, true);
279		$this->logger->debug("Task params: " . $this->params);
280
281		if ($params === null && ! empty($this->params)) {
282			return [
283				'status' => 'failed',
284				'message' => tr('Unable to decode task params.')
285			];
286		}
287
288		$task = new $class($this->logger);
289		$result = $task->execute($params);
290
291		$executionStatus = $result ? 'done' : 'failed';
292		$outputMessage = $task->getOutput();
293
294		if ($userTriggered) {
295			$userlib = TikiLib::lib('user');
296			$email = $userlib->get_user_email($user);
297			$outputMessage = sprintf('Run triggered by %s - %s.' . PHP_EOL, $user, $email) . (empty($outputMessage) ? '' : '<hr>') . $outputMessage;
298		}
299
300		$endTime = $schedlib->end_scheduler_run($this->id, $runId, $executionStatus, $outputMessage, null, 0);
301		$this->logger->debug("End time: " . $endTime);
302
303		$this->reduceLogs();
304
305		return [
306			'status' => $executionStatus,
307			'message' => $outputMessage,
308		];
309	}
310
311	/**
312	 * Return scheduler last run
313	 *
314	 * @return array|null An array with last run details or null if not found
315	 */
316	public function getLastRun()
317	{
318		$schedlib = TikiLib::lib('scheduler');
319		$runs = $schedlib->get_scheduler_runs($this->id, 1);
320
321		if (empty($runs)) {
322			return null;
323		}
324
325		return $runs[0];
326	}
327
328	/**
329	 * Transforms an array (from schedulers lib) to a Scheduler_Item object
330	 *
331	 * @param array 			$scheduler	The scheduler details
332	 * @param LoggerInterface	$logger		Logger
333	 *
334	 * @return Scheduler_Item
335	 */
336	public static function fromArray(array $scheduler, $logger)
337	{
338		return new self(
339			$scheduler['id'],
340			$scheduler['name'],
341			$scheduler['description'],
342			$scheduler['task'],
343			$scheduler['params'],
344			$scheduler['run_time'],
345			$scheduler['status'],
346			$scheduler['re_run'],
347			$scheduler['run_only_once'],
348			$logger
349		);
350	}
351}
352