[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |