mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-25 16:22:43 +01:00
Increase the power of bin/config
Summary: Fixes T2254. Make the CLI for config more powerful: - Add validation for `set`. - Add `get`. - Add `list`. - Add `delete`. The `get` command produces fairly verbose JSON to support flags like `--all`, or `--source database` later. The other commands are straightforward. Test Plan: Tested `config set`: $ ./bin/config set Usage Exception: Specify a configuration key and a value to set it to. $ ./bin/config set x Usage Exception: Specify a value to set the key 'x' to. $ ./bin/config set phabricator.base-uri Usage Exception: Specify a value to set the key 'phabricator.base-uri' to. $ ./bin/config set phabricator.base-uri x Usage Exception: Config option 'phabricator.base-uri' is invalid. The URI must start with 'http://' or 'https://'. $ ./bin/config set phabricator.base-uri http://x Usage Exception: Config option 'phabricator.base-uri' is invalid. The URI must contain a dot ('.'), like 'http://example.com/', not just a bare name like 'http://example/'. Some web browsers will not set cookies on domains with no TLD. $ ./bin/config set phabricator.base-uri http://x.com Set 'phabricator.base-uri' in local configuration. $ Tested `config get`: $ ./bin/config get pygments.enabled { "config" : [] } $ ./bin/config set pygments.enabled true Set 'pygments.enabled' in local configuration. $ ./bin/config get pygments.enabled { "config" : [ { "key" : "pygments.enabled", "source" : "local", "value" : true } ] } $ Tested `config delete`: $ ./bin/config delete Usage Exception: Specify a configuration key to delete. $ ./bin/config delete x x Usage Exception: Too many arguments: expected one key. $ ./bin/config delete x Usage Exception: No such configuration key 'x'! Use `config list` to list all keys. $ ./bin/config delete pygments.enabled Deleted 'pygments.enabled' from local configuration. $ ./bin/config delete pygments.enabled Usage Exception: Configuration key 'pygments.enabled' is not set in local configuration! $ Tested `config list`: $ ./bin/config list account.editable account.minimum-password-length amazon-ec2.access-key amazon-ec2.secret-key amazon-s3.access-key amazon-s3.endpoint amazon-s3.secret-key amazon-ses.access-key amazon-ses.secret-key aphront.default-application-configuration-class audit.can-author-close-audit auth.email-domains auth.login-message auth.password-auth-enabled auth.require-email-verification auth.sessions.conduit auth.sessions.web auth.sshkeys.enabled cache.enable-deflate celerity.force-disk-reads celerity.minify celerity.resource-hash celerity.resource-path config.hide config.lock config.mask controller.oauth-registration darkconsole.always-on darkconsole.enabled debug.profile-rate debug.stop-on-redirect differential.allow-reopen differential.allow-self-accept differential.always-allow-close differential.anonymous-access differential.custom-remarkup-block-rules differential.custom-remarkup-rules differential.days-fresh differential.days-stale differential.enable-email-accept differential.expose-emails-prudently differential.field-selector differential.generated-paths differential.require-test-plan-field differential.revision-custom-detail-renderer differential.show-host-field differential.show-test-plan-field differential.whitespace-matters disqus.application-id disqus.application-secret disqus.auth-enabled disqus.auth-permanent disqus.registration-enabled disqus.shortname environment.append-paths events.listeners facebook.application-id facebook.application-secret facebook.auth-enabled facebook.auth-permanent facebook.registration-enabled facebook.require-https-auth feed.http-hooks feed.public files.image-mime-types files.viewable-mime-types gcdaemon.ttl.daemon-logs gcdaemon.ttl.differential-parse-cache gcdaemon.ttl.general-cache gcdaemon.ttl.herald-transcripts gcdaemon.ttl.markup-cache gcdaemon.ttl.task-archive github.application-id github.application-secret github.auth-enabled github.auth-permanent github.registration-enabled google.application-id google.application-secret google.auth-enabled google.auth-permanent google.registration-enabled ldap.activedirectory_domain ldap.anonymous-user-name ldap.anonymous-user-password ldap.auth-enabled ldap.base_dn ldap.hostname ldap.port ldap.real_name_attributes ldap.referrals ldap.search-first ldap.search_attribute ldap.start-tls ldap.username-attribute ldap.version load-libraries log.access.format log.access.path maniphest.custom-fields maniphest.custom-task-extensions-class maniphest.default-priority maniphest.enabled metamta.can-send-as-user metamta.default-address metamta.differential.attach-patches metamta.differential.inline-patches metamta.differential.patch-format metamta.differential.reply-handler metamta.differential.reply-handler-domain metamta.differential.subject-prefix metamta.differential.unified-comment-context metamta.diffusion.attach-patches metamta.diffusion.byte-limit metamta.diffusion.inline-patches metamta.diffusion.reply-handler metamta.diffusion.reply-handler-domain metamta.diffusion.subject-prefix metamta.diffusion.time-limit metamta.domain metamta.herald.show-hints metamta.insecure-auth-with-reply-to metamta.macro.reply-handler-domain metamta.macro.subject-prefix metamta.mail-adapter metamta.maniphest.default-public-author metamta.maniphest.public-create-email metamta.maniphest.reply-handler metamta.maniphest.reply-handler-domain metamta.maniphest.subject-prefix metamta.one-mail-per-recipient metamta.package.reply-handler metamta.package.subject-prefix metamta.pholio.reply-handler-domain metamta.pholio.subject-prefix metamta.placeholder-to-recipient metamta.precedence-bulk metamta.public-replies metamta.re-prefix metamta.recipients.show-hints metamta.reply.show-hints metamta.send-immediately metamta.single-reply-handler-prefix metamta.user-address-format metamta.vary-subjects mysql.configuration-provider mysql.host mysql.implementation mysql.pass mysql.user notification.client-uri notification.debug notification.enabled notification.log notification.pidfile notification.server-uri notification.user phabricator.application-id phabricator.application-secret phabricator.auth-enabled phabricator.auth-permanent phabricator.base-uri phabricator.csrf-key phabricator.env phabricator.mail-key phabricator.oauth-uri phabricator.production-uri phabricator.registration-enabled phabricator.serious-business phabricator.setup phabricator.show-beta-applications phabricator.show-error-callout phabricator.show-stack-traces phabricator.timezone phame.skins phd.log-directory phd.pid-directory phd.start-taskmasters phd.trace phd.verbose phid.external-loaders phpmailer.mailer phpmailer.smtp-host phpmailer.smtp-password phpmailer.smtp-port phpmailer.smtp-protocol phpmailer.smtp-user phriction.enabled policy.allow-public pygments.dropdown-choices pygments.enabled recaptcha.enabled recaptcha.private-key recaptcha.public-key remarkup.enable-embedded-youtube repository.default-local-path search.elastic.host search.engine-selector security.alternate-file-domain security.hmac-key security.require-https sendgrid.api-key sendgrid.api-user storage.default-namespace storage.engine-selector storage.local-disk.path storage.mysql-engine.max-size storage.s3.bucket storage.upload-size-limit style.monospace syntax-highlighter.engine syntax.filemap test.value tokenizer.ondemand translation.override translation.provider uri.allowed-protocols $ Reviewers: btrahan, codeblock Reviewed By: btrahan CC: aran Maniphest Tasks: T2254 Differential Revision: https://secure.phabricator.com/D4570
This commit is contained in:
parent
08e61c6ff1
commit
baa9d96082
9 changed files with 254 additions and 53 deletions
|
@ -15,7 +15,10 @@ EOSYNOPSIS
|
||||||
$args->parseStandardArguments();
|
$args->parseStandardArguments();
|
||||||
|
|
||||||
$workflows = array(
|
$workflows = array(
|
||||||
|
new PhabricatorConfigManagementListWorkflow(),
|
||||||
new PhabricatorConfigManagementSetWorkflow(),
|
new PhabricatorConfigManagementSetWorkflow(),
|
||||||
|
new PhabricatorConfigManagementGetWorkflow(),
|
||||||
|
new PhabricatorConfigManagementDeleteWorkflow(),
|
||||||
new PhutilHelpArgumentWorkflow(),
|
new PhutilHelpArgumentWorkflow(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -719,8 +719,11 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorConfigJSON' => 'applications/config/json/PhabricatorConfigJSON.php',
|
'PhabricatorConfigJSON' => 'applications/config/json/PhabricatorConfigJSON.php',
|
||||||
'PhabricatorConfigListController' => 'applications/config/controller/PhabricatorConfigListController.php',
|
'PhabricatorConfigListController' => 'applications/config/controller/PhabricatorConfigListController.php',
|
||||||
'PhabricatorConfigLocalSource' => 'infrastructure/env/PhabricatorConfigLocalSource.php',
|
'PhabricatorConfigLocalSource' => 'infrastructure/env/PhabricatorConfigLocalSource.php',
|
||||||
'PhabricatorConfigManagementSetWorkflow' => 'infrastructure/env/management/PhabricatorConfigManagementSetWorkflow.php',
|
'PhabricatorConfigManagementDeleteWorkflow' => 'applications/config/management/PhabricatorConfigManagementDeleteWorkflow.php',
|
||||||
'PhabricatorConfigManagementWorkflow' => 'infrastructure/env/management/PhabricatorConfigManagementWorkflow.php',
|
'PhabricatorConfigManagementGetWorkflow' => 'applications/config/management/PhabricatorConfigManagementGetWorkflow.php',
|
||||||
|
'PhabricatorConfigManagementListWorkflow' => 'applications/config/management/PhabricatorConfigManagementListWorkflow.php',
|
||||||
|
'PhabricatorConfigManagementSetWorkflow' => 'applications/config/management/PhabricatorConfigManagementSetWorkflow.php',
|
||||||
|
'PhabricatorConfigManagementWorkflow' => 'applications/config/management/PhabricatorConfigManagementWorkflow.php',
|
||||||
'PhabricatorConfigOption' => 'applications/config/option/PhabricatorConfigOption.php',
|
'PhabricatorConfigOption' => 'applications/config/option/PhabricatorConfigOption.php',
|
||||||
'PhabricatorConfigProxySource' => 'infrastructure/env/PhabricatorConfigProxySource.php',
|
'PhabricatorConfigProxySource' => 'infrastructure/env/PhabricatorConfigProxySource.php',
|
||||||
'PhabricatorConfigSource' => 'infrastructure/env/PhabricatorConfigSource.php',
|
'PhabricatorConfigSource' => 'infrastructure/env/PhabricatorConfigSource.php',
|
||||||
|
@ -2117,6 +2120,9 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorConfigIssueViewController' => 'PhabricatorConfigController',
|
'PhabricatorConfigIssueViewController' => 'PhabricatorConfigController',
|
||||||
'PhabricatorConfigListController' => 'PhabricatorConfigController',
|
'PhabricatorConfigListController' => 'PhabricatorConfigController',
|
||||||
'PhabricatorConfigLocalSource' => 'PhabricatorConfigProxySource',
|
'PhabricatorConfigLocalSource' => 'PhabricatorConfigProxySource',
|
||||||
|
'PhabricatorConfigManagementDeleteWorkflow' => 'PhabricatorConfigManagementWorkflow',
|
||||||
|
'PhabricatorConfigManagementGetWorkflow' => 'PhabricatorConfigManagementWorkflow',
|
||||||
|
'PhabricatorConfigManagementListWorkflow' => 'PhabricatorConfigManagementWorkflow',
|
||||||
'PhabricatorConfigManagementSetWorkflow' => 'PhabricatorConfigManagementWorkflow',
|
'PhabricatorConfigManagementSetWorkflow' => 'PhabricatorConfigManagementWorkflow',
|
||||||
'PhabricatorConfigManagementWorkflow' => 'PhutilArgumentWorkflow',
|
'PhabricatorConfigManagementWorkflow' => 'PhutilArgumentWorkflow',
|
||||||
'PhabricatorConfigOption' =>
|
'PhabricatorConfigOption' =>
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorConfigManagementDeleteWorkflow
|
||||||
|
extends PhabricatorConfigManagementWorkflow {
|
||||||
|
|
||||||
|
protected function didConstruct() {
|
||||||
|
$this
|
||||||
|
->setName('delete')
|
||||||
|
->setExamples('**delete** __key__')
|
||||||
|
->setSynopsis('Delete a local configuration value.')
|
||||||
|
->setArguments(
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'name' => 'args',
|
||||||
|
'wildcard' => true,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(PhutilArgumentParser $args) {
|
||||||
|
$console = PhutilConsole::getConsole();
|
||||||
|
|
||||||
|
$argv = $args->getArg('args');
|
||||||
|
if (count($argv) == 0) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
"Specify a configuration key to delete.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = $argv[0];
|
||||||
|
|
||||||
|
if (count($argv) > 1) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
"Too many arguments: expected one key.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$options = PhabricatorApplicationConfigOptions::loadAllOptions();
|
||||||
|
if (empty($options[$key])) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
"No such configuration key '{$key}'! Use `config list` to list all ".
|
||||||
|
"keys.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = new PhabricatorConfigLocalSource();
|
||||||
|
$values = $config->getKeys(array($key));
|
||||||
|
if (!$values) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
"Configuration key '{$key}' is not set in local configuration!");
|
||||||
|
}
|
||||||
|
|
||||||
|
$config->deleteKeys(array($key));
|
||||||
|
|
||||||
|
$console->writeOut(
|
||||||
|
pht("Deleted '%s' from local configuration.", $key)."\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorConfigManagementGetWorkflow
|
||||||
|
extends PhabricatorConfigManagementWorkflow {
|
||||||
|
|
||||||
|
protected function didConstruct() {
|
||||||
|
$this
|
||||||
|
->setName('get')
|
||||||
|
->setExamples('**get** __key__')
|
||||||
|
->setSynopsis('Get a local configuration value.')
|
||||||
|
->setArguments(
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'name' => 'args',
|
||||||
|
'wildcard' => true,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(PhutilArgumentParser $args) {
|
||||||
|
$console = PhutilConsole::getConsole();
|
||||||
|
|
||||||
|
$argv = $args->getArg('args');
|
||||||
|
if (count($argv) == 0) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
"Specify a configuration key to get.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = $argv[0];
|
||||||
|
|
||||||
|
if (count($argv) > 1) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
"Too many arguments: expected one key.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$options = PhabricatorApplicationConfigOptions::loadAllOptions();
|
||||||
|
if (empty($options[$key])) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
"No such configuration key '{$key}'! Use `config list` to list all ".
|
||||||
|
"keys.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = new PhabricatorConfigLocalSource();
|
||||||
|
$values = $config->getKeys(array($key));
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
foreach ($values as $key => $value) {
|
||||||
|
$result[] = array(
|
||||||
|
'key' => $key,
|
||||||
|
'source' => 'local',
|
||||||
|
'value' => $value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$result = array(
|
||||||
|
'config' => $result,
|
||||||
|
);
|
||||||
|
|
||||||
|
$json = new PhutilJSON();
|
||||||
|
$console->writeOut($json->encodeFormatted($result));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorConfigManagementListWorkflow
|
||||||
|
extends PhabricatorConfigManagementWorkflow {
|
||||||
|
|
||||||
|
protected function didConstruct() {
|
||||||
|
$this
|
||||||
|
->setName('list')
|
||||||
|
->setExamples('**list**')
|
||||||
|
->setSynopsis('List all configuration keys.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(PhutilArgumentParser $args) {
|
||||||
|
$options = PhabricatorApplicationConfigOptions::loadAllOptions();
|
||||||
|
ksort($options);
|
||||||
|
|
||||||
|
$console = PhutilConsole::getConsole();
|
||||||
|
foreach ($options as $option) {
|
||||||
|
$console->writeOut($option->getKey()."\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorConfigManagementSetWorkflow
|
||||||
|
extends PhabricatorConfigManagementWorkflow {
|
||||||
|
|
||||||
|
protected function didConstruct() {
|
||||||
|
$this
|
||||||
|
->setName('set')
|
||||||
|
->setExamples('**set** __key__ __value__')
|
||||||
|
->setSynopsis('Set a local configuration value.')
|
||||||
|
->setArguments(
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'name' => 'args',
|
||||||
|
'wildcard' => true,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(PhutilArgumentParser $args) {
|
||||||
|
$console = PhutilConsole::getConsole();
|
||||||
|
|
||||||
|
$argv = $args->getArg('args');
|
||||||
|
if (count($argv) == 0) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
"Specify a configuration key and a value to set it to.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = $argv[0];
|
||||||
|
|
||||||
|
if (count($argv) == 1) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
"Specify a value to set the key '{$key}' to.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $argv[1];
|
||||||
|
|
||||||
|
if (count($argv) > 2) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
"Too many arguments: expected one key and one value.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$options = PhabricatorApplicationConfigOptions::loadAllOptions();
|
||||||
|
if (empty($options[$key])) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
"No such configuration key '{$key}'! Use `config list` to list all ".
|
||||||
|
"keys.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$option = $options[$key];
|
||||||
|
|
||||||
|
$type = $option->getType();
|
||||||
|
switch ($type) {
|
||||||
|
case 'string':
|
||||||
|
case 'class':
|
||||||
|
$value = (string)$value;
|
||||||
|
break;
|
||||||
|
case 'int':
|
||||||
|
if (!ctype_digit($value)) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
"Config key '{$key}' is of type '{$type}'. Specify an integer.");
|
||||||
|
}
|
||||||
|
$value = (int)$value;
|
||||||
|
break;
|
||||||
|
case 'bool':
|
||||||
|
if ($value == 'true') {
|
||||||
|
$value = true;
|
||||||
|
} else if ($value == 'false') {
|
||||||
|
$value = false;
|
||||||
|
} else {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
"Config key '{$key}' is of type '{$type}'. ".
|
||||||
|
"Specify 'true' or 'false'.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$value = json_decode($value, true);
|
||||||
|
if (!is_array($value)) {
|
||||||
|
throw new PhutilArgumentUsageException(
|
||||||
|
"Config key '{$key}' is of type '{$type}'. Specify it in JSON.");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$option->getGroup()->validateOption($option, $value);
|
||||||
|
} catch (PhabricatorConfigValidationException $validation) {
|
||||||
|
// Convert this into a usage exception so we don't dump a stack trace.
|
||||||
|
throw new PhutilArgumentUsageException($validation->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = new PhabricatorConfigLocalSource();
|
||||||
|
$config->setKeys(array($key => $value));
|
||||||
|
|
||||||
|
$console->writeOut(
|
||||||
|
pht("Set '%s' in local configuration.", $key)."\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -38,7 +38,7 @@ abstract class PhabricatorConfigProxySource
|
||||||
}
|
}
|
||||||
|
|
||||||
public function deleteKeys(array $keys) {
|
public function deleteKeys(array $keys) {
|
||||||
$this->getSource()->deleteKeys();
|
$this->getSource()->deleteKeys($keys);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
final class PhabricatorConfigManagementSetWorkflow
|
|
||||||
extends PhabricatorConfigManagementWorkflow {
|
|
||||||
|
|
||||||
protected function didConstruct() {
|
|
||||||
$this
|
|
||||||
->setName('set')
|
|
||||||
->setExamples('**set** __key__ __value__')
|
|
||||||
->setSynopsis('Set a local configuration value.')
|
|
||||||
->setArguments(
|
|
||||||
array(
|
|
||||||
array(
|
|
||||||
'name' => 'args',
|
|
||||||
'wildcard' => true,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function execute(PhutilArgumentParser $args) {
|
|
||||||
$console = PhutilConsole::getConsole();
|
|
||||||
|
|
||||||
$argv = $args->getArg('args');
|
|
||||||
if (count($argv) == 0) {
|
|
||||||
throw new PhutilArgumentUsageException(
|
|
||||||
"Specify a configuration key and a value to set it to.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$key = $argv[0];
|
|
||||||
|
|
||||||
if (count($argv) == 1) {
|
|
||||||
throw new PhutilArgumentUsageException(
|
|
||||||
"Specify a value to set the key '{$key}' to.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$value = $argv[1];
|
|
||||||
|
|
||||||
if (count($argv) > 2) {
|
|
||||||
throw new PhutilArgumentUsageException(
|
|
||||||
"Too many arguments: expected one key and one value.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$config = new PhabricatorConfigLocalSource();
|
|
||||||
$config->setKeys(array($key => $value));
|
|
||||||
|
|
||||||
$console->writeOut(
|
|
||||||
pht("Set '%s' to '%s' in local configuration.", $key, $value)."\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
Loading…
Reference in a new issue