<?php
/**
* This file implements the UI controller for Cron table.
*
* This file is part of the evoCore framework - {@link http://evocore.net/}
* See also {@link https://github.com/b2evolution/b2evolution}.
*
* @license GNU GPL v2 - {@link http://b2evolution.net/about/gnu-gpl-license}
*
* @copyright (c)2003-2020 by Francois Planque - {@link http://fplanque.com/}
*
* @package admin
*/
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );
load_funcs( 'cron/_cron.funcs.php' );
// Check minimum permission:
check_user_perm( 'admin', 'normal', true );
check_user_perm( 'options', 'view', true );
$AdminUI->set_path( 'options', 'cron', 'list' );
param( 'action', 'string', 'list' );
param( 'tab', 'string', 'list' );
if( param( 'ctsk_ID', 'integer', '', true) )
{// Load cronjob from cache:
$CronjobCache = & get_CronjobCache();
if( ( $edited_Cronjob = & $CronjobCache->get_by_ID( $ctsk_ID, false ) ) === false )
{
unset( $edited_Cronjob );
forget_param( 'ctsk_ID' );
$Messages->add( sprintf( TB_('Requested «%s» object does not exist any longer.'), TB_('Scheduled job') ), 'error' );
$action = 'list';
}
}
switch( $action )
{
case 'new':
// Check that we have permission to edit options:
check_user_perm( 'options', 'edit', true, NULL );
load_class( 'cron/model/_cronjob.class.php', 'Cronjob' );
$edited_Cronjob = new Cronjob();
// Get this param to preselect job type by url param:
param( 'cjob_type', 'string' );
break;
case 'edit':
case 'copy':
// Check that we have permission to edit options:
check_user_perm( 'options', 'edit', true, NULL );
if( ( $action == 'edit' && $edited_Cronjob->get_status() != 'pending' ) ||
( $action == 'copy' && $edited_Cronjob->get_status() != 'error' ) )
{ // Don't edit cron jobs with not "pending" status
header_redirect( '?ctrl=crontab', 303 ); // Will EXIT
// We have EXITed already at this point!!
}
if( $action == 'copy' )
{ // Reset time to now for copied cron job
global $localtimenow;
$edited_Cronjob->start_timestamp = $localtimenow;
}
break;
case 'create':
// Check that this action request is not a CSRF hacked request:
$Session->assert_received_crumb( 'crontask' );
// Check that we have permission to edit options:
check_user_perm( 'options', 'edit', true, NULL );
if( !empty( $edited_Cronjob ) )
{ // It is a copy action, we should save the fields "key" & "params"
$ctsk_key = $edited_Cronjob->get( 'key' );
$ctsk_params = $edited_Cronjob->get( 'params' );
}
// CREATE OBJECT:
load_class( '/cron/model/_cronjob.class.php', 'Cronjob' );
$edited_Cronjob = new Cronjob();
if( $edited_Cronjob->load_from_Request() )
{ // We could load data from form without errors:
if( !empty( $ctsk_key ) )
{ // Save controller field from copied object
$edited_Cronjob->set( 'key', $ctsk_key );
}
if( !empty( $ctsk_params ) )
{ // Save params field from copied object
$edited_Cronjob->set( 'params', $ctsk_params );
}
// Save to DB:
$edited_Cronjob->dbinsert();
$Messages->add( TB_('New job has been scheduled.'), 'success' );
// Redirect so that a reload doesn't write to the DB twice:
header_redirect( '?ctrl=crontab', 303 ); // Will EXIT
// We have EXITed already at this point!!
}
break;
case 'update':
// Check that this action request is not a CSRF hacked request:
$Session->assert_received_crumb( 'crontask' );
// Check that we have permission to edit options:
check_user_perm( 'options', 'edit', true, NULL );
if( $edited_Cronjob->load_from_Request() )
{ // We could load data from form without errors:
if( $edited_Cronjob->dbupdate() )
{ // The job was updated successfully
$Messages->add( TB_('The scheduled job has been updated.'), 'success' );
}
else
{ // Errors on updating, probably this job has not "pending" status
$Messages->add( TB_('This scheduled job can not be updated.'), 'error' );
}
// Redirect so that a reload doesn't write to the DB twice:
header_redirect( '?ctrl=crontab', 303 ); // Will EXIT
// We have EXITed already at this point!!
}
break;
case 'delete':
// Check that this action request is not a CSRF hacked request:
$Session->assert_received_crumb( 'crontask' );
// Make sure we got an ord_ID:
param( 'ctsk_ID', 'integer', true );
// Check that we have permission to edit options:
check_user_perm( 'options', 'edit', true, NULL );
// TODO: prevent deletion of running tasks.
$DB->begin();
$tsk_status = $DB->get_var(
'SELECT clog_status
FROM T_cron__log
WHERE clog_ctsk_ID= '.$ctsk_ID,
0, 0, 'Check that task is not running' );
if( $tsk_status == 'started' )
{
$DB->rollback();
$Messages->add( sprintf( TB_('Job #%d is currently running. It cannot be deleted.'), $ctsk_ID ), 'error' );
}
else
{
// Delete task:
$DB->query( 'DELETE FROM T_cron__task
WHERE ctsk_ID = '.$ctsk_ID );
// Delete log (if exists):
$DB->query( 'DELETE FROM T_cron__log
WHERE clog_ctsk_ID = '.$ctsk_ID );
$DB->commit();
$Messages->add( sprintf( TB_('Scheduled job #%d deleted.'), $ctsk_ID ), 'success' );
}
//forget_param( 'ctsk_ID' );
//$action = 'list';
// Redirect so that a reload doesn't write to the DB twice:
header_redirect( regenerate_url( 'action,ctsk_ID', '', '', '&' ), 303 ); // Will EXIT
// We have EXITed already at this point!!
break;
case 'settings':
// Update settings of cron jobs:
// Check that this action request is not a CSRF hacked request:
$Session->assert_received_crumb( 'cronsettings' );
// Check permission:
check_user_perm( 'options', 'edit', true );
$cron_jobs = get_cron_jobs_config( 'name' );
foreach( $cron_jobs as $cron_job_key => $cron_job_name )
{
// Max execution time:
$Settings->set( 'cjob_timeout_'.$cron_job_key, param_duration( 'cjob_timeout_'.$cron_job_key ) );
$cjob_maxemail = param( 'cjob_maxemail_'.$cron_job_key, 'string', NULL );
if( $cjob_maxemail !== NULL )
{ // Setting only for cron jobs that use email sending:
$cjob_maxemail = intval( $cjob_maxemail );
$Settings->set( 'cjob_maxemail_'.$cron_job_key, ( $cjob_maxemail > 0 ? $cjob_maxemail : '' ) );
}
$cjob_imap_error = param( 'cjob_imap_error_'.$cron_job_key, 'string', NULL );
if( $cjob_imap_error !== NULL )
{ // Setting only for cron jobs that use IMAP email sending:
$cjob_imap_error = intval( $cjob_imap_error );
$Settings->set( 'cjob_imap_error_'.$cron_job_key, ( $cjob_imap_error > 1 ? $cjob_imap_error : 1 ) );
}
// Additional settings per cron job:
switch( $cron_job_key )
{
case 'send-email-campaign':
// Send a chunk of x emails for the campaign:
if( check_user_perm( 'emails', 'edit' ) )
{ // Allow to edit email cron setting "Chunk Size" only if user has a permission:
$Settings->set( 'email_campaign_chunk_size', param( 'email_campaign_chunk_size', 'integer', 0 ) );
}
// Delay between chunks:
$Settings->set( 'email_campaign_cron_repeat', param_duration( 'email_campaign_cron_repeat' ) );
// Delay between chunks in case all remaining recipients have reached max # of emails for the current day:
$Settings->set( 'email_campaign_cron_limited', param_duration( 'email_campaign_cron_limited' ) );
// Max emails to same domain:
$Settings->set( 'email_campaign_max_domain', param( 'email_campaign_max_domain', 'integer', 0 ) );
break;
case 'prune-old-hits-and-sessions':
// Prune old hits & sessions (includes OPTIMIZE):
param( 'auto_prune_stats', 'integer', $Settings->get_default( 'auto_prune_stats' ), false, false, true, false );
$Settings->set( 'auto_prune_stats', get_param( 'auto_prune_stats' ) );
break;
case 'prune-recycled-comments':
// Prune recycled comments:
param( 'auto_empty_trash', 'integer', $Settings->get_default( 'auto_empty_trash' ), false, false, true, false );
$Settings->set( 'auto_empty_trash', get_param( 'auto_empty_trash' ) );
break;
case 'cleanup-scheduled-jobs':
// Clean up scheduled jobs older than a threshold:
$Settings->set( 'cleanup_jobs_threshold', param( 'cleanup_jobs_threshold', 'integer', 0 ) );
$Settings->set( 'cleanup_jobs_threshold_failed', param( 'cleanup_jobs_threshold_failed', 'integer', 0 ) );
break;
case 'cleanup-email-logs':
// Clean up email logs older than a threshold:
$Settings->set( 'cleanup_email_logs_threshold', param_duration( 'cleanup_email_logs_threshold', 'integer', 0 ) );
break;
case 'send-non-activated-account-reminders':
// Send reminders about non-activated accounts:
$Settings->set( 'activate_account_reminder_threshold', param_duration( 'activate_account_reminder_threshold' ) );
// Account activation reminder settings:
$reminder_config = array();
$reminder_config_num = param( 'activate_account_reminder_config_num', 'integer', 0 );
for( $c = 0; $c <= $reminder_config_num; $c++ )
{
$reminder_config_value = param_duration( 'activate_account_reminder_config_'.$c );
if( $reminder_config_value > 0 || $c >= $reminder_config_num - 2 )
{ // Store only a selected reminder and 3 last options("Mark as Failed / Pending delete", "Delete warning", "Delete account"):
$reminder_config[ $c ] = $reminder_config_value;
}
}
if( count( $reminder_config ) < 4 )
{ // If no reminder has been selected:
param_error( 'activate_account_reminder_config_0', TB_('Please select at least one reminder for account activation reminder after subscription.') );
}
if( empty( $reminder_config[ $reminder_config_num - 2 ] ) )
{ // If "Mark as Failed / Pending delete" is not selected:
param_error( 'activate_account_reminder_config_'.( $reminder_config_num - 2 ), /* Do NOT translate because it is impossible for normal form */'Please select account activation reminder threshold to "Marked as Failed / Pending delete" after subscription.' );
}
$Settings->set( 'activate_account_reminder_config', implode( ',', $reminder_config ) );
break;
case 'send-inactive-account-reminders':
// Send reminders about inactivate accounts:
$Settings->set( 'inactive_account_reminder_threshold', param_duration( 'inactive_account_reminder_threshold' ) );
break;
case 'send-unmoderated-comments-reminders':
// Send reminders about comments awaiting moderation:
$Settings->set( 'comment_moderation_reminder_threshold', param_duration( 'comment_moderation_reminder_threshold' ) );
break;
case 'send-unmoderated-posts-reminders':
// Send reminders about posts awaiting moderation:
$Settings->set( 'post_moderation_reminder_threshold', param_duration( 'post_moderation_reminder_threshold' ) );
break;
case 'send-unread-messages-reminders':
// Send reminders about unread messages:
$Settings->set( 'unread_message_reminder_threshold', param_duration( 'unread_message_reminder_threshold' ) );
// Unread private messages reminder settings:
$reminder_delay = array();
$i = 1;
$prev_reminder_delay_day = 0;
$prev_reminder_delay_spacing = 0;
for( $d = 1; $d <= 10; $d++ )
{
$reminder_delay_day = param( 'unread_message_reminder_delay_day_'.$d, 'integer', 0 );
$reminder_delay_spacing = param( 'unread_message_reminder_delay_spacing_'.$d, 'integer', 0 );
if( $reminder_delay_day > 0 || $reminder_delay_spacing > 0 )
{ // Store only a filled reminder:
if( empty( $reminder_delay_day ) )
{ // If one field is not filled:
param_error( 'unread_message_reminder_delay_day_'.$i, sprintf( TB_('Please fill both fields of the unread private messages reminder #%d.'), $i ) );
$reminder_delay_day = 0;
}
elseif( $prev_reminder_delay_day >= $reminder_delay_day )
{ // If current value is less than previous:
param_error( 'unread_message_reminder_delay_day_'.$i, TB_('The values of the unread private messages reminder must be ascending.') );
}
if( empty( $reminder_delay_spacing ) )
{ // If one field is not filled:
param_error( 'unread_message_reminder_delay_spacing_'.$i, sprintf( TB_('Please fill both fields of the unread private messages reminder #%d.'), $i ) );
$reminder_delay_spacing = 0;
}
elseif( $prev_reminder_delay_spacing >= $reminder_delay_spacing )
{ // If current value is less than previous:
param_error( 'unread_message_reminder_delay_spacing_'.$i, TB_('The values of the unread private messages reminder must be ascending.') );
}
$reminder_delay[] = $reminder_delay_day.':'.$reminder_delay_spacing;
$prev_reminder_delay_day = $reminder_delay_day;
$prev_reminder_delay_spacing = $reminder_delay_spacing;
$i++;
}
}
if( empty( $reminder_delay ) )
{ // If no reminder has been selected:
param_error( 'unread_message_reminder_delay_day_1', TB_('Please select at least one reminder for unread private messages.') );
// Set one empty reminder in order to display all 10 reminders on the error form:
$reminder_delay[] = '0:0';
}
$Settings->set( 'unread_message_reminder_delay', implode( ',', $reminder_delay ) );
break;
case 'manage-email-statuses':
// Manage email address statuses:
$manage_email_statuses_min_delay = param_duration( 'manage_email_statuses_min_delay' );
param_check_not_empty( 'manage_email_statuses_min_delay', sprintf( TB_('The field «%s» cannot be empty.'), TB_('Minimum delay since last error') ) );
$Settings->set( 'manage_email_statuses_min_delay', $manage_email_statuses_min_delay );
param_integer_range( 'manage_email_statuses_min_sends', 1, 999999999, sprintf( TB_('The minimum value of the field "%s" is %d.'), TB_('Minimum sends since last error'), 1 ) );
$Settings->set( 'manage_email_statuses_min_sends', $manage_email_statuses_min_sends );
break;
}
}
if( param_errors_detected() )
{ // Don't store settings if errors:
break;
}
// Update settings:
$Settings->dbupdate();
$Messages->add( TB_('Scheduler settings have been updated.'), 'success' );
// Redirect so that a reload doesn't write to the DB twice:
header_redirect( $admin_url.'?ctrl=crontab&tab='.$tab, 303 ); // Will EXIT
// We have EXITed already at this point!!
break;
case 'view':
$cjob_ID = param( 'cjob_ID', 'integer', true );
$sql = 'SELECT *
FROM T_cron__task LEFT JOIN T_cron__log ON ctsk_ID = clog_ctsk_ID
WHERE ctsk_ID = '.$cjob_ID;
$cjob_row = $DB->get_row( $sql, OBJECT, 0, 'Get cron job and log' );
if( empty( $cjob_row ) )
{
$Messages->add( sprintf( TB_('Job #%d does not exist any longer.'), $cjob_ID ), 'error' );
$action = 'list';
}
break;
case 'list':
if( $tab == 'list' )
{ // Detect timed out tasks:
detect_timeout_cron_jobs();
}
break;
}
$AdminUI->breadcrumbpath_init( false ); // fp> I'm playing with the idea of keeping the current blog in the path here...
$AdminUI->breadcrumbpath_add( TB_('System'), $admin_url.'?ctrl=system' );
$AdminUI->breadcrumbpath_add( TB_('Scheduler'), $admin_url.'?ctrl=crontab' );
// Set an url for manual page:
switch( $action )
{
case 'new':
case 'create':
case 'edit':
case 'update':
case 'copy':
$AdminUI->set_page_manual_link( 'scheduled-job-form' );
break;
case 'view':
$AdminUI->set_page_manual_link( 'scheduled-job-info' );
break;
default:
switch( $tab )
{
case 'settings':
$AdminUI->set_path( 'options', 'cron', 'settings' );
$AdminUI->set_page_manual_link( 'scheduled-jobs-settings' );
break;
case 'test':
$AdminUI->set_path( 'options', 'cron', 'test' );
$AdminUI->set_page_manual_link( 'scheduled-jobs-test' );
break;
default:
$AdminUI->set_page_manual_link( 'scheduled-jobs-list' );
}
break;
}
if( in_array( $action, array( 'new', 'create', 'edit', 'update', 'copy', 'list' ) ) )
{ // Initialize date picker for cronjob.form.php
init_datepicker_js();
}
// Display <html><head>...</head> section! (Note: should be done early if actions do not redirect)
$AdminUI->disp_html_head();
// Display title, menu, messages, etc. (Note: messages MUST be displayed AFTER the actions)
$AdminUI->disp_body_top();
// Begin payload block:
$AdminUI->disp_payload_begin();
switch( $action )
{
case 'new':
case 'create':
case 'edit':
case 'update':
case 'copy':
// Display VIEW:
$AdminUI->disp_view( 'cron/views/_cronjob.form.php' );
break;
case 'view':
// Display VIEW:
$AdminUI->disp_view( 'cron/views/_cronjob.view.php' ); // uses $cjob_row
break;
default:
// Display VIEW:
switch( $tab )
{
case 'settings':
$AdminUI->disp_view( 'cron/views/_cronjob_settings.form.php' );
break;
case 'test':
// Require this template without function $AdminUI->disp_view() in order to keep all global vars which are used by cron_exec.php:
require $inc_path.'cron/views/_cronjob_test.view.php';
break;
default:
$AdminUI->disp_view( 'cron/views/_cronjob_list.view.php' );
}
}
// End payload block:
$AdminUI->disp_payload_end();
// Display body bottom, debug info and close </html>:
$AdminUI->disp_global_footer();
?>