Seditio Source
Root |
./othercms/ips_4.3.4/system/Task/Task.php
<?php
/**
 * @brief        Task Model
 * @author        <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
 * @copyright    (c) Invision Power Services, Inc.
 * @license        https://www.invisioncommunity.com/legal/standards/
 * @package        Invision Community
 * @since        5 Aug 2013
 */

namespace IPS;

/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
   
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
    exit;
}

/**
 * Task Model
 */
class _Task extends \IPS\Patterns\ActiveRecord
{
   
/**
     * Run a background queue task
     *
     * @return    mixed
     */    
   
public static function runQueue()
    {
       
$queueData    = \IPS\Db::i()->select( '*', 'core_queue', array( 'app_enabled=?', 1 ), 'priority ASC, RAND()', 1 )->join( 'core_applications', "app=app_directory" )->first();
       
$newOffset    = 0;

       
$queueData['_originalOffset']    = $queueData['offset'];

        try
        {
           
$extensions = \IPS\Application::load( $queueData['app'] )->extensions( 'core', 'Queue', FALSE );
            if ( !isset(
$extensions[ $queueData['key'] ] ) )
            {
                throw new \
IPS\Task\Queue\OutOfRangeException;
            }
           
           
$class = new $extensions[ $queueData['key'] ];
           
$json  = json_decode( $queueData['data'], TRUE );
           
$newOffset = $class->run( $json, $queueData['offset'] );

           
$queueData['offset']            = $newOffset;

           
/* This is here for legacy purposes - background queue tasks should throw \IPS\Task\Queue\OutOfRangeException to indicate they are completed, but
                for now we'll still support this and log it for investigation */
           
if ( is_null( $newOffset ) )
            {
                \
IPS\Log::log( $queueData['key'] . " returned a NULL offset - tasks should throw \\IPS\\Task\\Queue\\OutOfRangeException when they are finished", 'runQueue_log' );
                \
IPS\Db::i()->delete( 'core_queue', array( 'id=?', $queueData['id'] ) );

               
/* Do we have a post-completion callback? */
               
if( method_exists( $class, 'postComplete' ) )
                {
                   
$class->postComplete( $queueData );
                }
            }
           
/* Task completed successfully and a new offset was returned - store the value and then return it */
           
else
            {
                \
IPS\Log::debug( $queueData['key'] . " returned a new offset of " . $newOffset, 'runQueue_log' );

                \
IPS\Db::i()->update( 'core_queue', array( 'offset' => $newOffset ), array( 'id=?', $queueData['id'] ) );
               
               
$newData = json_encode( $json );
               
               
/* Did it change?? */
               
if ( $newData !== $queueData['data'] )
                {
                   
$queueData['data'] = $newData;
                    \
IPS\Db::i()->update( 'core_queue', array( 'data' => $newData ), array( 'id=?', $queueData['id'] ) );
                }
            }
        }
       
/* This means the task is done */
       
catch( \IPS\Task\Queue\OutOfRangeException $e )
        {
            \
IPS\Log::debug( $queueData['key'] . " completed successfully", 'runQueue_log' );

            \
IPS\Db::i()->delete( 'core_queue', array( 'id=?', $queueData['id'] ) );

           
/* Do we have a post-completion callback? */
           
if( isset( $class ) AND method_exists( $class, 'postComplete' ) )
            {
               
$class->postComplete( $queueData );
            }
        }

        return
$queueData;
    }

   
/**
     * Queue a background task
     *
     * @param    string    $app                        The application that will be responsible for processing
     * @param    string    $key                        The key of the extension that will be responsible for processing
     * @param    mixed    $data                        Data necessary for processing
     * @param    int        $priority                    Run order. Values 1 to 5 are allowed, 1 being highest priority.
     * @param    array    $checkForDuplicationKeys    Pass keys to check to prevent duplicate queue tasks being added
     * @return    void
     * @throws    \InvalidArgumentException    If $app or $key is invalid
     */    
   
public static function queue( $app, $key, $data = NULL, $priority=5, $checkForDuplicationKeys=NULL )
    {
        try
        {
           
$extensions = \IPS\Application::load( $app )->extensions( 'core', 'Queue', FALSE );
        }
        catch ( \
OutOfRangeException $e )
        {
            throw new \
InvalidArgumentException;
        }
        if ( !isset(
$extensions[ $key ] ) )
        {
            throw new \
InvalidArgumentException;
        }
       
        if (
method_exists( $extensions[ $key ], 'preQueueData' ) )
        {
           
$class = new $extensions[ $key ];
           
$data = $class->preQueueData( $data );
            if (
$data === NULL )
            {
                return;
            }
        }
       
        if (
is_array( $checkForDuplicationKeys ) and is_array( $data ) )
        {
           
$insert = FALSE;
            foreach( \
IPS\Db::i()->select( '*', 'core_queue', array( '`app`=? AND `key`=?', $app, $key ) ) as $row )
            {
                if (
$row['data'] )
                {
                   
$oldData = json_decode( $row['data'], TRUE );
                   
$got = 0;
                   
                    foreach(
$checkForDuplicationKeys as $k )
                    {
                        if ( isset(
$oldData[ $k ] ) and isset( $data[ $k ] ) and $oldData[ $k ] == $data[ $k ] )
                        {
                           
$got++;
                        }
                    }
                   
                    if (
$got === count( $checkForDuplicationKeys ) )
                    {
                       
/* Ok, so we have a duplicate queue item, lets remove it so the new one which is set with the correct count is used and offset is returned to 0 to start over */
                       
\IPS\Db::i()->delete( 'core_queue', array( 'id=?', $row['id'] ) );
                    }
                }
            }
        }
   
        \
IPS\Db::i()->insert( 'core_queue', array(
           
'data'        => json_encode( $data ),
           
'date'        => time(),
           
'app'        => $app,
           
'key'        => $key,
           
'priority'    => $priority
       
) );
       
        \
IPS\Db::i()->update( 'core_tasks', array( 'enabled' => 1 ), array( '`key`=?', 'queue' ) );
    }
   
   
/* !Task */
   
    /**
     * Get next queued task
     *
     * @return    \IPS\Task|NULL
     */
   
public static function queued()
    {        
       
$fifteenMinutesAgo = ( time() - 900 );
        foreach ( \
IPS\Db::i()->select( '*', 'core_tasks', array( 'next_run<? AND enabled=1', ( time() + 60 ) ), 'next_run ASC', NULL, NULL, NULL, \IPS\Db::SELECT_FROM_WRITE_SERVER ) as $task )
        {
            try
            {
               
$task = static::constructFromData( $task );
       
                if ( !
$task->running or $task->next_run < $fifteenMinutesAgo )
                {
                    if (
$task->running )
                    {
                       
$task->unlock();
                    }
                    else
                    {
                        return
$task;
                    }
                }
            }
            catch( \
RuntimeException $e ) { }
        }
        return
NULL;
    }
   
   
/**
     * Run and log
     *
     * @return    void
     */
   
public function runAndLog()
    {
       
$result = NULL;
       
$error = FALSE;
       
        try
        {
           
$result = $this->run();
        }
        catch ( \
IPS\Task\Exception $e )
        {
           
$result = $e->getMessage();
           
$error  = 1;
        }
       
        if (
$error !== FALSE or $result !== NULL )
        {
            \
IPS\Db::i()->insert( 'core_tasks_log', array(
               
'task'    => $this->id,
               
'error'    => $error,
               
'log'    => json_encode( $result ),
               
'time'    => time()
            ) );
        }
    }
   
   
/**
     * Run
     *
     * @return    mixed    Message to log or NULL
     * @throws    \IPS\Task\Exception
     */
   
public function run()
    {        
       
$this->running = TRUE;
       
$this->next_run = time();
       
$this->save();
       
       
$output = $this->execute();
       
       
$this->running = 0;
       
$this->lock_count = 0;
       
$this->next_run = \IPS\DateTime::create()->add( new \DateInterval( $this->frequency ) )->getTimestamp();
       
$this->last_run = \IPS\DateTime::create()->getTimestamp();
       
$this->save();
       
        return
$output;
    }
   
   
/**
     * Execute
     *
     * If ran successfully, should return anything worth logging. Only log something
     * worth mentioning (don't log "task ran successfully"). Return NULL (actual NULL, not '' or 0) to not log (which will be most cases).
     * If an error occurs which means the task could not finish running, throw an \IPS\core\Task\Exception - do not log an error as a normal log.
     * Tasks should execute within the time of a normal HTTP request.
     *
     * @return    mixed    Message to log or NULL
     * @throws    \IPS\Task\Exception
     */
   
public function execute()
    {
       
    }
   
   
/**
     * Run until timeout
     *
     * @param    callback    $callback    The code to run. Should return TRUE if we're happy to keep going, and FALSE if there's no more work to do
     * @param    int|null    $limit        A hard limit on the number of times to call $callback, or NULL for no limit
     * @return    void
     */
   
public function runUntilTimeout( $callback, $limit=NULL )
    {
       
/* Work out the maximum execution time */
       
$timeLeft = 45;
        if (
$phpMaxExecutionTime = ini_get('max_execution_time') and $phpMaxExecutionTime <= $timeLeft )
        {
           
$timeLeft = $phpMaxExecutionTime - 2;
        }

       
/* Factor in wait_timeout if possible */
       
try
        {
           
$mysqlTimeout = \IPS\Db::i()->query( "SHOW SESSION VARIABLES LIKE 'wait_timeout'" )->fetch_assoc();
           
$mysqlTimeout = $mysqlTimeout['Value'];

            if(
$mysqlTimeout <= $timeLeft )
            {
               
$timeLeft = $mysqlTimeout - 2;
            }
        }
        catch( \
IPS\Db\Exception $e ){}

       
$timeTheLastRunTook = 0;
               
       
/* Work out the memory limit */
       
$memoryLeft = 0;
       
$memoryUnlimited = FALSE;
        if (
function_exists( 'memory_get_usage' ) )
        {
           
$memory_limit = ini_get('memory_limit');
            if (
$memory_limit == -1 )
            {
               
$memoryUnlimited = TRUE;
            }
            else
            {
                if (
preg_match('/^(\d+)(.)$/', $memory_limit, $matches ) )
                {
                    if (
$matches[2] == 'G' )
                    {
                       
$memory_limit = $matches[1] * 1024 * 1024 * 1024;
                    }
                    elseif (
$matches[2] == 'M' )
                    {
                       
$memory_limit = $matches[1] * 1024 * 1024;
                    }
                    elseif (
$matches[2] == 'K' )
                    {
                       
$memory_limit = $matches[1] * 1024;
                    }
                }
               
$memoryLeft = $memory_limit - memory_get_usage( TRUE );
            }
        }
       
$memoryTheLastRunTook = 0;
       
       
/* Run until we run out of time or hit our limit */
       
$calls = 0;
        do
        {
           
/* Start a timer */
           
$timer = microtime( TRUE );
           
$memoryTimer = function_exists( 'memory_get_usage' ) ? memory_get_usage( TRUE ) : 0;
           
           
/* Execute */
           
if ( call_user_func( $callback ) === FALSE )
            {
                break;
            }

           
$calls++;

           
/* If we have a limit and we've hit it, then stop now */
           
if( $limit !== NULL AND $calls >= $limit )
            {
                break;
            }
           
           
/* Decrease the time left */
           
$timeTheLastRunTook = round( ( microtime( TRUE ) - $timer ), 2 );
           
$timeLeft -= $timeTheLastRunTook;
           
$memoryTheLastRunTook = function_exists( 'memory_get_usage' ) ? ( memory_get_usage( TRUE ) - $memoryTimer ) : 0;
            if ( !
$memoryUnlimited )
            {
               
$memoryLeft = $memory_limit - memory_get_usage( TRUE );
            }
           
        }
        while (
$timeLeft > $timeTheLastRunTook and ( $memoryUnlimited or $memoryLeft > $memoryTheLastRunTook ) );
    }
   
   
/**
     * Unlock
     *
     * @return    void
     */
   
public function unlock()
    {
        if (
$this->running )
        {
           
$this->running = FALSE;
            if (
$this->lock_count < 3 ) // Allowing this to grow infinitely will eventually overflow. The warning triggers at 3, so we only need to know if it's more than 3 or not.
           
{
               
$this->lock_count++;
            }
           
$this->next_run = \IPS\DateTime::create()->add( new \DateInterval( $this->frequency ) )->getTimestamp();
           
$this->save();
           
$this->cleanup();
        }
    }
   
   
/**
     * Cleanup
     *
     * If your task takes longer than 15 minutes to run, this method
     * will be called before execute(). Use it to clean up anything which
     * may not have been done
     *
     * @return    void
     */
   
public function cleanup()
    {
       
    }
   
   
/* Dev management */
   
    /**
     * Dev Table
     *
     * @param    string            $json                Path to JSON file
     * @param    \IPS\Http\Url    $url                URL to page
     * @param    string            $taskDirectory        Directory where PHP files are stored
     * @param    string            $subpackage            The value to use for the subpackage in the task file's header
     * @param    string            $namespace            The namespace for the task file
     * @param    int|string        $appKeyOrPluginId    If taks belongs to an application, it's key, or if a plun, it's ID
     * @return    string
     */
   
public static function devTable( $json, $url, $taskDirectory, $subpackage, $namespace, $appKeyOrPluginId )
    {
        if ( !
file_exists( $json ) )
        {
            \
file_put_contents( $json, json_encode( array() ) );
        }
       
        switch ( \
IPS\Request::i()->taskTable )
        {
            case
'form':
           
               
$form = new \IPS\Helpers\Form;
               
               
$current = NULL;
                if ( isset( \
IPS\Request::i()->key ) )
                {
                   
$tasks = json_decode( file_get_contents( $json ), TRUE );
                    if (
array_key_exists( \IPS\Request::i()->key, $tasks ) )
                    {
                       
$current = array(
                           
'dev_task_key'            => \IPS\Request::i()->key,
                           
'dev_task_frequency'    => new \DateInterval( $tasks[ \IPS\Request::i()->key ] )
                        );

                        try
                        {
                           
$current['id']    = \IPS\Db::i()->select( 'id', 'core_tasks', array( '`key`=?', \IPS\Request::i()->key ) )->first();
                        }
                        catch( \
UnderflowException $e ){}
                       
                       
$form->hiddenValues['old'] = $current['dev_task_key'];
                    }
                    unset(
$tasks );                    
                }
       
               
$form->add( new \IPS\Helpers\Form\Text( 'dev_task_key', $current ? $current['dev_task_key'] : NULL, TRUE, array( 'maxLength' => 255, 'regex' => '/^[a-z0-9_]*$/i' ), function( $val ) use ( $current )
                {
                   
$where = array( array( '`key`=?', $val ) );

                    if ( isset(
$current['id'] ) )
                    {
                       
$where[] = array( 'id<>?', $current['id'] );
                    }
                   
                    if ( \
IPS\Db::i()->select( 'count(*)', 'core_tasks', $where )->first() )
                    {
                        throw new \
DomainException( 'dev_task_key_err' );
                    }
                } ) );
               
$form->add( new \IPS\Helpers\Form\Custom( 'dev_task_frequency', $current ? $current['dev_task_frequency'] : NULL, TRUE, array(
                   
'getHtml' => function( $element )
                    {
                        return \
IPS\Theme::i()->getTemplate( 'forms', 'core' )->dateinterval( $element->name, $element->value ?: new \DateInterval( 'P0D' ) );
                    },
                   
'formatValue' => function ( $element )
                    {
                        if ( !(
$element->value instanceof \DateInterval ) )
                        {
                            if( !empty(
$element->value) )
                            {
                                try
                                {
                                   
$interval    = new \DateInterval( "P{$element->value['y']}Y{$element->value['m']}M{$element->value['d']}DT{$element->value['h']}H{$element->value['i']}M{$element->value['s']}S" );
                                }
                                catch( \
Exception $e )
                                {
                                   
$interval    = \DateInterval::createFromDateString('1 day');
                                }
                            }
                            else
                            {
                               
$interval    = \DateInterval::createFromDateString('1 day');
                            }
       
                            return
$interval;
                        }
                        return
$element->value;
                    }
                ), function (
$val )
                {
                    foreach (
$val as $k => $v )
                    {
                        if (
$v )
                        {
                            return;
                        }
                    }
                    throw new \
InvalidArgumentException( 'form_required' );
                } ) );
               
                if (
$values = $form->values() )
                {
                   
/* Write PHP file */
                   
$taskFile =  $taskDirectory . "/{$values['dev_task_key']}.php";
                    if ( isset(
$values['old'] ) and $values['old'] !== $values['dev_task_key'] and file_exists( $taskDirectory . "/{$values['old']}.php" ) )
                    {
                        @
rename( $taskDirectory . "/{$values['old']}.php", $taskFile );
                        \
IPS\Db::i()->delete( 'core_tasks', array( '`key`=?', $values['old'] ) );
                    }
                    if ( !
file_exists( $taskFile ) )
                    {
                        if ( !
is_dir( $taskDirectory ) )
                        {
                           
mkdir( $taskDirectory );
                           
chmod( $taskDirectory, \IPS\IPS_FOLDER_PERMISSION );
                        }
                       
                        \
file_put_contents( $taskFile, str_replace(
                            array(
                               
'{key}',
                               
"{subpackage}\n",
                               
'{date}',
                               
'{namespace}',
                            ),
                            array(
                               
$values['dev_task_key'],
                                (
$subpackage != 'core' ) ? ( " * @subpackage\t" . $subpackage . "\n" ) : '',
                               
date( 'd M Y' ),
                               
$namespace,
                            ),
                           
file_get_contents( \IPS\ROOT_PATH . "/applications/core/data/defaults/Task.txt" )
                        ) );
                    }
                   
                   
/* Add to DB */
                   
$frequency = "P{$values['dev_task_frequency']->y}Y{$values['dev_task_frequency']->m}M{$values['dev_task_frequency']->d}DT{$values['dev_task_frequency']->h}H{$values['dev_task_frequency']->i}M{$values['dev_task_frequency']->s}S";
                    \
IPS\Db::i()->replace( 'core_tasks', array(
                       
'app'        => is_string( $appKeyOrPluginId ) ? $appKeyOrPluginId : NULL,
                       
'plugin'    => is_numeric( $appKeyOrPluginId ) ? $appKeyOrPluginId : NULL,
                       
'key'        => $values['dev_task_key'],
                       
'frequency'    => $frequency,
                       
'next_run'    => \IPS\DateTime::create()->add( new \DateInterval( $frequency ) )->getTimestamp(),
                       
'running'    => 0,
                    ) );
                   
                   
/* Add to JSON file */
                   
$tasks = json_decode( file_get_contents( $json ), TRUE );
                   
$tasks[ $values['dev_task_key'] ] = $frequency;
                    if ( isset(
$values['old'] ) and $values['old'] !== $values['dev_task_key'] and isset( $tasks[ $values['old'] ] ) )
                    {
                        unset(
$tasks[ $values['old'] ] );
                    }
                    \
file_put_contents( $json, json_encode( $tasks ) );
                   
                   
/* Redirect */
                   
\IPS\Output::i()->redirect( $url, 'saved' );
                   
                }
               
                return
$form;
       
            case
'delete':
               
               
$tasks = json_decode( file_get_contents( $json ), TRUE );
                if (
array_key_exists( \IPS\Request::i()->key, $tasks ) )
                {
                    unset(
$tasks[ \IPS\Request::i()->key ] );
                    \
file_put_contents( $json, json_encode( $tasks ) );
                   
                    if (
file_exists( $taskDirectory . "/" . \IPS\Request::i()->key . ".php" ) )
                    {
                       
unlink( $taskDirectory . "/" . \IPS\Request::i()->key . ".php" );
                    }
                   
                    \
IPS\Db::i()->delete( 'core_tasks', array( ( is_string( $appKeyOrPluginId ) ? 'app' : 'plugin' ) . '=? AND `key`=?', $appKeyOrPluginId, \IPS\Request::i()->key ) );
                }
                \
IPS\Output::i()->redirect( $url, 'saved' );
           
            default:

               
$data = array();
                foreach (
json_decode( file_get_contents( $json ), TRUE ) as $k => $f )
                {
                   
$data[ $k ] = array(
                       
'dev_task_key' => $k,
                       
'dev_task_frequency' => $f
                   
);
                }
                               
               
$table = new \IPS\Helpers\Table\Custom( $data, $url );
               
$table->rootButtons = array(
                   
'add' => array(
                       
'icon'    => 'plus',
                       
'title'    => 'add',
                       
'link'    => $url->setQueryString( 'taskTable', 'form' ),
                       
'data'    => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('add') )
                    )
                );
               
$table->rowButtons = function( $row ) use ( $url )
                {
                    return array(
                       
'edit' => array(
                           
'icon'    => 'pencil',
                           
'title'    => 'edit',
                           
'link'    => $url->setQueryString( 'taskTable', 'form' )->setQueryString( 'key', $row['dev_task_key'] ),
                           
'data'    => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('edit') )
                        ),
                       
'delete' => array(
                           
'icon'    => 'times-circle',
                           
'title'    => 'delete',
                           
'link'    => $url->setQueryString( 'taskTable', 'delete' )->setQueryString( 'key', $row['dev_task_key'] ),
                           
'data'    => array( 'delete' => '' )
                        )
                    );
                };
               
               
$table->parsers = array(
                   
'dev_task_frequency' => function( $v )
                    {
                       
$interval = new \DateInterval( $v );
                       
$return = array();
                        foreach ( array(
'y' => 'years', 'm' => 'months', 'd' => 'days', 'h' => 'hours', 'i' => 'minutes', 's' => 'seconds' ) as $k => $v )
                        {
                            if (
$interval->$k )
                            {
                               
$return[] = \IPS\Member::loggedIn()->language()->addToStack( 'every_x_' . $v, FALSE, array( 'pluralize' => array( $interval->format( '%' . $k ) ) ) );
                            }
                        }
                       
                        return \
IPS\Member::loggedIn()->language()->formatList( $return );
                    }
                );
                       
                return
$table;
           
        }
    }
   
   
/* !ActiveRecord */
   
    /**
     * @brief    [ActiveRecord] Multiton Store
     */
   
protected static $multitons;
   
   
/**
     * @brief    [ActiveRecord] Database Table
     */
   
public static $databaseTable = 'core_tasks';
   
   
/**
     * @brief    [ActiveRecord] Database ID Fields
     */
   
protected static $databaseIdFields = array( 'key' );
   
   
/**
     * @brief    [ActiveRecord] Multiton Map
     */
   
protected static $multitonMap    = array();
   
   
/**
     * Construct ActiveRecord from database row
     *
     * @param    array    $data                            Row from database table
     * @param    bool    $updateMultitonStoreIfExists    Replace current object in multiton store if it already exists there?
     * @return    static
     */
   
public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE )
    {        
       
/* Initiate an object */
       
if ( $data['app'] )
        {
           
$classname =  'IPS\\' . $data['app'] . '\tasks\\' . $data['key'];
        }
        else
        {
           
$plugin = \IPS\Plugin::load( $data['plugin'] );
            require_once \
IPS\ROOT_PATH . '/plugins/' . $plugin->location . '/tasks/' . $data['key'] . '.php';
           
$classname = 'IPS\pluginTasks\\' . $data['key'];
            \
IPS\IPS::monkeyPatch( 'IPS\pluginTasks', $data['key'] );
        }
       
        if ( !
class_exists( $classname ) )
        {
            throw new \
RuntimeException;
        }
   
       
$obj = new $classname;
       
$obj->_new = FALSE;
       
       
/* Import data */
       
foreach ( $data as $k => $v )
        {
            if( static::
$databasePrefix )
            {
               
$k = \substr( $k, \strlen( static::$databasePrefix ) );
            }
           
           
$obj->_data[ $k ] = $v;
        }
       
$obj->changed = array();
               
       
/* Return */
       
return $obj;
    }

}