Seditio Source
Root |
./othercms/b2evolution_7.2.3/inc/cron/_cron.funcs.php
<?php
/**
 * This file implements cron (scheduled tasks) handling functions.
 *
 * 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 evocore
 */
if( !defined('EVO_MAIN_INIT') ) die( 'Please, do not access this page directly.' );


/**
 * Log a message from cron.
 * @param string Message
 * @param integer Level of importance. The higher the more important.
 *        (if $quiet (number of "-q" params passed to cron_exec.php)
 *         is higher than this, the message gets skipped)
 */
function cron_log( $message, $level = 0 )
{
    global
$is_web, $quiet;

    if(
$quiet > $level )
    {
        return;
    }

    if(
$is_web )
    {
        echo
'<p>'.$message.'</p>';
    }
    else
    {
        echo
"\n".$message."\n";
    }
}


/**
 * Append cron log and store in DB each 1 second OR 4096 bytes of the log message
 *
 * @param string Message text
 * @param string Message type: 'success', 'warning', 'error', 'note', NULL - to use default text without addition style color
 * @param string New line
 */
function cron_log_append( $message, $type = NULL, $nl = "\n" )
{
    global
$result_message, $cron_log_last_time, $cron_log_buffer_size, $cron_log_actions_num;

    if( ! isset(
$result_message ) )
    {    
// Initialize a var for cron log message:
       
$result_message = '';
    }

    if( ! isset(
$cron_log_buffer_size ) )
    {    
// Initialize a var to count a cron log message length:
       
$cron_log_buffer_size = 0;
    }

    if( ! isset(
$cron_log_actions_num ) )
    {    
// Initialize a var to count cron log actions:
       
$cron_log_actions_num = 0;
    }

    if( ! empty(
$message ) )
    {
       
// Set style for message depending on type:
       
switch( $type )
        {
            case
'success':
               
$message = '<span class="green">'.$message.'</span>';
                break;
            case
'error':
               
$message = '<span class="red">'.$message.'</span>';
                break;
            case
'warning':
               
$message = '<span class="orange">'.$message.'</span>';
                break;
            case
'note':
               
$message = '<span class="grey">'.$message.'</span>';
                break;
        }

       
// Append new line:
       
$message .= $nl;
    }

   
// Append new message to the cron log:
   
$result_message .= $message;

   
// Update buffer size:
   
$cron_log_buffer_size += strlen( $message );

    if(
$cron_log_buffer_size >= 4096 ||
        ( isset(
$cron_log_last_time ) && ( time() - $cron_log_last_time ) >= 1 ) )
    {    
// If log buffer >= 4096 OR last time execution >= 1 second:
       
if( $cron_log_buffer_size >= 4096 )
        {    
// Reset buffer size to count a next portion:
           
$cron_log_buffer_size = 0;
        }

       
// We must update cron log in DB in order to don't lose it on unexpected crash:
       
global $DB, $ctsk_ID, $time_difference;
        if( ! empty(
$ctsk_ID ) )
        {    
// We can update only cron job which is executing right now:
           
$DB->query( 'UPDATE T_cron__log
                  SET clog_messages = '
.$DB->quote( $result_message ).',
                      clog_actions_num = '
.$DB->quote( $cron_log_actions_num ).',
                      clog_realstop_datetime = '
.$DB->quote( date2mysql( time() + $time_difference ) ).'
                WHERE clog_ctsk_ID = '
.$ctsk_ID,
               
'Update a log message of the executing cron job #'.$ctsk_ID );
        }
    }

   
// Update a time global var to compare with next time:
   
$cron_log_last_time = time();
}


/**
 * Count a number of cron job actions and append cron log
 *
 * @param string Message
 * @param string Message type: 'success', 'warning', 'error', 'note'
 * @param string New line
 */
function cron_log_action_end( $message, $type = NULL, $nl = "\n" )
{
    global
$cron_log_actions_num;

    if( ! isset(
$cron_log_actions_num ) )
    {    
// Initialize a var to count cron log actions:
       
$cron_log_actions_num = 0;
    }

   
// Mark this as separate action:
   
$cron_log_actions_num++;

   
// Append cron log:
   
cron_log_append( $message.get_cron_log_time( $cron_log_actions_num ), $type, $nl );
}


