Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d31cec6ad | ||
|
|
8cbb3d02a8 | ||
|
|
d7b71dab24 | ||
|
|
944adf0c06 | ||
|
|
a1d65c1483 | ||
|
|
fb0509ae7e | ||
|
|
866b172c5f | ||
|
|
8d085347b9 | ||
|
|
fc3b7e948f | ||
|
|
2b2daee8bb | ||
|
|
e4b9203537 | ||
|
|
c94d9b4795 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,3 @@
|
||||
/build/
|
||||
/data/
|
||||
/vendor/
|
||||
/composer.lock
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
`kelunik/acme-client` is an ACME client written in PHP. ACME is the protocol that powers the [Let's Encrypt](https://letsencrypt.org) certificate authority.
|
||||
|
||||
## Requirements
|
||||
|
||||
* PHP 5.5+ with OpenSSL
|
||||
* Works on Unix and Windows
|
||||
|
||||
## Documentation
|
||||
|
||||
* [Installation](./doc/installation.md)
|
||||
|
||||
100
bin/acme
100
bin/acme
@@ -2,17 +2,22 @@
|
||||
<?php
|
||||
|
||||
use Auryn\Injector;
|
||||
use Kelunik\AcmeClient\AcmeFactory;
|
||||
use League\CLImate\CLImate;
|
||||
|
||||
$logo = <<<LOGO
|
||||
____ __________ ___ ___
|
||||
/ __ `/ ___/ __ `__ \/ _ \
|
||||
/ /_/ / /__/ / / / / / __/
|
||||
\__,_/\___/_/ /_/ /_/\___/
|
||||
|
||||
LOGO;
|
||||
|
||||
if (!file_exists(__DIR__ . "/../vendor/autoload.php")) {
|
||||
echo $logo;
|
||||
echo <<<HELP
|
||||
|
||||
____ __________ ___ ___
|
||||
/ __ `/ ___/ __ `__ \/ _ \
|
||||
/ /_/ / /__/ / / / / / __/
|
||||
\__,_/\___/_/ /_/ /_/\___/
|
||||
|
||||
You need to install the composer dependencies.
|
||||
You need to install the composer dependencies.
|
||||
|
||||
composer install --no-dev
|
||||
|
||||
@@ -22,14 +27,10 @@ HELP;
|
||||
}
|
||||
|
||||
if (!function_exists("openssl_pkey_get_private")) {
|
||||
echo $logo;
|
||||
echo <<<HELP
|
||||
|
||||
____ __________ ___ ___
|
||||
/ __ `/ ___/ __ `__ \/ _ \
|
||||
/ /_/ / /__/ / / / / / __/
|
||||
\__,_/\___/_/ /_/ /_/\___/
|
||||
|
||||
You need to enable OpenSSL in your php.ini
|
||||
You need to enable OpenSSL in your php.ini
|
||||
|
||||
|
||||
HELP;
|
||||
@@ -39,55 +40,57 @@ HELP;
|
||||
require __DIR__ . "/../vendor/autoload.php";
|
||||
|
||||
$commands = [
|
||||
"setup",
|
||||
"issue",
|
||||
"check",
|
||||
"revoke",
|
||||
"setup" => "Setup and register account.",
|
||||
"issue" => "Issue a new certificate.",
|
||||
"check" => "Check if a certificate is still valid long enough.",
|
||||
"revoke" => "Revoke a certificate.",
|
||||
"status" => "Show status about local certificates.",
|
||||
"version" => "Print version information.",
|
||||
"help" => "Print this help information.",
|
||||
];
|
||||
|
||||
$binary = \Kelunik\AcmeClient\getBinary();
|
||||
|
||||
$help = implode("\n ", array_map(function ($command) use ($binary) {
|
||||
return "{$binary} {$command}";
|
||||
}, $commands));
|
||||
$help = implode(PHP_EOL, array_map(function ($command) use ($commands) {
|
||||
$help = " <green>{$command}</green>\n";
|
||||
$help .= " └─ {$commands[$command]}\n";
|
||||
return $help;
|
||||
}, array_keys($commands)));
|
||||
|
||||
$help = <<<EOT
|
||||
|
||||
____ __________ ___ ___
|
||||
/ __ `/ ___/ __ `__ \/ _ \
|
||||
/ /_/ / /__/ / / / / / __/
|
||||
\__,_/\___/_/ /_/ /_/\___/
|
||||
<yellow>Usage:</yellow>
|
||||
bin/acme [command] [--args]
|
||||
|
||||
Usage: bin/acme command --args
|
||||
|
||||
Available Commands:
|
||||
{$help}
|
||||
|
||||
Get more help by appending --help to specific commands.
|
||||
<yellow>Options:</yellow>
|
||||
<green>-h, --help</green>
|
||||
└─ Print this help information.
|
||||
|
||||
<yellow>Available commands:</yellow>
|
||||
{$help}
|
||||
Get more help by appending <yellow>--help</yellow> to specific commands.
|
||||
|
||||
EOT;
|
||||
|
||||
$climate = new CLImate;
|
||||
$injector = new Injector;
|
||||
|
||||
if (!in_array(PHP_SAPI, ["cli", "phpdbg"], true)) {
|
||||
$climate->error("Please run this script via CLI!");
|
||||
$climate->error("Please run this script on the command line!");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (count($argv) === 1 || in_array($argv[1], ["h", "-h", "help", "--help"], true)) {
|
||||
$climate->out($help);
|
||||
if (count($argv) === 1 || in_array($argv[1], ["-h", "help", "--help"], true)) {
|
||||
$climate->out($logo . $help);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (!in_array($argv[1], $commands)) {
|
||||
$climate->br()->error(" Unknown command '{$argv[1]}'. Use --help for a list of available commands.");
|
||||
if (!in_array($argv[1], array_keys($commands))) {
|
||||
$climate->error("Unknown command '{$argv[1]}'. Use --help for a list of available commands.");
|
||||
|
||||
$suggestion = \Kelunik\AcmeClient\suggestCommand($argv[1], $commands);
|
||||
$suggestion = \Kelunik\AcmeClient\suggestCommand($argv[1], array_keys($commands));
|
||||
|
||||
if ($suggestion) {
|
||||
$climate->br()->out(" Did you mean '$suggestion'?");
|
||||
$climate->br()->out(" Did you mean '$suggestion'?");
|
||||
}
|
||||
|
||||
$climate->br();
|
||||
@@ -104,25 +107,28 @@ try {
|
||||
unset($args[1]);
|
||||
|
||||
$climate->arguments->add($definition);
|
||||
$climate->arguments->parse(array_values($args));
|
||||
} catch (Exception $e) {
|
||||
if (count($argv) === 3 && in_array($argv[2], ["h", "-h", "--help", "help"], true)) {
|
||||
|
||||
if (count($argv) === 3 && in_array($argv[2], ["-h", "--help"], true)) {
|
||||
$climate->usage(["{$binary} {$argv[1]}"]);
|
||||
$climate->br();
|
||||
|
||||
exit(0);
|
||||
} else {
|
||||
$climate->usage(["{$binary} {$argv[1]}"]);
|
||||
$climate->br();
|
||||
|
||||
$climate->error($e->getMessage());
|
||||
$climate->br();
|
||||
|
||||
exit(1);
|
||||
$climate->arguments->parse(array_values($args));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$climate->usage(["{$binary} {$argv[1]}"]);
|
||||
$climate->br();
|
||||
|
||||
$climate->error($e->getMessage());
|
||||
$climate->br();
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$injector = new Injector;
|
||||
$injector->share($climate);
|
||||
$injector->share(new AcmeFactory);
|
||||
|
||||
$command = $injector->make($class);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kelunik/acme-client",
|
||||
"description": "Standalone PHP ACME client.",
|
||||
"description": "Let's Encrypt / ACME client written in PHP for the CLI.",
|
||||
"keywords": [
|
||||
"ACME",
|
||||
"letsencrypt",
|
||||
@@ -24,7 +24,7 @@
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^5",
|
||||
"fabpot/php-cs-fixer": "^1.9",
|
||||
"macfja/phar-builder": "dev-master#a2db582eab26ef7b15144c013408749a79fae361"
|
||||
"macfja/phar-builder": "dev-events-dev-files"
|
||||
},
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
@@ -43,13 +43,30 @@
|
||||
"src/functions.php"
|
||||
]
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/kelunik/pharbuilder"
|
||||
}
|
||||
],
|
||||
"extra": {
|
||||
"phar-builder": {
|
||||
"compression": "GZip",
|
||||
"name": "acme-client.phar",
|
||||
"output-dir": "build",
|
||||
"include": ["src", "vendor/kelunik/acme/res", "vendor/amphp/socket/var"],
|
||||
"entry-point": "bin/acme"
|
||||
"include": ["info", "src", "vendor/kelunik/acme/res", "vendor/amphp/socket/var"],
|
||||
"entry-point": "bin/acme",
|
||||
"events": {
|
||||
"command.package.start": [
|
||||
"mkdir -p info",
|
||||
"git describe --tags > info/build.version",
|
||||
"php -r 'echo time();' > info/build.time"
|
||||
],
|
||||
"command.package.end": [
|
||||
"rm -rf info",
|
||||
"chmod +x build/acme-client.phar"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2941
composer.lock
generated
Normal file
2941
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
src/AcmeFactory.php
Normal file
16
src/AcmeFactory.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Kelunik\AcmeClient;
|
||||
|
||||
use Kelunik\Acme\AcmeClient;
|
||||
use Kelunik\Acme\AcmeService;
|
||||
use Kelunik\Acme\KeyPair;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
class AcmeFactory {
|
||||
public function build($directory, KeyPair $keyPair) {
|
||||
Assert::string($directory);
|
||||
|
||||
return new AcmeService(new AcmeClient($directory, $keyPair));
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,11 @@ namespace Kelunik\AcmeClient\Commands;
|
||||
use Amp\CoroutineResult;
|
||||
use Amp\Dns\Record;
|
||||
use Exception;
|
||||
use Kelunik\Acme\AcmeClient;
|
||||
use Kelunik\Acme\AcmeException;
|
||||
use Kelunik\Acme\AcmeService;
|
||||
use Kelunik\Acme\KeyPair;
|
||||
use Kelunik\Acme\OpenSSLKeyGenerator;
|
||||
use Kelunik\AcmeClient\AcmeFactory;
|
||||
use Kelunik\AcmeClient\Stores\CertificateStore;
|
||||
use Kelunik\AcmeClient\Stores\ChallengeStore;
|
||||
use Kelunik\AcmeClient\Stores\KeyStore;
|
||||
@@ -21,9 +21,11 @@ use Throwable;
|
||||
|
||||
class Issue implements Command {
|
||||
private $climate;
|
||||
private $acmeFactory;
|
||||
|
||||
public function __construct(CLImate $climate) {
|
||||
public function __construct(CLImate $climate, AcmeFactory $acmeFactory) {
|
||||
$this->climate = $climate;
|
||||
$this->acmeFactory = $acmeFactory;
|
||||
}
|
||||
|
||||
public function execute(Manager $args) {
|
||||
@@ -77,7 +79,7 @@ class Issue implements Command {
|
||||
|
||||
$this->climate->br();
|
||||
|
||||
$acme = new AcmeService(new AcmeClient($server, $keyPair));
|
||||
$acme = $this->acmeFactory->build($server, $keyPair);
|
||||
$promises = [];
|
||||
|
||||
foreach ($domains as $i => $domain) {
|
||||
@@ -176,7 +178,7 @@ class Issue implements Command {
|
||||
list($errors) = (yield \Amp\any($promises));
|
||||
|
||||
if (!empty($errors)) {
|
||||
throw new AcmeException("Couldn't resolve the following domains to an IPv4 record: " . implode(array_keys($errors)));
|
||||
throw new AcmeException("Couldn't resolve the following domains to an IPv4 record: " . implode(", ", array_keys($errors)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@ namespace Kelunik\AcmeClient\Commands;
|
||||
|
||||
use Amp\CoroutineResult;
|
||||
use Amp\File\FilesystemException;
|
||||
use Kelunik\Acme\AcmeClient;
|
||||
use Kelunik\Acme\AcmeService;
|
||||
use Kelunik\AcmeClient\AcmeFactory;
|
||||
use Kelunik\AcmeClient\Stores\CertificateStore;
|
||||
use Kelunik\AcmeClient\Stores\KeyStore;
|
||||
use Kelunik\Certificate\Certificate;
|
||||
@@ -14,9 +13,11 @@ use League\CLImate\CLImate;
|
||||
|
||||
class Revoke implements Command {
|
||||
private $climate;
|
||||
private $acmeFactory;
|
||||
|
||||
public function __construct(CLImate $climate) {
|
||||
public function __construct(CLImate $climate, AcmeFactory $acmeFactory) {
|
||||
$this->climate = $climate;
|
||||
$this->acmeFactory = $acmeFactory;
|
||||
}
|
||||
|
||||
public function execute(Manager $args) {
|
||||
@@ -30,7 +31,7 @@ class Revoke implements Command {
|
||||
$keyFile = \Kelunik\AcmeClient\serverToKeyname($server);
|
||||
|
||||
$keyPair = (yield $keyStore->get("accounts/{$keyFile}.pem"));
|
||||
$acme = new AcmeService(new AcmeClient($server, $keyPair), $keyPair);
|
||||
$acme = $this->acmeFactory->build($server, $keyPair);
|
||||
|
||||
$this->climate->br();
|
||||
$this->climate->whisper(" Revoking certificate ...");
|
||||
@@ -57,7 +58,7 @@ class Revoke implements Command {
|
||||
$this->climate->br();
|
||||
$this->climate->info(" Certificate has been revoked.");
|
||||
|
||||
yield (new CertificateStore(\Kelunik\AcmeClient\normalizePath($args->get("storage")). "/certs/" . $keyFile))->delete($args->get("name"));
|
||||
yield (new CertificateStore(\Kelunik\AcmeClient\normalizePath($args->get("storage")) . "/certs/" . $keyFile))->delete($args->get("name"));
|
||||
|
||||
yield new CoroutineResult(0);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,10 @@ use Amp\CoroutineResult;
|
||||
use Amp\Dns\Record;
|
||||
use Amp\Dns\ResolutionException;
|
||||
use InvalidArgumentException;
|
||||
use Kelunik\Acme\AcmeClient;
|
||||
use Kelunik\Acme\AcmeException;
|
||||
use Kelunik\Acme\AcmeService;
|
||||
use Kelunik\Acme\OpenSSLKeyGenerator;
|
||||
use Kelunik\Acme\Registration;
|
||||
use Kelunik\AcmeClient\AcmeFactory;
|
||||
use Kelunik\AcmeClient\Stores\KeyStore;
|
||||
use Kelunik\AcmeClient\Stores\KeyStoreException;
|
||||
use League\CLImate\Argument\Manager;
|
||||
@@ -18,9 +17,11 @@ use League\CLImate\CLImate;
|
||||
|
||||
class Setup implements Command {
|
||||
private $climate;
|
||||
private $acmeFactory;
|
||||
|
||||
public function __construct(CLImate $climate) {
|
||||
public function __construct(CLImate $climate, AcmeFactory $acmeFactory) {
|
||||
$this->climate = $climate;
|
||||
$this->acmeFactory = $acmeFactory;
|
||||
}
|
||||
|
||||
public function execute(Manager $args) {
|
||||
@@ -53,7 +54,7 @@ class Setup implements Command {
|
||||
$this->climate->whisper(" Generated new private key with {$bits} bits.");
|
||||
}
|
||||
|
||||
$acme = new AcmeService(new AcmeClient($server, $keyPair), $keyPair);
|
||||
$acme = $this->acmeFactory->build($server, $keyPair);
|
||||
|
||||
$this->climate->whisper(" Registering with " . substr($server, 8) . " ...");
|
||||
|
||||
|
||||
80
src/Commands/Status.php
Normal file
80
src/Commands/Status.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Kelunik\AcmeClient\Commands;
|
||||
|
||||
use Kelunik\AcmeClient\Stores\CertificateStore;
|
||||
use Kelunik\AcmeClient\Stores\KeyStore;
|
||||
use Kelunik\AcmeClient\Stores\KeyStoreException;
|
||||
use Kelunik\Certificate\Certificate;
|
||||
use League\CLImate\Argument\Manager;
|
||||
use League\CLImate\CLImate;
|
||||
|
||||
class Status {
|
||||
private $climate;
|
||||
|
||||
public function __construct(CLImate $climate) {
|
||||
$this->climate = $climate;
|
||||
}
|
||||
|
||||
public function execute(Manager $args) {
|
||||
return \Amp\resolve($this->doExecute($args));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Manager $args
|
||||
* @return \Generator
|
||||
*/
|
||||
private function doExecute(Manager $args) {
|
||||
$server = \Kelunik\AcmeClient\resolveServer($args->get("server"));
|
||||
$keyName = \Kelunik\AcmeClient\serverToKeyname($server);
|
||||
|
||||
$storage = \Kelunik\AcmeClient\normalizePath($args->get("storage"));
|
||||
|
||||
try {
|
||||
$keyStore = new KeyStore($storage);
|
||||
yield $keyStore->get("accounts/{$keyName}.pem");
|
||||
|
||||
$setup = true;
|
||||
} catch (KeyStoreException $e) {
|
||||
$setup = false;
|
||||
}
|
||||
|
||||
$this->climate->br();
|
||||
$this->climate->out(" [" . ($setup ? "<green> ✓ </green>" : "<red> ✗ </red>") . "] " . ($setup ? "Registered on " : "Not yet registered on ") . $server);
|
||||
$this->climate->br();
|
||||
|
||||
if (yield \Amp\File\exists($storage . "/certs/{$keyName}")) {
|
||||
$certificateStore = new CertificateStore($storage . "/certs/{$keyName}");
|
||||
|
||||
$domains = (yield \Amp\File\scandir($storage . "/certs/{$keyName}"));
|
||||
|
||||
foreach ($domains as $domain) {
|
||||
$pem = (yield $certificateStore->get($domain));
|
||||
$cert = new Certificate($pem);
|
||||
|
||||
$symbol = time() > $cert->getValidTo() ? "<red> ✗ </red>" : "<green> ✓ </green>";
|
||||
|
||||
if (time() < $cert->getValidTo() && time() + $args->get("ttl") * 24 * 60 * 60 > $cert->getValidTo()) {
|
||||
$symbol = "<yellow> ⭮ </yellow>";
|
||||
}
|
||||
|
||||
$this->climate->out(" [" . $symbol . "] " . implode(", ", $cert->getNames()));
|
||||
}
|
||||
|
||||
$this->climate->br();
|
||||
}
|
||||
}
|
||||
|
||||
public static function getDefinition() {
|
||||
return [
|
||||
"server" => \Kelunik\AcmeClient\getArgumentDescription("server"),
|
||||
"storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"),
|
||||
"ttl" => [
|
||||
"longPrefix" => "ttl",
|
||||
"description" => "Minimum valid time in days, shows ⭮ if renewal is required.",
|
||||
"defaultValue" => 30,
|
||||
"castTo" => "int",
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
76
src/Commands/Version.php
Normal file
76
src/Commands/Version.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Kelunik\AcmeClient\Commands;
|
||||
|
||||
use League\CLImate\Argument\Manager;
|
||||
use League\CLImate\CLImate;
|
||||
use RuntimeException;
|
||||
|
||||
class Version implements Command {
|
||||
private $climate;
|
||||
|
||||
public function __construct(CLImate $climate) {
|
||||
$this->climate = $climate;
|
||||
}
|
||||
|
||||
public function execute(Manager $args) {
|
||||
$version = $this->getVersion();
|
||||
|
||||
$buildTime = $this->readFileOr("info/build.time", time());
|
||||
$buildDate = date('M jS Y H:i:s T', (int) trim($buildTime));
|
||||
|
||||
$package = json_decode($this->readFileOr("composer.json", new RuntimeException("No composer.json found.")));
|
||||
|
||||
$this->climate->out("┌ <green>kelunik/acme-client</green> @ <yellow>{$version}</yellow> (built: {$buildDate})");
|
||||
$this->climate->out(($args->defined("deps") ? "│" : "└") . " " . $this->getDescription($package));
|
||||
|
||||
if ($args->defined("deps")) {
|
||||
$lockFile = json_decode($this->readFileOr("composer.lock", new RuntimeException("No composer.lock found.")));
|
||||
$packages = $lockFile->packages;
|
||||
|
||||
for ($i = 0; $i < count($packages); $i++) {
|
||||
$link = $i === count($packages) - 1 ? "└──" : "├──";
|
||||
$this->climate->out("{$link} <green>{$packages[$i]->name}</green> @ <yellow>{$packages[$i]->version}</yellow>");
|
||||
|
||||
$link = $i === count($packages) - 1 ? " " : "│ ";
|
||||
$this->climate->out("{$link} " . $this->getDescription($packages[$i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getDescription($package) {
|
||||
return \Kelunik\AcmeClient\ellipsis(isset($package->description) ? $package->description : "");
|
||||
}
|
||||
|
||||
private function getVersion() {
|
||||
if (file_exists(__DIR__ . "/../../.git")) {
|
||||
$version = `git describe --tags`;
|
||||
} else {
|
||||
$version = $this->readFileOr("info/build.version", "-unknown");
|
||||
}
|
||||
|
||||
return substr(trim($version), 1);
|
||||
}
|
||||
|
||||
private function readFileOr($file, $default = "") {
|
||||
if (file_exists(__DIR__ . "/../../" . $file)) {
|
||||
return file_get_contents(__DIR__ . "/../../" . $file);
|
||||
} else {
|
||||
if ($default instanceof \Exception || $default instanceof \Throwable) {
|
||||
throw $default;
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
public static function getDefinition() {
|
||||
return [
|
||||
"deps" => [
|
||||
"longPrefix" => "deps",
|
||||
"description" => "Show also the bundled dependency versions.",
|
||||
"noValue" => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,21 @@
|
||||
|
||||
namespace Kelunik\AcmeClient;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Kelunik\Acme\AcmeException;
|
||||
use Phar;
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* Suggests a command based on similarity in a list of available commands.
|
||||
*
|
||||
* @param string $badCommand invalid command
|
||||
* @param array $commands list of available commands
|
||||
* @param int $suggestThreshold similarity threshold
|
||||
* @return string suggestion or empty string if no command is similar enough
|
||||
*/
|
||||
function suggestCommand($badCommand, array $commands, $suggestThreshold = 70) {
|
||||
Assert::string($badCommand, "Bad command must be a string. Got: %s");
|
||||
Assert::integer($suggestThreshold, "Suggest threshold must be an integer. Got: %s");
|
||||
@@ -30,6 +39,13 @@ function suggestCommand($badCommand, array $commands, $suggestThreshold = 70) {
|
||||
return $bestMatchPercentage >= $suggestThreshold ? $bestMatch : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a server to a valid URI. If a valid shortcut is passed, it's resolved to the defined URI. If a URI without
|
||||
* protocol is passed, it will default to HTTPS.
|
||||
*
|
||||
* @param string $uri URI to resolve
|
||||
* @return string resolved URI
|
||||
*/
|
||||
function resolveServer($uri) {
|
||||
Assert::string($uri, "URI must be a string. Got: %s");
|
||||
|
||||
@@ -43,6 +59,10 @@ function resolveServer($uri) {
|
||||
return $shortcuts[$uri];
|
||||
}
|
||||
|
||||
if (strpos($uri, "/") === false) {
|
||||
throw new InvalidArgumentException("Invalid server URI: " . $uri);
|
||||
}
|
||||
|
||||
$protocol = substr($uri, 0, strpos($uri, "://"));
|
||||
|
||||
if (!$protocol || $protocol === $uri) {
|
||||
@@ -52,6 +72,12 @@ function resolveServer($uri) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a directory URI to a valid filename for usage as key file name.
|
||||
*
|
||||
* @param string $server URI to the directory
|
||||
* @return string identifier usable as file name
|
||||
*/
|
||||
function serverToKeyname($server) {
|
||||
$server = substr($server, strpos($server, "://") + 3);
|
||||
|
||||
@@ -62,6 +88,11 @@ function serverToKeyname($server) {
|
||||
return $keyFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the application is currently running as Phar.
|
||||
*
|
||||
* @return bool {@code true} if running as Phar, {@code false} otherwise
|
||||
*/
|
||||
function isPhar() {
|
||||
if (!class_exists("Phar")) {
|
||||
return false;
|
||||
@@ -70,10 +101,23 @@ function isPhar() {
|
||||
return Phar::running(true) !== "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a path. Replaces all backslashes with slashes and removes trailing slashes.
|
||||
*
|
||||
* @param string $path path to normalize
|
||||
* @return string normalized path
|
||||
*/
|
||||
function normalizePath($path) {
|
||||
return rtrim(str_replace("\\", "/", $path), "/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a consistent argument description for CLIMate. Valid arguments are "server" and "storage".
|
||||
*
|
||||
* @param string $argument argument name
|
||||
* @return array CLIMate argument description
|
||||
* @throws AcmeException if the provided acme-client.yml file is invalid
|
||||
*/
|
||||
function getArgumentDescription($argument) {
|
||||
$isPhar = \Kelunik\AcmeClient\isPhar();
|
||||
|
||||
@@ -140,10 +184,15 @@ function getArgumentDescription($argument) {
|
||||
return $argument;
|
||||
|
||||
default:
|
||||
throw new \InvalidArgumentException("Unknown argument: " . $argument);
|
||||
throw new InvalidArgumentException("Unknown argument: " . $argument);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the binary that currently runs. Can be included in help texts about other commands.
|
||||
*
|
||||
* @return string binary callable, shortened based on PATH and CWD
|
||||
*/
|
||||
function getBinary() {
|
||||
$binary = "bin/acme";
|
||||
|
||||
@@ -169,4 +218,26 @@ function getBinary() {
|
||||
}
|
||||
|
||||
return $binary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cuts a text to a certain length and appends an ellipsis if necessary.
|
||||
*
|
||||
* @param string $text text to shorten
|
||||
* @param int $max maximum length
|
||||
* @param string $append appendix when too long
|
||||
* @return string shortened string
|
||||
*/
|
||||
function ellipsis($text, $max = 70, $append = "…") {
|
||||
if (strlen($text) <= $max) {
|
||||
return $text;
|
||||
}
|
||||
|
||||
$out = substr($text, 0, $max);
|
||||
|
||||
if (strpos($text, " ") === false) {
|
||||
return $out . $append;
|
||||
}
|
||||
|
||||
return preg_replace("/\\w+$/", "", $out) . $append;
|
||||
}
|
||||
Reference in New Issue
Block a user