[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/administrator/components/com_scheduler/src/Model/ -> TaskModel.php (source)

   1  <?php
   2  
   3  /**
   4   * @package     Joomla.Administrator
   5   * @subpackage  com_scheduler
   6   *
   7   * @copyright   (C) 2021 Open Source Matters, Inc. <https://www.joomla.org>
   8   * @license     GNU General Public License version 2 or later; see LICENSE.txt
   9   */
  10  
  11  namespace Joomla\Component\Scheduler\Administrator\Model;
  12  
  13  use Joomla\CMS\Application\AdministratorApplication;
  14  use Joomla\CMS\Component\ComponentHelper;
  15  use Joomla\CMS\Event\AbstractEvent;
  16  use Joomla\CMS\Factory;
  17  use Joomla\CMS\Form\Form;
  18  use Joomla\CMS\Form\FormFactoryInterface;
  19  use Joomla\CMS\Language\Text;
  20  use Joomla\CMS\Log\Log;
  21  use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
  22  use Joomla\CMS\MVC\Model\AdminModel;
  23  use Joomla\CMS\Object\CMSObject;
  24  use Joomla\CMS\Plugin\PluginHelper;
  25  use Joomla\CMS\Table\Table;
  26  use Joomla\Component\Scheduler\Administrator\Helper\ExecRuleHelper;
  27  use Joomla\Component\Scheduler\Administrator\Helper\SchedulerHelper;
  28  use Joomla\Component\Scheduler\Administrator\Table\TaskTable;
  29  use Joomla\Component\Scheduler\Administrator\Task\TaskOption;
  30  use Joomla\Database\ParameterType;
  31  use Symfony\Component\OptionsResolver\Exception\AccessException;
  32  use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
  33  use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException;
  34  use Symfony\Component\OptionsResolver\OptionsResolver;
  35  
  36  // phpcs:disable PSR1.Files.SideEffects
  37  \defined('_JEXEC') or die;
  38  // phpcs:enable PSR1.Files.SideEffects
  39  
  40  /**
  41   * MVC Model to interact with the Scheduler DB.
  42   * Implements methods to add, remove, edit tasks.
  43   *
  44   * @since  4.1.0
  45   */
  46  class TaskModel extends AdminModel
  47  {
  48      /**
  49       * Maps logical states to their values in the DB
  50       * ? Do we end up using this?
  51       *
  52       * @var array
  53       * @since  4.1.0
  54       */
  55      protected const TASK_STATES = [
  56          'enabled'  => 1,
  57          'disabled' => 0,
  58          'trashed'  => -2,
  59      ];
  60  
  61      /**
  62       * The name of the  database table with task records.
  63       *
  64       * @var  string
  65       * @since 4.1.0
  66       */
  67      public const TASK_TABLE = '#__scheduler_tasks';
  68  
  69      /**
  70       * Prefix used with controller messages
  71       *
  72       * @var string
  73       * @since  4.1.0
  74       */
  75      protected $text_prefix = 'COM_SCHEDULER';
  76  
  77      /**
  78       * Type alias for content type
  79       *
  80       * @var string
  81       * @since  4.1.0
  82       */
  83      public $typeAlias = 'com_scheduler.task';
  84  
  85      /**
  86       * The Application object, for convenience
  87       *
  88       * @var AdministratorApplication $app
  89       * @since  4.1.0
  90       */
  91      protected $app;
  92  
  93      /**
  94       * The event to trigger before unlocking the data.
  95       *
  96       * @var    string
  97       * @since  4.1.0
  98       */
  99      protected $event_before_unlock = null;
 100  
 101      /**
 102       * The event to trigger after unlocking the data.
 103       *
 104       * @var    string
 105       * @since  4.1.0
 106       */
 107      protected $event_unlock = null;
 108  
 109      /**
 110       * TaskModel constructor. Needed just to set $app
 111       *
 112       * @param   array                      $config       An array of configuration options
 113       * @param   MVCFactoryInterface|null   $factory      The factory
 114       * @param   FormFactoryInterface|null  $formFactory  The form factory
 115       *
 116       * @since  4.1.0
 117       * @throws \Exception
 118       */
 119      public function __construct($config = array(), MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null)
 120      {
 121          $config['events_map'] = $config['events_map'] ?? [];
 122  
 123          $config['events_map'] = array_merge(
 124              [
 125                  'save'     => 'task',
 126                  'validate' => 'task',
 127                  'unlock'   => 'task',
 128              ],
 129              $config['events_map']
 130          );
 131  
 132          if (isset($config['event_before_unlock'])) {
 133              $this->event_before_unlock = $config['event_before_unlock'];
 134          } elseif (empty($this->event_before_unlock)) {
 135              $this->event_before_unlock = 'onContentBeforeUnlock';
 136          }
 137  
 138          if (isset($config['event_unlock'])) {
 139              $this->event_unlock = $config['event_unlock'];
 140          } elseif (empty($this->event_unlock)) {
 141              $this->event_unlock = 'onContentUnlock';
 142          }
 143  
 144          $this->app = Factory::getApplication();
 145  
 146          parent::__construct($config, $factory, $formFactory);
 147      }
 148  
 149      /**
 150       * Fetches the form object associated with this model. By default,
 151       * loads the corresponding data from the DB and binds it with the form.
 152       *
 153       * @param   array  $data      Data that needs to go into the form
 154       * @param   bool   $loadData  Should the form load its data from the DB?
 155       *
 156       * @return Form|boolean  A JForm object on success, false on failure.
 157       *
 158       * @since  4.1.0
 159       * @throws \Exception
 160       */
 161      public function getForm($data = array(), $loadData = true)
 162      {
 163          Form::addFieldPath(JPATH_ADMINISTRATOR . 'components/com_scheduler/src/Field');
 164  
 165          /**
 166           *  loadForm() (defined by FormBehaviourTrait) also loads the form data by calling
 167           *  loadFormData() : $data [implemented here] and binds it to the form by calling
 168           *  $form->bind($data).
 169           */
 170          $form = $this->loadForm('com_scheduler.task', 'task', ['control' => 'jform', 'load_data' => $loadData]);
 171  
 172          if (empty($form)) {
 173              return false;
 174          }
 175  
 176          $user = $this->app->getIdentity();
 177  
 178          // If new entry, set task type from state
 179          if ($this->getState('task.id', 0) === 0 && $this->getState('task.type') !== null) {
 180              $form->setValue('type', null, $this->getState('task.type'));
 181          }
 182  
 183          // @todo : Check if this is working as expected for new items (id == 0)
 184          if (!$user->authorise('core.edit.state', 'com_scheduler.task.' . $this->getState('task.id'))) {
 185              // Disable fields
 186              $form->setFieldAttribute('state', 'disabled', 'true');
 187  
 188              // No "hacking" ._.
 189              $form->setFieldAttribute('state', 'filter', 'unset');
 190          }
 191  
 192          return $form;
 193      }
 194  
 195      /**
 196       * Determine whether a record may be deleted taking into consideration
 197       * the user's permissions over the record.
 198       *
 199       * @param   object  $record  The database row/record in question
 200       *
 201       * @return  boolean  True if the record may be deleted
 202       *
 203       * @since  4.1.0
 204       * @throws \Exception
 205       */
 206      protected function canDelete($record): bool
 207      {
 208          // Record doesn't exist, can't delete
 209          if (empty($record->id)) {
 210              return false;
 211          }
 212  
 213          return $this->app->getIdentity()->authorise('core.delete', 'com_scheduler.task.' . $record->id);
 214      }
 215  
 216      /**
 217       * Populate the model state, we use these instead of toying with input or the global state
 218       *
 219       * @return  void
 220       *
 221       * @since  4.1.0
 222       * @throws \Exception
 223       */
 224      protected function populateState(): void
 225      {
 226          $app = $this->app;
 227  
 228          $taskId   = $app->getInput()->getInt('id');
 229          $taskType = $app->getUserState('com_scheduler.add.task.task_type');
 230  
 231          // @todo: Remove this. Get the option through a helper call.
 232          $taskOption = $app->getUserState('com_scheduler.add.task.task_option');
 233  
 234          $this->setState('task.id', $taskId);
 235          $this->setState('task.type', $taskType);
 236          $this->setState('task.option', $taskOption);
 237  
 238          // Load component params, though com_scheduler does not (yet) have any params
 239          $cParams = ComponentHelper::getParams($this->option);
 240          $this->setState('params', $cParams);
 241      }
 242  
 243      /**
 244       * Don't need to define this method since the parent getTable()
 245       * implicitly deduces $name and $prefix anyways. This makes the object
 246       * more transparent though.
 247       *
 248       * @param   string  $name     Name of the table
 249       * @param   string  $prefix   Class prefix
 250       * @param   array   $options  Model config array
 251       *
 252       * @return Table
 253       *
 254       * @since  4.1.0
 255       * @throws \Exception
 256       */
 257      public function getTable($name = 'Task', $prefix = 'Table', $options = array()): Table
 258      {
 259          return parent::getTable($name, $prefix, $options);
 260      }
 261  
 262      /**
 263       * Fetches the data to be injected into the form
 264       *
 265       * @return object  Associative array of form data.
 266       *
 267       * @since  4.1.0
 268       * @throws \Exception
 269       */
 270      protected function loadFormData()
 271      {
 272          $data = $this->app->getUserState('com_scheduler.edit.task.data', array());
 273  
 274          // If the data from UserState is empty, we fetch it with getItem()
 275          if (empty($data)) {
 276              /** @var CMSObject $data */
 277              $data = $this->getItem();
 278  
 279              // @todo : further data processing goes here
 280  
 281              // For a fresh object, set exec-day and exec-time
 282              if (!($data->id ?? 0)) {
 283                  $data->execution_rules['exec-day']  = gmdate('d');
 284                  $data->execution_rules['exec-time'] = gmdate('H:i');
 285              }
 286          }
 287  
 288          // Let plugins manipulate the data
 289          $this->preprocessData('com_scheduler.task', $data, 'task');
 290  
 291          return $data;
 292      }
 293  
 294      /**
 295       * Overloads the parent getItem() method.
 296       *
 297       * @param   integer  $pk  Primary key
 298       *
 299       * @return  object|boolean  Object on success, false on failure
 300       *
 301       * @since  4.1.0
 302       * @throws \Exception
 303       */
 304      public function getItem($pk = null)
 305      {
 306          $item = parent::getItem($pk);
 307  
 308          if (!\is_object($item)) {
 309              return false;
 310          }
 311  
 312          // Parent call leaves `execution_rules` and `cron_rules` JSON encoded
 313          $item->set('execution_rules', json_decode($item->get('execution_rules', '')));
 314          $item->set('cron_rules', json_decode($item->get('cron_rules', '')));
 315  
 316          $taskOption = SchedulerHelper::getTaskOptions()->findOption(
 317              ($item->id ?? 0) ? ($item->type ?? 0) : $this->getState('task.type')
 318          );
 319  
 320          $item->set('taskOption', $taskOption);
 321  
 322          return $item;
 323      }
 324  
 325      /**
 326       * Get a task from the database, only if an exclusive "lock" on the task can be acquired.
 327       * The method supports options to customise the limitations on the fetch.
 328       *
 329       * @param   array  $options  Array with options to fetch the task:
 330       *                           1. `id`: Optional id of the task to fetch.
 331       *                           2. `allowDisabled`: If true, disabled tasks can also be fetched.
 332       *                           (default: false)
 333       *                           3. `bypassScheduling`: If true, tasks that are not due can also be
 334       *                           fetched. Should only be true if an `id` is targeted instead of the
 335       *                           task queue. (default: false)
 336       *                           4. `allowConcurrent`: If true, fetches even when another task is
 337       *                           running ('locked'). (default: false)
 338       *                           5. `includeCliExclusive`: If true, can also fetch CLI exclusive tasks. (default: true)
 339       *
 340       * @return ?\stdClass  Task entry as in the database.
 341       *
 342       * @since   4.1.0
 343       * @throws UndefinedOptionsException|InvalidOptionsException
 344       * @throws \RuntimeException
 345       */
 346      public function getTask(array $options = []): ?\stdClass
 347      {
 348          $resolver = new OptionsResolver();
 349  
 350          try {
 351              $this->configureTaskGetterOptions($resolver);
 352          } catch (\Exception $e) {
 353          }
 354  
 355          try {
 356              $options = $resolver->resolve($options);
 357          } catch (\Exception $e) {
 358              if ($e instanceof UndefinedOptionsException || $e instanceof InvalidOptionsException) {
 359                  throw $e;
 360              }
 361          }
 362  
 363          $db  = $this->getDatabase();
 364          $now = Factory::getDate()->toSql();
 365  
 366          // Get lock on the table to help with concurrency issues
 367          $db->lockTable(self::TASK_TABLE);
 368  
 369          // If concurrency is not allowed, we only get a task if another one does not have a "lock"
 370          if (!$options['allowConcurrent']) {
 371              // Get count of locked (presumed running) tasks
 372              $lockCountQuery = $db->getQuery(true)
 373                  ->from($db->quoteName(self::TASK_TABLE))
 374                  ->select('COUNT(id)')
 375                  ->where($db->quoteName('locked') . ' IS NOT NULL');
 376  
 377              try {
 378                  $runningCount = $db->setQuery($lockCountQuery)->loadResult();
 379              } catch (\RuntimeException $e) {
 380                  $db->unlockTables();
 381  
 382                  return null;
 383              }
 384  
 385              if ($runningCount !== 0) {
 386                  $db->unlockTables();
 387  
 388                  return null;
 389              }
 390          }
 391  
 392          $lockQuery = $db->getQuery(true);
 393  
 394          $lockQuery->update($db->quoteName(self::TASK_TABLE))
 395              ->set($db->quoteName('locked') . ' = :now1')
 396              ->bind(':now1', $now);
 397  
 398          // Array of all active routine ids
 399          $activeRoutines = array_map(
 400              static function (TaskOption $taskOption): string {
 401                  return $taskOption->id;
 402              },
 403              SchedulerHelper::getTaskOptions()->options
 404          );
 405  
 406          // "Orphaned" tasks are not a part of the task queue!
 407          $lockQuery->whereIn($db->quoteName('type'), $activeRoutines, ParameterType::STRING);
 408  
 409          // If directed, exclude CLI exclusive tasks
 410          if (!$options['includeCliExclusive']) {
 411              $lockQuery->where($db->quoteName('cli_exclusive') . ' = 0');
 412          }
 413  
 414          if (!$options['bypassScheduling']) {
 415              $lockQuery->where($db->quoteName('next_execution') . ' <= :now2')
 416                  ->bind(':now2', $now);
 417          }
 418  
 419          if ($options['allowDisabled']) {
 420              $lockQuery->whereIn($db->quoteName('state'), [0, 1]);
 421          } else {
 422              $lockQuery->where($db->quoteName('state') . ' = 1');
 423          }
 424  
 425          if ($options['id'] > 0) {
 426              $lockQuery->where($db->quoteName('id') . ' = :taskId')
 427                  ->bind(':taskId', $options['id'], ParameterType::INTEGER);
 428          } else {
 429              // Pick from the front of the task queue if no 'id' is specified
 430              // Get the id of the next task in the task queue
 431              $idQuery = $db->getQuery(true)
 432                  ->from($db->quoteName(self::TASK_TABLE))
 433                  ->select($db->quoteName('id'))
 434                  ->where($db->quoteName('state') . ' = 1')
 435                  ->order($db->quoteName('priority') . ' DESC')
 436                  ->order($db->quoteName('next_execution') . ' ASC')
 437                  ->setLimit(1);
 438  
 439              try {
 440                  $ids = $db->setQuery($idQuery)->loadColumn();
 441              } catch (\RuntimeException $e) {
 442                  $db->unlockTables();
 443  
 444                  return null;
 445              }
 446  
 447              if (count($ids) === 0) {
 448                  $db->unlockTables();
 449  
 450                  return null;
 451              }
 452  
 453              $lockQuery->whereIn($db->quoteName('id'), $ids);
 454          }
 455  
 456          try {
 457              $db->setQuery($lockQuery)->execute();
 458          } catch (\RuntimeException $e) {
 459          } finally {
 460              $affectedRows = $db->getAffectedRows();
 461  
 462              $db->unlockTables();
 463          }
 464  
 465          if ($affectedRows != 1) {
 466              /*
 467               // @todo
 468              // ? Fatal failure handling here?
 469              // ! Question is, how? If we check for tasks running beyond there time here, we have no way of
 470              //  ! what's already been notified (since we're not auto-unlocking/recovering tasks anymore).
 471              // The solution __may__ be in a "last_successful_finish" (or something) column.
 472              */
 473  
 474              return null;
 475          }
 476  
 477          $getQuery = $db->getQuery(true);
 478  
 479          $getQuery->select('*')
 480              ->from($db->quoteName(self::TASK_TABLE))
 481              ->where($db->quoteName('locked') . ' = :now')
 482              ->bind(':now', $now);
 483  
 484          $task = $db->setQuery($getQuery)->loadObject();
 485  
 486          $task->execution_rules = json_decode($task->execution_rules);
 487          $task->cron_rules      = json_decode($task->cron_rules);
 488  
 489          $task->taskOption = SchedulerHelper::getTaskOptions()->findOption($task->type);
 490  
 491          return $task;
 492      }
 493  
 494      /**
 495       * Set up an {@see OptionsResolver} to resolve options compatible with the {@see GetTask()} method.
 496       *
 497       * @param   OptionsResolver  $resolver  The {@see OptionsResolver} instance to set up.
 498       *
 499       * @return OptionsResolver
 500       *
 501       * @since 4.1.0
 502       * @throws AccessException
 503       */
 504      public static function configureTaskGetterOptions(OptionsResolver $resolver): OptionsResolver
 505      {
 506          $resolver->setDefaults(
 507              [
 508                  'id'                  => 0,
 509                  'allowDisabled'       => false,
 510                  'bypassScheduling'    => false,
 511                  'allowConcurrent'     => false,
 512                  'includeCliExclusive' => true,
 513              ]
 514          )
 515              ->setAllowedTypes('id', 'numeric')
 516              ->setAllowedTypes('allowDisabled', 'bool')
 517              ->setAllowedTypes('bypassScheduling', 'bool')
 518              ->setAllowedTypes('allowConcurrent', 'bool')
 519              ->setAllowedTypes('includeCliExclusive', 'bool');
 520  
 521          return $resolver;
 522      }
 523  
 524      /**
 525       * @param   array  $data  The form data
 526       *
 527       * @return  boolean  True on success, false on failure
 528       *
 529       * @since  4.1.0
 530       * @throws \Exception
 531       */
 532      public function save($data): bool
 533      {
 534          $id    = (int) ($data['id'] ?? $this->getState('task.id'));
 535          $isNew = $id === 0;
 536  
 537          // Clean up execution rules
 538          $data['execution_rules'] = $this->processExecutionRules($data['execution_rules']);
 539  
 540          // If a new entry, we'll have to put in place a pseudo-last_execution
 541          if ($isNew) {
 542              $basisDayOfMonth = $data['execution_rules']['exec-day'];
 543              [$basisHour, $basisMinute] = explode(':', $data['execution_rules']['exec-time']);
 544  
 545              $data['last_execution'] = Factory::getDate('now', 'GMT')->format('Y-m')
 546                  . "-$basisDayOfMonth $basisHour:$basisMinute:00";
 547          } else {
 548              $data['last_execution'] = $this->getItem($id)->last_execution;
 549          }
 550  
 551          // Build the `cron_rules` column from `execution_rules`
 552          $data['cron_rules'] = $this->buildExecutionRules($data['execution_rules']);
 553  
 554          // `next_execution` would be null if scheduling is disabled with the "manual" rule!
 555          $data['next_execution'] = (new ExecRuleHelper($data))->nextExec();
 556  
 557          if ($isNew) {
 558              $data['last_execution'] = null;
 559          }
 560  
 561          // If no params, we set as empty array.
 562          // ? Is this the right place to do this
 563          $data['params'] = $data['params'] ?? [];
 564  
 565          // Parent method takes care of saving to the table
 566          return parent::save($data);
 567      }
 568  
 569      /**
 570       * Clean up and standardise execution rules
 571       *
 572       * @param   array  $unprocessedRules  The form data [? can just replace with execution_interval]
 573       *
 574       * @return array  Processed rules
 575       *
 576       * @since  4.1.0
 577       */
 578      private function processExecutionRules(array $unprocessedRules): array
 579      {
 580          $executionRules = $unprocessedRules;
 581  
 582          $ruleType       = $executionRules['rule-type'];
 583          $retainKeys     = ['rule-type', $ruleType, 'exec-day', 'exec-time'];
 584          $executionRules = array_intersect_key($executionRules, array_flip($retainKeys));
 585  
 586          // Default to current date-time in UTC/GMT as the basis
 587          $executionRules['exec-day']  = $executionRules['exec-day'] ?: (string) gmdate('d');
 588          $executionRules['exec-time'] = $executionRules['exec-time'] ?: (string) gmdate('H:i');
 589  
 590          // If custom ruleset, sort it
 591          // ? Is this necessary
 592          if ($ruleType === 'cron-expression') {
 593              foreach ($executionRules['cron-expression'] as &$values) {
 594                  sort($values);
 595              }
 596          }
 597  
 598          return $executionRules;
 599      }
 600  
 601      /**
 602       * Private method to build execution expression from input execution rules.
 603       * This expression is used internally to determine execution times/conditions.
 604       *
 605       * @param   array  $executionRules  Execution rules from the Task form, post-processing.
 606       *
 607       * @return array
 608       *
 609       * @since  4.1.0
 610       * @throws \Exception
 611       */
 612      private function buildExecutionRules(array $executionRules): array
 613      {
 614          // Maps interval strings, use with sprintf($map[intType], $interval)
 615          $intervalStringMap = [
 616              'minutes' => 'PT%dM',
 617              'hours'   => 'PT%dH',
 618              'days'    => 'P%dD',
 619              'months'  => 'P%dM',
 620              'years'   => 'P%dY',
 621          ];
 622  
 623          $ruleType        = $executionRules['rule-type'];
 624          $ruleClass       = strpos($ruleType, 'interval') === 0 ? 'interval' : $ruleType;
 625          $buildExpression = '';
 626  
 627          if ($ruleClass === 'interval') {
 628              // Rule type for intervals interval-<minute/hours/...>
 629              $intervalType    = explode('-', $ruleType)[1];
 630              $interval        = $executionRules["interval-$intervalType"];
 631              $buildExpression = sprintf($intervalStringMap[$intervalType], $interval);
 632          }
 633  
 634          if ($ruleClass === 'cron-expression') {
 635              // ! custom matches are disabled in the form
 636              $matches         = $executionRules['cron-expression'];
 637              $buildExpression .= $this->wildcardIfMatch($matches['minutes'], range(0, 59), true);
 638              $buildExpression .= ' ' . $this->wildcardIfMatch($matches['hours'], range(0, 23), true);
 639              $buildExpression .= ' ' . $this->wildcardIfMatch($matches['days_month'], range(1, 31), true);
 640              $buildExpression .= ' ' . $this->wildcardIfMatch($matches['months'], range(1, 12), true);
 641              $buildExpression .= ' ' . $this->wildcardIfMatch($matches['days_week'], range(0, 6), true);
 642          }
 643  
 644          return [
 645              'type' => $ruleClass,
 646              'exp'  => $buildExpression,
 647          ];
 648      }
 649  
 650      /**
 651       * This method releases "locks" on a set of tasks from the database.
 652       * These locks are pseudo-locks that are used to keep a track of running tasks. However, they require require manual
 653       * intervention to release these locks in cases such as when a task process crashes, leaving the task "locked".
 654       *
 655       * @param   array  $pks  A list of the primary keys to unlock.
 656       *
 657       * @return  boolean  True on success.
 658       *
 659       * @since   4.1.0
 660       * @throws \RuntimeException|\UnexpectedValueException|\BadMethodCallException
 661       */
 662      public function unlock(array &$pks): bool
 663      {
 664          /** @var TaskTable $table */
 665          $table = $this->getTable();
 666  
 667          $user = Factory::getApplication()->getIdentity();
 668  
 669          $context = $this->option . '.' . $this->name;
 670  
 671          // Include the plugins for the change of state event.
 672          PluginHelper::importPlugin($this->events_map['unlock']);
 673  
 674          // Access checks.
 675          foreach ($pks as $i => $pk) {
 676              $table->reset();
 677  
 678              if ($table->load($pk)) {
 679                  if (!$this->canEditState($table)) {
 680                      // Prune items that you can't change.
 681                      unset($pks[$i]);
 682                      Log::add(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), Log::WARNING, 'jerror');
 683  
 684                      return false;
 685                  }
 686  
 687                  // Prune items that are already at the given state.
 688                  $lockedColumnName = $table->getColumnAlias('locked');
 689  
 690                  if (property_exists($table, $lockedColumnName) && \is_null($table->get($lockedColumnName))) {
 691                      unset($pks[$i]);
 692                  }
 693              }
 694          }
 695  
 696          // Check if there are items to change.
 697          if (!\count($pks)) {
 698              return true;
 699          }
 700  
 701          $event = AbstractEvent::create(
 702              $this->event_before_unlock,
 703              [
 704                  'subject' => $this,
 705                  'context' => $context,
 706                  'pks'     => $pks,
 707              ]
 708          );
 709  
 710          try {
 711              Factory::getApplication()->getDispatcher()->dispatch($this->event_before_unlock, $event);
 712          } catch (\RuntimeException $e) {
 713              $this->setError($e->getMessage());
 714  
 715              return false;
 716          }
 717  
 718          // Attempt to unlock the records.
 719          if (!$table->unlock($pks, $user->id)) {
 720              $this->setError($table->getError());
 721  
 722              return false;
 723          }
 724  
 725          // Trigger the after unlock event
 726          $event = AbstractEvent::create(
 727              $this->event_unlock,
 728              [
 729                  'subject' => $this,
 730                  'context' => $context,
 731                  'pks'     => $pks,
 732              ]
 733          );
 734  
 735          try {
 736              Factory::getApplication()->getDispatcher()->dispatch($this->event_unlock, $event);
 737          } catch (\RuntimeException $e) {
 738              $this->setError($e->getMessage());
 739  
 740              return false;
 741          }
 742  
 743          // Clear the component's cache
 744          $this->cleanCache();
 745  
 746          return true;
 747      }
 748  
 749      /**
 750       * Determine if an array is populated by all its possible values by comparison to a reference array, if found a
 751       * match a wildcard '*' is returned.
 752       *
 753       * @param   array  $target       The target array
 754       * @param   array  $reference    The reference array, populated by the complete set of possible values in $target
 755       * @param   bool   $targetToInt  If true, converts $target array values to integers before comparing
 756       *
 757       * @return string  A wildcard string if $target is fully populated, else $target itself.
 758       *
 759       * @since  4.1.0
 760       */
 761      private function wildcardIfMatch(array $target, array $reference, bool $targetToInt = false): string
 762      {
 763          if ($targetToInt) {
 764              $target = array_map(
 765                  static function (string $x): int {
 766                      return (int) $x;
 767                  },
 768                  $target
 769              );
 770          }
 771  
 772          $isMatch = array_diff($reference, $target) === [];
 773  
 774          return $isMatch ? "*" : implode(',', $target);
 775      }
 776  
 777      /**
 778       * Method to allow derived classes to preprocess the form.
 779       *
 780       * @param   Form    $form   A Form object.
 781       * @param   mixed   $data   The data expected for the form.
 782       * @param   string  $group  The name of the plugin group to import (defaults to "content").
 783       *
 784       * @return  void
 785       *
 786       * @since   4.1.0
 787       * @throws  \Exception if there is an error in the form event.
 788       */
 789      protected function preprocessForm(Form $form, $data, $group = 'content'): void
 790      {
 791          // Load the 'task' plugin group
 792          PluginHelper::importPlugin('task');
 793  
 794          // Let the parent method take over
 795          parent::preprocessForm($form, $data, $group);
 796      }
 797  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer