mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-17 20:32:41 +01:00
Allow Calendar imports to be configured with hourly or daily auto-updates
Summary: Ref T10747. For URI-based (and, in the future, Google-based) imports, we can automatically refresh them periodically. (In the general case there's no way to get a push notification for an ICS file, so we just have to do this every-so-often.) Test Plan: - Set an ICS file to update hourly. - Used `bin/trigger fire --id ...` to fire it artificially. - Saw Calendar update. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16752
This commit is contained in:
parent
a69ac888b3
commit
2d7f574b9d
13 changed files with 344 additions and 9 deletions
|
@ -0,0 +1,8 @@
|
|||
ALTER TABLE {$NAMESPACE}_calendar.calendar_import
|
||||
ADD triggerPHID VARBINARY(64);
|
||||
|
||||
ALTER TABLE {$NAMESPACE}_calendar.calendar_import
|
||||
ADD triggerFrequency VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT};
|
||||
|
||||
UPDATE {$NAMESPACE}_calendar.calendar_import
|
||||
SET triggerFrequency = 'once' WHERE triggerFrequency = '';
|
|
@ -2121,6 +2121,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorCalendarImportEpochLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportEpochLogType.php',
|
||||
'PhabricatorCalendarImportFetchLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportFetchLogType.php',
|
||||
'PhabricatorCalendarImportFrequencyLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportFrequencyLogType.php',
|
||||
'PhabricatorCalendarImportFrequencyTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportFrequencyTransaction.php',
|
||||
'PhabricatorCalendarImportICSFileTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php',
|
||||
'PhabricatorCalendarImportICSLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportICSLogType.php',
|
||||
'PhabricatorCalendarImportICSURITransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSURITransaction.php',
|
||||
|
@ -2139,10 +2140,12 @@ phutil_register_library_map(array(
|
|||
'PhabricatorCalendarImportQuery' => 'applications/calendar/query/PhabricatorCalendarImportQuery.php',
|
||||
'PhabricatorCalendarImportReloadController' => 'applications/calendar/controller/PhabricatorCalendarImportReloadController.php',
|
||||
'PhabricatorCalendarImportReloadTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportReloadTransaction.php',
|
||||
'PhabricatorCalendarImportReloadWorker' => 'applications/calendar/worker/PhabricatorCalendarImportReloadWorker.php',
|
||||
'PhabricatorCalendarImportSearchEngine' => 'applications/calendar/query/PhabricatorCalendarImportSearchEngine.php',
|
||||
'PhabricatorCalendarImportTransaction' => 'applications/calendar/storage/PhabricatorCalendarImportTransaction.php',
|
||||
'PhabricatorCalendarImportTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarImportTransactionQuery.php',
|
||||
'PhabricatorCalendarImportTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarImportTransactionType.php',
|
||||
'PhabricatorCalendarImportTriggerLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportTriggerLogType.php',
|
||||
'PhabricatorCalendarImportUpdateLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportUpdateLogType.php',
|
||||
'PhabricatorCalendarImportViewController' => 'applications/calendar/controller/PhabricatorCalendarImportViewController.php',
|
||||
'PhabricatorCalendarRemarkupRule' => 'applications/calendar/remarkup/PhabricatorCalendarRemarkupRule.php',
|
||||
|
@ -6964,6 +6967,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorCalendarImportEpochLogType' => 'PhabricatorCalendarImportLogType',
|
||||
'PhabricatorCalendarImportFetchLogType' => 'PhabricatorCalendarImportLogType',
|
||||
'PhabricatorCalendarImportFrequencyLogType' => 'PhabricatorCalendarImportLogType',
|
||||
'PhabricatorCalendarImportFrequencyTransaction' => 'PhabricatorCalendarImportTransactionType',
|
||||
'PhabricatorCalendarImportICSFileTransaction' => 'PhabricatorCalendarImportTransactionType',
|
||||
'PhabricatorCalendarImportICSLogType' => 'PhabricatorCalendarImportLogType',
|
||||
'PhabricatorCalendarImportICSURITransaction' => 'PhabricatorCalendarImportTransactionType',
|
||||
|
@ -6986,10 +6990,12 @@ phutil_register_library_map(array(
|
|||
'PhabricatorCalendarImportQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||
'PhabricatorCalendarImportReloadController' => 'PhabricatorCalendarController',
|
||||
'PhabricatorCalendarImportReloadTransaction' => 'PhabricatorCalendarImportTransactionType',
|
||||
'PhabricatorCalendarImportReloadWorker' => 'PhabricatorWorker',
|
||||
'PhabricatorCalendarImportSearchEngine' => 'PhabricatorApplicationSearchEngine',
|
||||
'PhabricatorCalendarImportTransaction' => 'PhabricatorModularTransaction',
|
||||
'PhabricatorCalendarImportTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
|
||||
'PhabricatorCalendarImportTransactionType' => 'PhabricatorModularTransactionType',
|
||||
'PhabricatorCalendarImportTriggerLogType' => 'PhabricatorCalendarImportLogType',
|
||||
'PhabricatorCalendarImportUpdateLogType' => 'PhabricatorCalendarImportLogType',
|
||||
'PhabricatorCalendarImportViewController' => 'PhabricatorCalendarController',
|
||||
'PhabricatorCalendarRemarkupRule' => 'PhabricatorObjectRemarkupRule',
|
||||
|
|
|
@ -33,7 +33,7 @@ final class PhabricatorCalendarImportDropController
|
|||
->addCancelButton($cancel_uri, pht('Done'));
|
||||
}
|
||||
|
||||
$engine = new PhabricatorCalendarICSImportEngine();
|
||||
$engine = new PhabricatorCalendarICSFileImportEngine();
|
||||
$imports = array();
|
||||
foreach ($files as $file) {
|
||||
$import = PhabricatorCalendarImport::initializeNewCalendarImport(
|
||||
|
|
|
@ -167,6 +167,50 @@ final class PhabricatorCalendarImportViewController
|
|||
pht('Source Type'),
|
||||
$engine->getImportEngineTypeName());
|
||||
|
||||
if ($import->getIsDisabled()) {
|
||||
$auto_updates = phutil_tag('em', array(), pht('Import Disabled'));
|
||||
$has_trigger = false;
|
||||
} else {
|
||||
$frequency = $import->getTriggerFrequency();
|
||||
$frequency_map = PhabricatorCalendarImport::getTriggerFrequencyMap();
|
||||
$frequency_names = ipull($frequency_map, 'name');
|
||||
$auto_updates = idx($frequency_names, $frequency, $frequency);
|
||||
|
||||
if ($frequency == PhabricatorCalendarImport::FREQUENCY_ONCE) {
|
||||
$has_trigger = false;
|
||||
$auto_updates = phutil_tag('em', array(), $auto_updates);
|
||||
} else {
|
||||
$has_trigger = true;
|
||||
}
|
||||
}
|
||||
|
||||
$properties->addProperty(
|
||||
pht('Automatic Updates'),
|
||||
$auto_updates);
|
||||
|
||||
if ($has_trigger) {
|
||||
$trigger = id(new PhabricatorWorkerTriggerQuery())
|
||||
->setViewer($viewer)
|
||||
->withPHIDs(array($import->getTriggerPHID()))
|
||||
->needEvents(true)
|
||||
->executeOne();
|
||||
|
||||
if (!$trigger) {
|
||||
$next_trigger = phutil_tag('em', array(), pht('Invalid Trigger'));
|
||||
} else {
|
||||
$now = PhabricatorTime::getNow();
|
||||
$next_epoch = $trigger->getNextEventPrediction();
|
||||
$next_trigger = pht(
|
||||
'%s (%s)',
|
||||
phabricator_datetime($next_epoch, $viewer),
|
||||
phutil_format_relative_time($next_epoch - $now));
|
||||
}
|
||||
|
||||
$properties->addProperty(
|
||||
pht('Next Update'),
|
||||
$next_trigger);
|
||||
}
|
||||
|
||||
$engine->appendImportProperties(
|
||||
$viewer,
|
||||
$import,
|
||||
|
|
|
@ -80,6 +80,9 @@ final class PhabricatorCalendarImportEditEngine
|
|||
protected function buildCustomEditFields($object) {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$engine = $object->getEngine();
|
||||
$can_trigger = $engine->supportsTriggers($object);
|
||||
|
||||
$fields = array(
|
||||
id(new PhabricatorTextEditField())
|
||||
->setKey('name')
|
||||
|
@ -89,6 +92,7 @@ final class PhabricatorCalendarImportEditEngine
|
|||
PhabricatorCalendarImportNameTransaction::TRANSACTIONTYPE)
|
||||
->setConduitDescription(pht('Rename the import.'))
|
||||
->setConduitTypeDescription(pht('New import name.'))
|
||||
->setPlaceholder($object->getDisplayName())
|
||||
->setValue($object->getName()),
|
||||
id(new PhabricatorBoolEditField())
|
||||
->setKey('disabled')
|
||||
|
@ -123,6 +127,22 @@ final class PhabricatorCalendarImportEditEngine
|
|||
->setValue(false),
|
||||
);
|
||||
|
||||
if ($can_trigger) {
|
||||
$frequency_map = PhabricatorCalendarImport::getTriggerFrequencyMap();
|
||||
$frequency_options = ipull($frequency_map, 'name');
|
||||
|
||||
$fields[] = id(new PhabricatorSelectEditField())
|
||||
->setKey('frequency')
|
||||
->setLabel(pht('Update Automatically'))
|
||||
->setDescription(pht('Configure an automatic update frequncy.'))
|
||||
->setTransactionType(
|
||||
PhabricatorCalendarImportFrequencyTransaction::TRANSACTIONTYPE)
|
||||
->setConduitDescription(pht('Set the automatic update frequency.'))
|
||||
->setConduitTypeDescription(pht('Update frequency constant.'))
|
||||
->setValue($object->getTriggerFrequency())
|
||||
->setOptions($frequency_options);
|
||||
}
|
||||
|
||||
$import_engine = $object->getEngine();
|
||||
foreach ($import_engine->newEditEngineFields($this, $object) as $field) {
|
||||
$fields[] = $field;
|
||||
|
|
|
@ -27,26 +27,112 @@ final class PhabricatorCalendarImportEditor
|
|||
protected function applyFinalEffects(
|
||||
PhabricatorLiskDAO $object,
|
||||
array $xactions) {
|
||||
|
||||
$type_reload = PhabricatorCalendarImportReloadTransaction::TRANSACTIONTYPE;
|
||||
$actor = $this->getActor();
|
||||
|
||||
// We import events when you create a source, or if you later reload it
|
||||
// explicitly.
|
||||
$should_reload = $this->getIsNewObject();
|
||||
|
||||
// We adjust the import trigger if you change the import frequency or
|
||||
// disable the import.
|
||||
$should_trigger = false;
|
||||
|
||||
foreach ($xactions as $xaction) {
|
||||
if ($xaction->getTransactionType() == $type_reload) {
|
||||
$should_reload = true;
|
||||
break;
|
||||
$xaction_type = $xaction->getTransactionType();
|
||||
switch ($xaction_type) {
|
||||
case PhabricatorCalendarImportReloadTransaction::TRANSACTIONTYPE:
|
||||
$should_reload = true;
|
||||
break;
|
||||
case PhabricatorCalendarImportFrequencyTransaction::TRANSACTIONTYPE:
|
||||
$should_trigger = true;
|
||||
break;
|
||||
case PhabricatorCalendarImportDisableTransaction::TRANSACTIONTYPE:
|
||||
$should_trigger = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($should_reload) {
|
||||
$actor = $this->getActor();
|
||||
|
||||
$import_engine = $object->getEngine();
|
||||
$import_engine->importEventsFromSource($actor, $object);
|
||||
}
|
||||
|
||||
if ($should_trigger) {
|
||||
$trigger_phid = $object->getTriggerPHID();
|
||||
if ($trigger_phid) {
|
||||
$trigger = id(new PhabricatorWorkerTriggerQuery())
|
||||
->setViewer($actor)
|
||||
->withPHIDs(array($trigger_phid))
|
||||
->executeOne();
|
||||
|
||||
if ($trigger) {
|
||||
$engine = new PhabricatorDestructionEngine();
|
||||
$engine->destroyObject($trigger);
|
||||
}
|
||||
}
|
||||
|
||||
$frequency = $object->getTriggerFrequency();
|
||||
$now = PhabricatorTime::getNow();
|
||||
switch ($frequency) {
|
||||
case PhabricatorCalendarImport::FREQUENCY_ONCE:
|
||||
$clock = null;
|
||||
break;
|
||||
case PhabricatorCalendarImport::FREQUENCY_HOURLY:
|
||||
$clock = new PhabricatorMetronomicTriggerClock(
|
||||
array(
|
||||
'period' => phutil_units('1 hour in seconds'),
|
||||
));
|
||||
break;
|
||||
case PhabricatorCalendarImport::FREQUENCY_DAILY:
|
||||
$clock = new PhabricatorDailyRoutineTriggerClock(
|
||||
array(
|
||||
'start' => $now,
|
||||
));
|
||||
break;
|
||||
default:
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Unknown import trigger frequency "%s".',
|
||||
$frequency));
|
||||
}
|
||||
|
||||
// If the object has been disabled, don't write a new trigger.
|
||||
if ($object->getIsDisabled()) {
|
||||
$clock = null;
|
||||
}
|
||||
|
||||
if ($clock) {
|
||||
$trigger_action = new PhabricatorScheduleTaskTriggerAction(
|
||||
array(
|
||||
'class' => 'PhabricatorCalendarImportReloadWorker',
|
||||
'data' => array(
|
||||
'importPHID' => $object->getPHID(),
|
||||
),
|
||||
'options' => array(
|
||||
'objectPHID' => $object->getPHID(),
|
||||
'priority' => PhabricatorWorker::PRIORITY_BULK,
|
||||
),
|
||||
));
|
||||
|
||||
$trigger_phid = PhabricatorPHID::generateNewPHID(
|
||||
PhabricatorWorkerTriggerPHIDType::TYPECONST);
|
||||
|
||||
$object
|
||||
->setTriggerPHID($trigger_phid)
|
||||
->save();
|
||||
|
||||
$trigger = id(new PhabricatorWorkerTrigger())
|
||||
->setClock($clock)
|
||||
->setAction($trigger_action)
|
||||
->setPHID($trigger_phid)
|
||||
->save();
|
||||
} else {
|
||||
$object
|
||||
->setTriggerPHID(null)
|
||||
->save();
|
||||
}
|
||||
}
|
||||
|
||||
return $xactions;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,10 @@ final class PhabricatorCalendarICSFileImportEngine
|
|||
return pht('Import an event in ".ics" (iCalendar) format.');
|
||||
}
|
||||
|
||||
public function supportsTriggers(PhabricatorCalendarImport $import) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function appendImportProperties(
|
||||
PhabricatorUser $viewer,
|
||||
PhabricatorCalendarImport $import,
|
||||
|
|
|
@ -17,6 +17,10 @@ final class PhabricatorCalendarICSURIImportEngine
|
|||
return pht('Import or subscribe to a calendar in .ics format by URI.');
|
||||
}
|
||||
|
||||
public function supportsTriggers(PhabricatorCalendarImport $import) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function appendImportProperties(
|
||||
PhabricatorUser $viewer,
|
||||
PhabricatorCalendarImport $import,
|
||||
|
|
|
@ -39,6 +39,9 @@ abstract class PhabricatorCalendarImportEngine
|
|||
throw new PhutilMethodNotImplementedException();
|
||||
}
|
||||
|
||||
abstract public function supportsTriggers(
|
||||
PhabricatorCalendarImport $import);
|
||||
|
||||
final public static function getAllImportEngines() {
|
||||
return id(new PhutilClassMapQuery())
|
||||
->setAncestorClass(__CLASS__)
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorCalendarImportTriggerLogType
|
||||
extends PhabricatorCalendarImportLogType {
|
||||
|
||||
const LOGTYPE = 'trigger';
|
||||
|
||||
public function getDisplayType(
|
||||
PhabricatorUser $viewer,
|
||||
PhabricatorCalendarImportLog $log) {
|
||||
return pht('Import Triggered');
|
||||
}
|
||||
|
||||
public function getDisplayDescription(
|
||||
PhabricatorUser $viewer,
|
||||
PhabricatorCalendarImportLog $log) {
|
||||
return pht('Triggered a periodic update.');
|
||||
}
|
||||
|
||||
public function getDisplayIcon(
|
||||
PhabricatorUser $viewer,
|
||||
PhabricatorCalendarImportLog $log) {
|
||||
return 'fa-clock-o';
|
||||
}
|
||||
|
||||
public function getDisplayColor(
|
||||
PhabricatorUser $viewer,
|
||||
PhabricatorCalendarImportLog $log) {
|
||||
return 'blue';
|
||||
}
|
||||
|
||||
}
|
|
@ -14,6 +14,12 @@ final class PhabricatorCalendarImport
|
|||
protected $engineType;
|
||||
protected $parameters = array();
|
||||
protected $isDisabled = 0;
|
||||
protected $triggerPHID;
|
||||
protected $triggerFrequency;
|
||||
|
||||
const FREQUENCY_ONCE = 'once';
|
||||
const FREQUENCY_HOURLY = 'hourly';
|
||||
const FREQUENCY_DAILY = 'daily';
|
||||
|
||||
private $engine = self::ATTACHABLE;
|
||||
|
||||
|
@ -27,7 +33,8 @@ final class PhabricatorCalendarImport
|
|||
->setEditPolicy($actor->getPHID())
|
||||
->setIsDisabled(0)
|
||||
->setEngineType($engine->getImportEngineType())
|
||||
->attachEngine($engine);
|
||||
->attachEngine($engine)
|
||||
->setTriggerFrequency(self::FREQUENCY_ONCE);
|
||||
}
|
||||
|
||||
protected function getConfiguration() {
|
||||
|
@ -40,6 +47,8 @@ final class PhabricatorCalendarImport
|
|||
'name' => 'text',
|
||||
'engineType' => 'text64',
|
||||
'isDisabled' => 'bool',
|
||||
'triggerPHID' => 'phid?',
|
||||
'triggerFrequency' => 'text64',
|
||||
),
|
||||
self::CONFIG_KEY_SCHEMA => array(
|
||||
'key_author' => array(
|
||||
|
@ -85,6 +94,21 @@ final class PhabricatorCalendarImport
|
|||
return $this->getEngine()->getDisplayName($this);
|
||||
}
|
||||
|
||||
public static function getTriggerFrequencyMap() {
|
||||
return array(
|
||||
self::FREQUENCY_ONCE => array(
|
||||
'name' => pht('No Automatic Updates'),
|
||||
),
|
||||
self::FREQUENCY_HOURLY => array(
|
||||
'name' => pht('Update Hourly'),
|
||||
),
|
||||
self::FREQUENCY_DAILY => array(
|
||||
'name' => pht('Update Daily'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/* -( PhabricatorPolicyInterface )----------------------------------------- */
|
||||
|
||||
|
||||
|
@ -155,6 +179,17 @@ final class PhabricatorCalendarImport
|
|||
|
||||
$this->openTransaction();
|
||||
|
||||
$trigger_phid = $this->getTriggerPHID();
|
||||
if ($trigger_phid) {
|
||||
$trigger = id(new PhabricatorWorkerTriggerQuery())
|
||||
->setViewer($viewer)
|
||||
->withPHIDs(array($trigger_phid))
|
||||
->executeOne();
|
||||
if ($trigger) {
|
||||
$engine->destroyObject($trigger);
|
||||
}
|
||||
}
|
||||
|
||||
$events = id(new PhabricatorCalendarEventQuery())
|
||||
->setViewer($viewer)
|
||||
->withImportSourcePHIDs(array($this->getPHID()))
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorCalendarImportReloadWorker extends PhabricatorWorker {
|
||||
|
||||
protected function doWork() {
|
||||
$import = $this->loadImport();
|
||||
$viewer = PhabricatorUser::getOmnipotentUser();
|
||||
|
||||
if ($import->getIsDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$author = id(new PhabricatorPeopleQuery())
|
||||
->setViewer($viewer)
|
||||
->withPHIDs(array($import->getAuthorPHID()))
|
||||
->needUserSettings(true)
|
||||
->executeOne();
|
||||
|
||||
$import_engine = $import->getEngine();
|
||||
|
||||
$import->newLogMessage(
|
||||
PhabricatorCalendarImportTriggerLogType::LOGTYPE,
|
||||
array());
|
||||
|
||||
$import_engine->importEventsFromSource($author, $import);
|
||||
}
|
||||
|
||||
private function loadImport() {
|
||||
$viewer = PhabricatorUser::getOmnipotentUser();
|
||||
|
||||
$data = $this->getTaskData();
|
||||
$import_phid = idx($data, 'importPHID');
|
||||
|
||||
$import = id(new PhabricatorCalendarImportQuery())
|
||||
->setViewer($viewer)
|
||||
->withPHIDs(array($import_phid))
|
||||
->executeOne();
|
||||
if (!$import) {
|
||||
throw new PhabricatorWorkerPermanentFailureException(
|
||||
pht(
|
||||
'Failed to load import with PHID "%s".',
|
||||
$import_phid));
|
||||
}
|
||||
|
||||
return $import;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorCalendarImportFrequencyTransaction
|
||||
extends PhabricatorCalendarImportTransactionType {
|
||||
|
||||
const TRANSACTIONTYPE = 'calendar.import.frequency';
|
||||
|
||||
public function generateOldValue($object) {
|
||||
return $object->getTriggerFrequency();
|
||||
}
|
||||
|
||||
public function applyInternalEffects($object, $value) {
|
||||
$object->setTriggerFrequency($value);
|
||||
}
|
||||
|
||||
public function getTitle() {
|
||||
return pht(
|
||||
'%s changed the automatic update frequency for this import.',
|
||||
$this->renderAuthor());
|
||||
}
|
||||
|
||||
public function validateTransactions($object, array $xactions) {
|
||||
$errors = array();
|
||||
|
||||
$frequency_map = PhabricatorCalendarImport::getTriggerFrequencyMap();
|
||||
$valid = array_keys($frequency_map);
|
||||
$valid = array_fuse($valid);
|
||||
|
||||
foreach ($xactions as $xaction) {
|
||||
$value = $xaction->getNewValue();
|
||||
|
||||
if (!isset($valid[$value])) {
|
||||
$errors[] = $this->newInvalidError(
|
||||
pht(
|
||||
'Import frequency "%s" is not valid. Valid frequences are: %s.',
|
||||
$value,
|
||||
implode(', ', $valid)),
|
||||
$xaction);
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue