Version 5.0.0 beta 1
This commit is contained in:
1 parent
25ddeb65d6
commit
15c7beabc5
6736 files changed
+627902
-497943
No files matched your search
+146
-382
@@ -11,43 +11,98 @@
|
||||
namespace IPS;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
|
||||
use DateInterval;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use IPS\core\AdminNotification;
|
||||
use IPS\Patterns\ActiveRecord;
|
||||
use OutOfRangeException;
|
||||
use RuntimeException;
|
||||
use function count;
|
||||
use function defined;
|
||||
use function function_exists;
|
||||
use function is_array;
|
||||
use function is_null;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Task Model
|
||||
*/
|
||||
class _Task extends \IPS\Patterns\ActiveRecord
|
||||
class Task extends ActiveRecord
|
||||
{
|
||||
/**
|
||||
* 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 ActiveRecord|static
|
||||
*/
|
||||
public static function constructFromData( array $data, bool $updateMultitonStoreIfExists = TRUE ): ActiveRecord|static
|
||||
{
|
||||
/* Initiate an object */
|
||||
$classname = null;
|
||||
if ( $data['app'] )
|
||||
{
|
||||
$classname = 'IPS\\' . $data['app'] . '\tasks\\' . $data['key'];
|
||||
}
|
||||
|
||||
if ( $classname === null OR ! 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a background queue task
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function runQueue()
|
||||
public static function runQueue(): mixed
|
||||
{
|
||||
/* Get all enabled applications */
|
||||
$enabledApps = array();
|
||||
foreach( \IPS\Application::enabledApplications() as $app )
|
||||
foreach( Application::enabledApplications() as $app )
|
||||
{
|
||||
$enabledApps[] = $app->directory;
|
||||
}
|
||||
|
||||
/* @note SELECT_FROM_WRITE_SERVER: Avoid race conditions with the task locking/finishing */
|
||||
$queueData = \IPS\Db::i()->select( '*', 'core_queue', array( \IPS\Db::i()->in('app', $enabledApps ) ), 'priority ASC, RAND()', 1, NULL, NULL, \IPS\Db::SELECT_FROM_WRITE_SERVER )->first();
|
||||
$queueData = Db::i()->select( '*', 'core_queue', array( Db::i()->in('app', $enabledApps ) ), 'priority ASC, RAND()', 1, NULL, NULL, Db::SELECT_FROM_WRITE_SERVER )->first();
|
||||
$newOffset = 0;
|
||||
|
||||
$queueData['_originalOffset'] = $queueData['offset'];
|
||||
|
||||
try
|
||||
{
|
||||
$extensions = \IPS\Application::load( $queueData['app'] )->extensions( 'core', 'Queue', FALSE );
|
||||
$extensions = Application::load( $queueData['app'] )->extensions( 'core', 'Queue', FALSE );
|
||||
if ( !isset( $extensions[ $queueData['key'] ] ) )
|
||||
{
|
||||
throw new \IPS\Task\Queue\OutOfRangeException;
|
||||
throw new Task\Queue\OutOfRangeException;
|
||||
}
|
||||
|
||||
$class = new $extensions[ $queueData['key'] ];
|
||||
@@ -58,10 +113,10 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
/* 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 ) )
|
||||
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'] ) );
|
||||
Log::log( $queueData['key'] . " returned a NULL offset - tasks should throw \\IPS\\Task\\Queue\\OutOfRangeException when they are finished", 'runQueue_log' );
|
||||
Db::i()->delete( 'core_queue', array( 'id=?', $queueData['id'] ) );
|
||||
|
||||
/* Do we have a post-completion callback? */
|
||||
if( method_exists( $class, 'postComplete' ) )
|
||||
@@ -72,9 +127,9 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
/* 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' );
|
||||
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'] ) );
|
||||
Db::i()->update( 'core_queue', array( 'offset' => $newOffset ), array( 'id=?', $queueData['id'] ) );
|
||||
|
||||
$newData = json_encode( $json );
|
||||
|
||||
@@ -82,16 +137,16 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
if ( $newData !== $queueData['data'] )
|
||||
{
|
||||
$queueData['data'] = $newData;
|
||||
\IPS\Db::i()->update( 'core_queue', array( 'data' => $newData ), array( 'id=?', $queueData['id'] ) );
|
||||
Db::i()->update( 'core_queue', array( 'data' => $newData ), array( 'id=?', $queueData['id'] ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
/* This means the task is done */
|
||||
catch( \IPS\Task\Queue\OutOfRangeException $e )
|
||||
catch(Task\Queue\OutOfRangeException $e )
|
||||
{
|
||||
\IPS\Log::debug( $queueData['key'] . " completed successfully", 'runQueue_log' );
|
||||
Log::debug( $queueData['key'] . " completed successfully", 'runQueue_log' );
|
||||
|
||||
\IPS\Db::i()->delete( 'core_queue', array( 'id=?', $queueData['id'] ) );
|
||||
Db::i()->delete( 'core_queue', array( 'id=?', $queueData['id'] ) );
|
||||
|
||||
/* Do we have a post-completion callback? */
|
||||
if( isset( $class ) AND method_exists( $class, 'postComplete' ) )
|
||||
@@ -100,11 +155,11 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
}
|
||||
/* Catch any OORE in the task and transform the exception so that we can display an error */
|
||||
catch( \OutOfRangeException $e )
|
||||
catch( OutOfRangeException $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'queue_oore');
|
||||
Log::log( $e, 'queue_oore');
|
||||
|
||||
throw new \RuntimeException( $e->getMessage() );
|
||||
throw new RuntimeException( $e->getMessage() );
|
||||
}
|
||||
|
||||
return $queueData;
|
||||
@@ -113,56 +168,49 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
/**
|
||||
* 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
|
||||
* @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|null $data Data necessary for processing
|
||||
* @param int $priority Run order. Values 1 to 5 are allowed, 1 being highest priority.
|
||||
* @param mixed $checkForDuplicationKeys Pass keys to check to prevent duplicate queue tasks being added
|
||||
* @return void
|
||||
* @throws \InvalidArgumentException If $app or $key is invalid
|
||||
* @throws InvalidArgumentException If $app or $key is invalid
|
||||
*/
|
||||
public static function queue( $app, $key, $data = NULL, $priority=5, $checkForDuplicationKeys=NULL )
|
||||
public static function queue( string $app, string $key, mixed $data = NULL, int $priority=5, mixed $checkForDuplicationKeys=NULL ) : void
|
||||
{
|
||||
try
|
||||
{
|
||||
$extensions = \IPS\Application::load( $app )->extensions( 'core', 'Queue', FALSE );
|
||||
$extensions = Application::load( $app )->extensions( 'core', 'Queue', FALSE );
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
catch ( OutOfRangeException $e )
|
||||
{
|
||||
throw new \InvalidArgumentException;
|
||||
throw new InvalidArgumentException;
|
||||
}
|
||||
if ( !isset( $extensions[ $key ] ) )
|
||||
{
|
||||
throw new \InvalidArgumentException;
|
||||
throw new InvalidArgumentException;
|
||||
}
|
||||
|
||||
if ( method_exists( $extensions[ $key ], 'preQueueData' ) )
|
||||
{
|
||||
$class = new $extensions[ $key ];
|
||||
try
|
||||
{
|
||||
$data = $class->preQueueData( $data );
|
||||
}
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
$data = NULL;
|
||||
}
|
||||
|
||||
if ( $data === NULL )
|
||||
{
|
||||
if ( method_exists( $class, 'postComplete' ) )
|
||||
{
|
||||
$class->postComplete( $data, FALSE );
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
$class = new $extensions[ $key ];
|
||||
try
|
||||
{
|
||||
$data = $class->preQueueData( $data );
|
||||
}
|
||||
catch( OutOfRangeException $e )
|
||||
{
|
||||
$data = NULL;
|
||||
}
|
||||
|
||||
if ( $data === NULL )
|
||||
{
|
||||
$class->postComplete( [], FALSE );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( \is_array( $checkForDuplicationKeys ) and \is_array( $data ) )
|
||||
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 )
|
||||
foreach(Db::i()->select( '*', 'core_queue', array( '`app`=? AND `key`=?', $app, $key ) ) as $row )
|
||||
{
|
||||
if ( $row['data'] )
|
||||
{
|
||||
@@ -177,16 +225,16 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
}
|
||||
|
||||
if ( $got === \count( $checkForDuplicationKeys ) )
|
||||
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'] ) );
|
||||
Db::i()->delete( 'core_queue', array( 'id=?', $row['id'] ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
\IPS\Db::i()->insert( 'core_queue', array(
|
||||
Db::i()->insert( 'core_queue', array(
|
||||
'data' => json_encode( $data ),
|
||||
'date' => time(),
|
||||
'app' => $app,
|
||||
@@ -194,7 +242,7 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
'priority' => $priority
|
||||
) );
|
||||
|
||||
\IPS\Db::i()->update( 'core_tasks', array( 'enabled' => 1 ), array( '`key`=?', 'queue' ) );
|
||||
Db::i()->update( 'core_tasks', array( 'enabled' => 1 ), array( '`key`=?', 'queue' ) );
|
||||
}
|
||||
|
||||
/* !Task */
|
||||
@@ -202,13 +250,12 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
/**
|
||||
* Get next queued task
|
||||
*
|
||||
* @return \IPS\Task|NULL
|
||||
* @return Task|NULL
|
||||
*/
|
||||
public static function queued()
|
||||
public static function queued(): ?Task
|
||||
{
|
||||
$fifteenMinutesAgo = ( time() - 900 );
|
||||
/* @note SELECT_FROM_WRITE_SERVER: Avoid race conditions with the task locking/finishing */
|
||||
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 )
|
||||
foreach (Db::i()->select( '*', 'core_tasks', array( 'next_run<? AND enabled=1', ( time() + 60 ) ), 'next_run ASC', NULL, NULL, NULL, Db::SELECT_FROM_WRITE_SERVER ) as $task )
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -226,7 +273,13 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( \RuntimeException $e ) { }
|
||||
catch( Exception $e )
|
||||
{
|
||||
if( IN_DEV )
|
||||
{
|
||||
Log::log( $e, 'task_exception' );
|
||||
}
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
@@ -236,7 +289,7 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function runAndLog()
|
||||
public function runAndLog() : void
|
||||
{
|
||||
$result = NULL;
|
||||
$error = FALSE;
|
||||
@@ -245,7 +298,7 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
$result = $this->run();
|
||||
}
|
||||
catch ( \IPS\Task\Exception $e )
|
||||
catch (Task\Exception $e )
|
||||
{
|
||||
$result = $e->getMessage();
|
||||
$error = 1;
|
||||
@@ -253,7 +306,7 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
if ( $error !== FALSE or $result !== NULL )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_tasks_log', array(
|
||||
Db::i()->insert( 'core_tasks_log', array(
|
||||
'task' => $this->id,
|
||||
'error' => $error,
|
||||
'log' => json_encode( $result ),
|
||||
@@ -266,12 +319,12 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
* Run
|
||||
*
|
||||
* @return mixed Message to log or NULL
|
||||
* @throws \IPS\Task\Exception
|
||||
* @throws Task\Exception
|
||||
*/
|
||||
public function run()
|
||||
public function run(): mixed
|
||||
{
|
||||
/* Enforce Lock. If affected rows = 0, the task has been locked by another process */
|
||||
if( (int) \IPS\Db::i()->update( 'core_tasks', array( 'next_run' => time(), 'running' => 1 ), array( '`running`=0 AND `id`=?', $this->id ) ) === 0 )
|
||||
if( (int) Db::i()->update( 'core_tasks', array( 'next_run' => time(), 'running' => 1 ), array( '`running`=0 AND `id`=?', $this->id ) ) === 0 )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
@@ -279,16 +332,16 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
/* Keep the task object aligned with the database changes made above */
|
||||
$this->running = TRUE;
|
||||
$this->next_run = time();
|
||||
|
||||
|
||||
$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->next_run = DateTime::create()->add( new DateInterval( $this->frequency ) )->getTimestamp();
|
||||
$this->last_run = DateTime::create()->getTimestamp();
|
||||
$this->save();
|
||||
|
||||
\IPS\core\AdminNotification::remove( 'core', 'ConfigurationError', "taskLock-{$this->id}" );
|
||||
AdminNotification::remove( 'core', 'ConfigurationError', "taskLock-{$this->id}" );
|
||||
|
||||
return $output;
|
||||
}
|
||||
@@ -302,21 +355,21 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
* Tasks should execute within the time of a normal HTTP request.
|
||||
*
|
||||
* @return mixed Message to log or NULL
|
||||
* @throws \IPS\Task\Exception
|
||||
* @throws Task\Exception
|
||||
*/
|
||||
public function execute()
|
||||
public function execute() : mixed
|
||||
{
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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 )
|
||||
public function runUntilTimeout( callable $callback, int $limit=NULL ) : void
|
||||
{
|
||||
/* Work out the maximum execution time */
|
||||
$timeLeft = 90;
|
||||
@@ -328,7 +381,7 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
/* Factor in wait_timeout if possible */
|
||||
try
|
||||
{
|
||||
$mysqlTimeout = \IPS\Db::i()->query( "SHOW SESSION VARIABLES LIKE 'wait_timeout'" )->fetch_assoc();
|
||||
$mysqlTimeout = Db::i()->query( "SHOW SESSION VARIABLES LIKE 'wait_timeout'" )->fetch_assoc();
|
||||
$mysqlTimeout = $mysqlTimeout['Value'];
|
||||
|
||||
if( $mysqlTimeout <= $timeLeft )
|
||||
@@ -336,14 +389,14 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
$timeLeft = $mysqlTimeout - 2;
|
||||
}
|
||||
}
|
||||
catch( \IPS\Db\Exception $e ){}
|
||||
catch( Db\Exception $e ){}
|
||||
|
||||
$timeTheLastRunTook = 0;
|
||||
|
||||
/* Work out the memory limit */
|
||||
$memoryLeft = 0;
|
||||
$memoryUnlimited = FALSE;
|
||||
if ( \function_exists( 'memory_get_usage' ) )
|
||||
if ( function_exists( 'memory_get_usage' ) )
|
||||
{
|
||||
$memory_limit = ini_get('memory_limit');
|
||||
if ( $memory_limit == -1 )
|
||||
@@ -378,7 +431,7 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
/* Start a timer */
|
||||
$timer = microtime( TRUE );
|
||||
$memoryTimer = \function_exists( 'memory_get_usage' ) ? memory_get_usage( TRUE ) : 0;
|
||||
$memoryTimer = function_exists( 'memory_get_usage' ) ? memory_get_usage( TRUE ) : 0;
|
||||
|
||||
/* Execute */
|
||||
if ( $callback() === FALSE )
|
||||
@@ -397,7 +450,7 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
/* Decrease the time left */
|
||||
$timeTheLastRunTook = round( ( microtime( TRUE ) - $timer ), 2 );
|
||||
$timeLeft -= $timeTheLastRunTook;
|
||||
$memoryTheLastRunTook = \function_exists( 'memory_get_usage' ) ? ( memory_get_usage( TRUE ) - $memoryTimer ) : 0;
|
||||
$memoryTheLastRunTook = function_exists( 'memory_get_usage' ) ? ( memory_get_usage( TRUE ) - $memoryTimer ) : 0;
|
||||
if ( !$memoryUnlimited )
|
||||
{
|
||||
$memoryLeft = $memory_limit - memory_get_usage( TRUE );
|
||||
@@ -412,7 +465,7 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function unlock()
|
||||
public function unlock() : void
|
||||
{
|
||||
if ( $this->running )
|
||||
{
|
||||
@@ -423,10 +476,10 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
if ( $this->lock_count >= 3 )
|
||||
{
|
||||
\IPS\core\AdminNotification::send( 'core', 'ConfigurationError', "taskLock-{$this->id}" );
|
||||
AdminNotification::send( 'core', 'ConfigurationError', "taskLock-{$this->id}" );
|
||||
}
|
||||
}
|
||||
$this->next_run = \IPS\DateTime::create()->add( new \DateInterval( $this->frequency ) )->getTimestamp();
|
||||
$this->next_run = DateTime::create()->add( new DateInterval( $this->frequency ) )->getTimestamp();
|
||||
$this->save();
|
||||
$this->cleanup();
|
||||
}
|
||||
@@ -445,315 +498,26 @@ class _Task extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/* 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':
|
||||
\IPS\Session::i()->csrfCheck();
|
||||
|
||||
$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'] )->csrf(),
|
||||
'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;
|
||||
protected static array $multitons;
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Table
|
||||
*/
|
||||
public static $databaseTable = 'core_tasks';
|
||||
public static ?string $databaseTable = 'core_tasks';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database ID Fields
|
||||
*/
|
||||
protected static $databaseIdFields = array( 'key' );
|
||||
protected static array $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\SITE_FILES_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;
|
||||
}
|
||||
|
||||
protected static array $multitonMap = array();
|
||||
}
|
||||
Reference in new issue
Block a user