/**
 * Set a number of cron job actions.
 *
 * Used for manual updating the actions number when cron job has no separate
 * actions, but it does many actions by single code like mysql query.
 *
 * @param integer Number of actions
 */
function cron_log_report_action_count( $num )
{
    global
$cron_log_actions_num;
   
$cron_log_actions_num = $num;
}


/**
 * Get a time of cron log
 *
 * @param integer A number of cron log action
 * @return string Cron log time
 */
function get_cron_log_time( $action_num = NULL )
{
    global
$Timer;

    return
'<div class="note">'
           
.( $action_num === NULL ? '' : 'Action #'.$action_num.' Finished. ' )
            .
'Elapsed time since beginning of task: '.$Timer->get_duration( 'cron_exec' ).' seconds'
       
.'</div>';
}


/**
 * Call a cron job.
 *
 * @param string Key of the job
 * @param string Params for the job:
 *               'ctsk_ID'   - task ID
 *               'ctsk_name' - task name
 * @return string Error message
 */
function call_job( $job_key, $job_params = array() )
{
    global
$DB, $inc_path, $Plugins, $admin_url, $is_web;

    global
$result_message, $result_status, $timestop, $time_difference;

   
$error_message = '';
   
$result_message = NULL;
   
$result_status = 'error';

   
$job_ctrl = get_cron_jobs_config( 'ctrl', $job_key );

    if(
preg_match( '~^plugin_(\d+)_(.*)$~', $job_ctrl, $match ) )
    {
// Cron job provided by a plugin:
       
if( ! is_object($Plugins) )
        {
           
load_class( 'plugins/model/_plugins.class.php', 'Plugins' );
           
$Plugins = new Plugins();
        }

       
$Plugin = & $Plugins->get_by_ID( $match[1] );
        if( !
$Plugin )
        {
           
$result_message = 'Plugin for controller ['.$job_ctrl.'] could not get instantiated.';
           
cron_log( $result_message, 2 );
            return
$result_message;
        }

       
// CALL THE PLUGIN TO HANDLE THE JOB:
       
$tmp_params = array( 'ctrl' => $match[2], 'params' => $job_params );
       
$sub_r = $Plugins->call_method( $Plugin->ID, 'ExecCronJob', $tmp_params );

       
$error_code = (int)$sub_r['code'];
       
$result_message = $sub_r['message'];
    }
    else
    {
       
$controller = $inc_path.$job_ctrl;
        if( !
is_file( $controller ) )
        {
           
$result_message = 'Controller ['.$job_ctrl.'] does not exist.';
           
cron_log( $result_message, 2 );
            return
$result_message;
        }

       
// INCLUDE THE JOB FILE AND RUN IT:
       
$error_code = require $controller;
    }

    if(
is_array( $result_message ) )
    {    
// If result is array (we should store it as serialized data later)
        // array keys: 'message' - Result message
        //             'table_cols' - Columns names of the table to display on the Execution details of the cron job log
        //             'table_data' - Array
       
$result_message_text = $result_message['message'];
    }
    else
    {    
// Result is text string
       
$result_message_text = $result_message;
    }

    if(
$error_code != 1 )
    {    
// We got an error
       
$result_status = ( $error_code == 20 ? 'imap_error' : 'error' );
       
$result_message_text = '[Error code: '.$error_code.']'."\n".$result_message_text;
        if(
is_array( $result_message ) )
        {
// If result is array
           
$result_message['message'] = $result_message_text;
        }
       
$cron_log_level = 2;

       
$error_message = $result_message_text;
    }
    else
    {
       
$result_status = 'finished';
       
$cron_log_level = 1;
    }

    if(
$is_web )
    {    
// Is web interface?
       
$result_message_text = str_replace( "\n", "<br>\n", $result_message_text );
    }
    else
    {    
// Is CLI mode?
       
$result_message_text = str_replace(
            array(
'<br>', '<b>', '</b>', '<code>', '</code>', '&#8800;', '&#8804;', '&#8805;' ),
            array(
"\n", '*', '*', '`', '`', '!=', '<=', '>=' ),
           
$result_message_text );
    }

   
$timestop = time() + $time_difference;
   
cron_log( 'Task finished at '.date( 'H:i:s', $timestop ).' with status: '.$result_status
       
.( $is_web ? '<br>' : "\n" ).'Message: '.$result_message_text, $cron_log_level );

    return
$error_message;
}


/**
 * Get status titles
 *
 * @return array
 */
function cron_statuses()
{
    return array(
           
'pending'    => T_('Pending'),
           
'started'    => T_('Started'),
           
'warning'    => T_('Warning'),
           
'timeout'    => T_('Timed out'),
           
'error'      => T_('Error'),
           
'imap_error' => T_('IMAP error'),
           
'finished'   => T_('Finished'),
        );
}


/**
 * Get status title of sheduled job by status value
 *
 * @param string Status
 * @return string Title
 */
function cron_status_title( $status )
{
   
$titles = cron_statuses();

    return isset(
$titles[ $status ] ) ? $titles[ $status ] : $status;
}


/**
 * Get status color of sheduled job by status value
 *
 * @param string Status value
 * @return string Color value
 */
function cron_status_color( $status )
{
   
$colors = array(
           
'pending'  => '808080',
           
'started'  => '4d77cb',
           
'warning'  => 'dbdb57',
           
'timeout'  => 'e09952',
           
'error'    => 'cb4d4d',
           
'imap_error' => 'cb4d4d',
           
'finished' => '34b27d',
        );

    return isset(
$colors[ $status ] ) ? '#'.$colors[ $status ] : 'none';
}


/**
 * Get the manual page link for the requested cron job, given by the key
 *
 * @param string job key
 * @return string Link to manual page of cron job task
 */
function cron_job_manual_link( $job_key )
{
   
$help_config = get_cron_jobs_config( 'help', $job_key );

    if( empty(
$help_config ) )
    {
// There was no 'help' topic defined for this job
       
return '';
    }

    if(
$help_config == '#' )
    {
// The 'help' topic is set to '#', use the default 'task-' + job_key
       
return get_manual_link( 'task-'.$job_key );
    }

    if(
is_url( $help_config ) )
    {
// This cron job help topic is an url
       
return action_icon( T_('Open relevant page in online manual'), 'manual', $help_config, T_('Manual'), 5, 1, array( 'target' => '_blank', 'style' => 'vertical-align:top' ) );
    }

    return
get_manual_link( $help_config );
}


/**
 * Get name of cron job
 *
 * @param string Job key
 * @param string Job name
 * @param string|array Job params
 * @return string Default value of job name of Name from DB
 */
function cron_job_name( $job_key, $job_name = '', $job_params = '' )
{
    if( empty(
$job_name ) )
    {
// Get default name by key
       
$job_name = get_cron_jobs_config( 'name', $job_key );
    }

   
$job_params = is_string( $job_params ) ? unserialize( $job_params ) : $job_params;
    if( ! empty(
$job_params ) )
    {
// Prepare job name with the specified params
       
switch( $job_key )
        {
            case
'send-post-notifications':
               
// Add item title to job name
               
if( ! empty( $job_params['item_ID'] ) )
                {
                   
$ItemCache = & get_ItemCache();
                    if(
$Item = $ItemCache->get_by_ID( $job_params['item_ID'], false, false ) )
                    {
                       
$job_name = sprintf( $job_name, $Item->ID, $Item->get( 'title' ) );
                    }
                }
                break;

            case
'send-comment-notifications':
               
// Add item title of the comment to job name
               
if( ! empty( $job_params['comment_ID'] ) )
                {
                   
$CommentCache = & get_CommentCache();
                    if(
$Comment = & $CommentCache->get_by_ID( $job_params['comment_ID'], false, false ) )
                    {
                        if(
$Item = $Comment->get_Item() )
                        {
                           
$job_name = sprintf( $job_name, $Item->get( 'title' ) );
                        }
                    }
                }
                break;

            case
'send-email-campaign':
               
// Add email campaign title and chunk size to job name:
               
global $Settings;
               
$email_campaign_title = '';
                if( ! empty(
$job_params['ecmp_ID'] ) )
                {
                   
$EmailCampaignCache = & get_EmailCampaignCache();
                    if(
$EmailCampaign = $EmailCampaignCache->get_by_ID( $job_params['ecmp_ID'], false, false ) )
                    {
                       
$email_campaign_title = $EmailCampaign->get( 'name' );
                    }
                }
               
$job_name = sprintf( $job_name, $Settings->get( 'email_campaign_chunk_size' ), $email_campaign_title );
                break;
        }
    }

    return
$job_name;
}


/**
 * Detect timed out cron jobs and Send notifications
 *
 * @param array Task with error
 *             'name'
 *             'message'
 */
function detect_timeout_cron_jobs( $error_task = NULL )
{
    global
$DB, $time_difference, $admin_url;

   
// Convert time difference to mysql format:
   
$mysql_time_difference = intval( $time_difference );
    if(
$mysql_time_difference >= 0 )
    {    
// Negative value already has a sign "-", but for positive value we must add a sigh "+" for correct mysql operation below:
       
$mysql_time_difference = '+ '.$mysql_time_difference;
    }

   
$SQL = new SQL( 'Find cron timeouts' );
   
$SQL->SELECT( 'ctsk_ID, ctsk_name, ctsk_key' );
   
$SQL->FROM( 'T_cron__log' );
   
$SQL->FROM_add( 'INNER JOIN T_cron__task ON ctsk_ID = clog_ctsk_ID' );
   
$SQL->FROM_add( 'LEFT JOIN T_settings ON set_name = CONCAT( "cjob_timeout_", ctsk_key )' );
   
$SQL->WHERE( 'clog_status = "started"' );
   
$SQL->WHERE_and( 'UNIX_TIMESTAMP( clog_realstart_datetime ) < UNIX_TIMESTAMP() - IFNULL( set_value, 600 ) - 120 '.$mysql_time_difference );
   
$SQL->GROUP_BY( 'ctsk_ID' );
   
$timeout_tasks = $DB->get_results( $SQL );

   
$tasks = array();

    if(
count( $timeout_tasks ) > 0 )
    {
       
$cron_jobs_names = get_cron_jobs_config( 'name' );
        foreach(
$timeout_tasks as $timeout_task )
        {
            if( ! empty(
$timeout_task->ctsk_name ) )
            {
// Task name is defined in DB
               
$task_name = $timeout_task->ctsk_name;
            }
            else
            {
// Try to get default task name by key:
               
$task_name = ( isset( $cron_jobs_names[ $timeout_task->ctsk_key ] ) ? $cron_jobs_names[ $timeout_task->ctsk_key ] : $timeout_task->ctsk_key );
            }
           
$tasks[ $timeout_task->ctsk_ID ] = $task_name;
        }

       
// Update timed out cron jobs:
       
$DB->query( 'UPDATE T_cron__log
              SET clog_status = "timeout"
            WHERE clog_ctsk_ID IN ( '
.$DB->quote( array_keys( $tasks ) ).' )', 'Mark timeouts in cron jobs.' );
    }

   
$timeout_tasks_num = count( $tasks );
    if(
$timeout_tasks_num > 0 || $error_task !== NULL )
    {    
// Send notification email about timed out and error cron jobs to users with edit options permission:
       
$email_template_params = array(
               
'timeout_tasks' => $tasks,
               
'error_task'    => $error_task,
            );
        if(
$timeout_tasks_num > 1 || ( $error_task !== NULL && $timeout_tasks_num > 0 ) )
        {    
// Set email subject for multiple cron jobs:
           
$email_subject = NT_('Errors in multiple scheduled tasks');
        }
        elseif(
$error_task !== NULL )
        {    
// Use name of error task in email subject:
           
$email_subject = array( NT_('Error in task: %s'), $error_task['name'] );
        }
        else
        {    
// Use name of first timed out task in email subject:
           
foreach( $tasks as $timeout_task_name )
            {
               
$email_subject = array( NT_('Error in task: %s'), $timeout_task_name );
                break;
            }
        }
       
send_admin_notification( $email_subject, 'scheduled_task_error_report', $email_template_params );
    }
}


/**
 * Get config array with all available cron jobs
 *
 * @param string What param we should get from config: 'name', 'ctrl', 'params'
 * @param string Get one cron job by specified key
 * @return array|string Array of all cron jobs | Value of specified cron job
 */
function get_cron_jobs_config( $get_param = '', $get_by_key = '' )
{
    global
$cron_jobs_config;

    if( isset(
$cron_jobs_config ) )
    {
// Get config from global vaiable to don't initialize this var twice
       
if( !empty( $get_by_key ) )
        {
// A specific job param(s) was requested
           
if( empty( $get_param ) )
            {
// Return all params
               
return $cron_jobs_config[ $get_by_key ];
            }
           
// Return the requested job param if it is set or NULL otherwise
           
return ( isset( $cron_jobs_config[ $get_by_key ][$get_param] ) ) ? $cron_jobs_config[ $get_by_key ][$get_param] : NULL;
        }

        if( empty(
$get_param ) )
        {
// No specific param or job key was requested, return the whole config list
           
return $cron_jobs_config;
        }

       
// Get a specific config param for all jobs
       
$restricted_config = array();
        foreach(
$cron_jobs_config as $job_key => $job_config )
        {
           
$restricted_config[ $job_key ] = isset( $job_config[ $get_param ] ) ? $job_config[ $get_param ] : NULL;
        }
        return
$restricted_config;
    }

   
// This array will contain the modules and the plugins cron jobs data
   
$cron_jobs_config = array();

   
// Get additional jobs from different modules and Plugins:
   
global $modules, $Plugins;

    foreach(
$modules as $module )
    {
       
$Module = & $GLOBALS[$module.'_Module'];
       
// Note: cron jobs with the same key will ovverride previously defined cron jobs data
       
$cron_jobs_config = array_merge( $cron_jobs_config, $Module->get_cron_jobs() );
    }

    if( !empty(
$Plugins ) )
    {
        foreach(
$Plugins->trigger_collect( 'GetCronJobs' ) as $plug_ID => $jobs )
        {
            if( !
is_array($jobs) )
            {
               
$Debuglog->add( sprintf('GetCronJobs() for plugin #%d did not return array. Ignoring its jobs.', $plug_ID), array('plugins', 'error') );
                continue;
            }
            foreach(
$jobs as $job )
            {
               
// Validate params from plugin:
               
if( ! isset($job['params']) )
                {
                   
$job['params'] = NULL;
                }
                if( !
is_array($job) || ! isset($job['ctrl'], $job['name']) )
                {
                   
$Debuglog->add( sprintf('GetCronJobs() for plugin #%d did return invalid job. Ignoring.', $plug_ID), array('plugins', 'error') );
                    continue;
                }
                if( isset(
$job['params']) && ! is_array($job['params']) )
                {
                   
$Debuglog->add( sprintf('GetCronJobs() for plugin #%d did return invalid job params (not an array). Ignoring.', $plug_ID), array('plugins', 'error') );
                    continue;
                }
               
$job['ctrl'] = 'plugin_'.$plug_ID.'_'.$job['ctrl'];

               
add_cron_jobs_config( str_replace( '_', '-', $job['ctrl'] ), $job, true );
            }
        }
    }

    return
get_cron_jobs_config( $get_param, $get_by_key );
}


/**
 * Add new cron job to config
 *
 * @param string Cron job key
 * @param array Cron job data, array with keys: 'name', 'ctrl', 'params'
 * @param boolean TRUE to rewrite previous key
 */
function add_cron_jobs_config( $key, $data, $force = false )
{
    global
$cron_jobs_config;

    if( !
$force && isset( $cron_jobs_config[ $key ] ))
    {
// Add new cron job to config array
       
return;
    }

   
$cron_jobs_config[ $key ] = $data;
}


/**
 * Build cron job names SELECT query from crong_jobs_config array
 *
 * @param string What fields to get, separated by comma
 * @return string SQL query
 */
function cron_job_sql_query( $fields = 'key,name' )
{
    global
$DB;

   
$cron_jobs_config = get_cron_jobs_config();

   
// We need to set the collation explicitly if the current db connection charset is utf-8 in order to avoid "Illegal mix of collation" issue
    // Basically this is a hack which should be reviewed when the charset issues are fixed generally.
    // TODO: asimo>Review this temporary solution after the charset issues were fixed.
   
$default_collation = ( $DB->is_expected_connection_charset( 'utf8' ) ) ? ' COLLATE utf8mb4_unicode_ci' : '';

   
$name_query = '';
    if( !empty(
$cron_jobs_config ) )
    {
       
$fields = explode( ',', $fields );

       
$name_query = 'SELECT task_'.implode( ', task_', $fields ).' FROM ('."\n";
       
$first_task = true;
        foreach(
$cron_jobs_config as $ctsk_key => $ctsk_data )
        {
           
$field_values = '';
           
$field_separator = '';
            foreach(
$fields as $field )
            {
               
$field_values .= $field_separator.$DB->quote( $field == 'key' ? $ctsk_key : $ctsk_data[ $field ] ).$default_collation.' AS task_'.$field;
               
$field_separator = ', ';
            }

            if(
$first_task )
            {
               
$name_query .= "\t".'SELECT '.$field_values."\n";
               
$first_task = false;
            }
            else
            {
               
$name_query .= "\t".'UNION SELECT '.$field_values."\n";
            }
        }
       
$name_query .= ') AS inner_temp';
    }

    return
$name_query;
}


/**
 * Error handler for cron job
 *
 * @return boolean FALSE - to continue normal error handler, TRUE - to don't run normal error handler
 */
function cron_job_error_handler()
{
   
$last_error = error_get_last();

    if(
$last_error['type'] === E_ERROR )
    {    
// If last error is fatal:
       
global $result_message, $error_message, $DB;

        if( empty(
$result_message ) )
        {    
// Initialize result message of current executing cron job:
           
$result_message = '';
        }

       
// Append error info to cron job log:
       
$new_error_message = "\n".'b2evolution caught an UNEXPECTED ERROR: '
           
.( $last_error
               
? '<b>File:</b> '.$last_error['file'].', '
                 
.'<b>Line:</b> '.$last_error['line'].', '
                 
.'<b>Message:</b> '.$last_error['message']
                :
'Unknown'
           
);
        if(
$new_error_message !== $error_message )
        {    
// Update only really new error, in order to exclude duplicates:
           
$error_message = $new_error_message;
           
$result_message .= $error_message;
        }

       
// We must rollback any started transaction in order to proper update a log of the interrupted cron job:
       
$DB->rollback();

        return
true;
    }

   
// To continue normal error handler:
   
return false;
}


/**
 * Shutdown function to save log of the interrupted cron job by unexpected error:
 */
function cron_job_shutdown()
{
    global
$result_message, $ctsk_ID, $ctsk_name, $DB;

    if( empty(
$ctsk_ID ) )
    {    
// Run this function to detect only interrupted cron jobs:
       
return;
    }

   
// Run error handler to store info of last error in $result_message:
   
cron_job_error_handler();

   
$DB->query( 'UPDATE T_cron__log
          SET clog_status = "error",
              clog_realstop_datetime = '
.$DB->quote( date2mysql( time() ) ).',
              clog_messages = '
.$DB->quote( $result_message ).'
        WHERE clog_ctsk_ID = '
.$ctsk_ID,
       
'Record task as finished with error by shutdown function.' );

   
// Detect timed out tasks and send notification about timed out and error tasks:
   
detect_timeout_cron_jobs( array(
           
'ID'      => $ctsk_ID,
           
'name'    => $ctsk_name,
           
'message' => $result_message,
        ) );
}
?>