// +---------------------------------------------------------------------------+ // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or any later version. // +---------------------------------------------------------------------------+ // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // +---------------------------------------------------------------------------+ /** * CronjobSchedule - Model for the database table "cronjobs_schedules" * * @author Jan-Hendrik Willms * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 * @category Stud.IP * @since 2.4 * * @property string $id alias column for schedule_id * @property string $schedule_id database column * @property string $task_id database column * @property int $active database column * @property string|null $title database column * @property string|null $description database column * @property string|null $parameters database column * @property int|null $minute database column * @property int|null $hour database column * @property int|null $day database column * @property int|null $month database column * @property int|null $day_of_week database column * @property int $next_execution database column * @property int|null $last_execution database column * @property string|null $last_result database column * @property int $execution_count database column * @property int $mkdate database column * @property int $chdate database column * @property SimpleORMapCollection $logs has_many CronjobLog * @property CronjobTask $task belongs_to CronjobTask */ class CronjobSchedule extends SimpleORMap { protected static function configure($config = []) { $config['db_table'] = 'cronjobs_schedules'; $config['belongs_to']['task'] = [ 'class_name' => CronjobTask::class, 'foreign_key' => 'task_id', ]; $config['has_many']['logs'] = [ 'class_name' => CronjobLog::class, 'on_delete' => function (CronjobSchedule $schedule) { // Direct db query to avoid memory issues return DBManager::get()->execute( 'DELETE FROM cronjobs_logs WHERE schedule_id = ?', [$schedule->id] ); }, 'on_store' => 'store', ]; $config['registered_callbacks']['before_store'][] = 'cbJsonifyParameters'; $config['registered_callbacks']['after_store'][] = 'cbJsonifyParameters'; $config['registered_callbacks']['before_store'][] = 'cbLogActivation'; $config['registered_callbacks']['before_delete'][] = 'cbLogDeleting'; $config['registered_callbacks']['after_initialize'][] = 'cbJsonifyParameters'; parent::configure($config); } /** * replaces title with task name if title is empty. * * @return string the title or the task name */ public function getTitle() { return $this->content['title'] ?: $this->task->name ?? ''; } protected function cbJsonifyParameters($type) { if ($type === 'before_store' && !is_string($this->parameters)) { $this->parameters = json_encode($this->parameters ?: null); } if (in_array($type, ['after_initialize', 'after_store']) && is_string($this->parameters)) { $parameters = json_decode($this->parameters, true) ?: []; if ($this->task && $this->task->valid) { $default_parameters = $this->task->extractDefaultParameters(); foreach ($default_parameters as $key => $value) { if (!isset($parameters[$key])) { $parameters[$key] = $value; } } } $this->parameters = $parameters; } } protected function cbLogActivation($type) { if ($this->active && !$this->content_db['active']) { StudipLog::log('CRONJOB_SCHEDULE_ACTIVATED', null, null, $this->getTitle()); } if (!$this->active && $this->content_db['active']) { StudipLog::log('CRONJOB_SCHEDULE_DEACTIVATED', null, null, $this->getTitle()); } } protected function cbLogDeleting($type) { StudipLog::log('CRONJOB_SCHEDULE_DELETED', null, null, $this->getTitle()); } /** * Stores the schedule in database. Will bail out with an exception if * the provided task does not exists. Will also nullify the title if it * matches the task name (see CronjobSchedule::getTitle()). * * @return CronjobSchedule Returns itself to allow chaining */ public function store() { if ($this->task === null) { $message = sprintf('A task with the id "%s" does not exist.', $this->task_id); throw new InvalidArgumentException($message); } // Remove title if it is the default (task's name) if ($this->title === $this->task->name) { $this->title = null; } parent::store(); return $this; } /** * Activates this schedule. * * @return CronjobSchedule Returns itself to allow chaining */ public function activate(bool $run_immediately = false) { $next_execution = $run_immediately ? strtotime('-1 minute') : $this->calculateNextExecution(); $this->active = true; $this->next_execution = $next_execution; $this->store(); return $this; } /** * Deactivates this schedule. * * @return CronjobSchedule Returns itself to allow chaining */ public function deactivate() { $this->active = false; $this->store(); return $this; } /** * Executes this schedule. * * @param bool $force Pass true to force execution of the schedule even * if it's not activated * @return mixed The result of the execution * @throws RuntimeException When either the schedule or the according is * not activated */ public function execute($force = false) { if (!$force && !$this->active) { throw new RuntimeException('Execution aborted. Schedule is not active'); } if (!$this->task->active) { throw new RuntimeException('Execution aborted. Associated task is not active'); } $this->last_execution = time(); $this->execution_count += 1; $this->next_execution = $this->calculateNextExecution(); $this->store(); $this->task->execution_count += 1; $this->task->store(); $result = $this->task->engage($this->last_result, $this->parameters); $this->last_result = $result; $this->store(); return $result; } /** * Determines whether the schedule should execute given the provided * timestamp. * * @param mixed $now Defines the temporal fix point * @return bool Whether the schedule should execute or not. */ public function shouldExecute($now = null) { return ($now ?: time()) >= $this->next_execution; } /** * Calculates the next execution for this schedule. * * The next execution is calculated by increasing the current timestamp * and testing whether all conditions match. This is not the best method * to test and should be optimized sooner or later. * * @param mixed $now Defines the temporal fix point * * @return int Timestamp of calculated next execution * @throws RuntimeException When calculation takes too long (you should * check the conditions for validity in that case) */ public function calculateNextExecution($now = null) { $now = $now ?: time(); $result = $now; $result -= $result % 60; $i = 366 * 24 * 60; // Maximum: A year $offset = 60; do { $result += $offset; // TODO: Performance - Adjust result according to conditions // See http://coderzone.org/library/PHP-PHP-Cron-Parser-Class_1084.htm $valid = $this->testTimestamp($result, $this->minute, 'i') && $this->testTimestamp($result, $this->hour, 'H') && $this->testTimestamp($result, $this->day, 'd') && $this->testTimestamp($result, $this->month, 'm') && $this->testTimestamp($result, $this->day_of_week, 'N'); } while (!$valid && $i-- > 0); if ($i <= 0) { throw new RuntimeException('No result, current: ' . date('d.m.Y H:i', $result)); } $this->next_execution = $result; return $result; } /** * Tests a timestamp against the passed condition. * * @param int $timestamp The timestamp to test * @param mixed $condition Can be either null for "don't care", a positive * number for an exact moment or a negative number * for a repeating moment * @param String $format Format for date() to extract a portion of the * timestamp */ protected function testTimestamp($timestamp, $condition, $format) { if ($condition === null) { return true; } $probe = (int) date($format, $timestamp); $condition = (int) $condition; if ($condition < 0) { return ($probe % abs($condition)) === 0; } return $probe === $condition; } }