From 88d932658885d5027684eab2bf47802a297ad946 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Tue, 14 Dec 2021 23:47:00 -0800 Subject: [PATCH] Civi::pipe - Convet from service-registry to negotation-flags How will the protocol evolve? This changes the planned mechanism that will allow evoluation. _Previous/Original_: Protocol had a version number. _Previous/Interm_: Any change to the protocol requires registering a new service. New connections must strictly choose between one service XOR another service. _Now_: Any change to the protocol requires defining a flag. There is a list of default flags, but callers may request alternative flags. The header line indicates success or failure of these flags. Comment: Handy characteristics of this design: * Default info is generally more useful and skimmable. * Addresses the current trusted/untrusted flag. * Expands the default info to include (1) CiviCRM version and (2) whether logins are allowed. * Allows augmenting or replacing jsonrpc-2.0 (if we don't like it). * The `Civi::pipe()` shell statements remain pithy - even if they require extra flags. --- Civi.php | 19 +++++++--- Civi/Core/Container.php | 4 +- Civi/Pipe/LineSessionTrait.php | 12 ++++-- Civi/Pipe/PipeSession.php | 37 ++++++++++++++++++- .../phpunit/Civi/Pipe/JsonRpcSessionTest.php | 5 +-- tests/phpunit/E2E/Extern/CliRunnerTest.php | 4 +- 6 files changed, 64 insertions(+), 17 deletions(-) diff --git a/Civi.php b/Civi.php index 828d58ecbd..a11c38b5a1 100644 --- a/Civi.php +++ b/Civi.php @@ -117,13 +117,22 @@ class Civi { /** * Initiate a bidirectional pipe for exchanging a series of multiple API requests. * - * @see \Civi\Pipe\BasicJsonSession + * @param string $negotiationFlags + * List of pipe initialization flags. Some combination of the following: + * - 'v': Report version in connection header. + * - 'j': Report JSON-RPC flavors in connection header. + * - 'l': Report on login support in connection header. + * - 't': Trusted session. Logins do not require credentials. API calls may execute with or without permission-checks. + * - 'u': Untrusted session. Logins require credentials. API calls may only execute with permission-checks. + * Note: The `Civi::pipe()` entry-point is designed to be amenable to shell orchestration (SSH/cv/drush/wp-cli/etc). + * The negotiation flags are therefore condensed to individual characters. + * Note: Future flags may be added to the default list. But be careful about removing flags from the default list. + * @see \Civi\Pipe\PipeSession */ - public static function pipe(string $protocol = 'jsonrpc20'): void { - Civi::service('pipe.' . $protocol) + public static function pipe(string $negotiationFlags = 'vtl'): void { + Civi::service('civi.pipe') ->setIO(STDIN, STDOUT) - ->setTrusted(TRUE) - ->run(); + ->run($negotiationFlags); } /** diff --git a/Civi/Core/Container.php b/Civi/Core/Container.php index daee0396cb..a6e6d555af 100644 --- a/Civi/Core/Container.php +++ b/Civi/Core/Container.php @@ -262,10 +262,10 @@ class Container { } $container->setAlias('cache.short', 'cache.default')->setPublic(TRUE); - $container->setDefinition('pipe.jsonrpc20', new Definition( + $container->setDefinition('civi.pipe', new Definition( 'Civi\Pipe\PipeSession', [] - ))->setPublic(TRUE); + ))->setPublic(TRUE)->setShared(FALSE); $container->setDefinition('resources', new Definition( 'CRM_Core_Resources', diff --git a/Civi/Pipe/LineSessionTrait.php b/Civi/Pipe/LineSessionTrait.php index 97828f8298..5de7025bdd 100644 --- a/Civi/Pipe/LineSessionTrait.php +++ b/Civi/Pipe/LineSessionTrait.php @@ -32,10 +32,13 @@ trait LineSessionTrait { /** * The onConnect() method is called when a new session is opened. * + * @param string $negotiationFlags + * List of pipe initialization flags. See Civi::pipe() for description of flags. * @return string|null * Header/welcome line, or NULL if none. + * @see Civi::pipe */ - protected function onConnect(): ?string { + protected function onConnect(string $negotiationFlags): ?string { return NULL; } @@ -110,9 +113,12 @@ trait LineSessionTrait { /** * Run the main loop. Poll for commands on $input and write responses to $output. + * + * @param string $negotiationFlags + * List of pipe initialization flags. See Civi::pipe() for description of flags. */ - public function run() { - $this->write($this->onConnect()); + public function run(string $negotiationFlags = '') { + $this->write($this->onConnect($negotiationFlags)); while (FALSE !== ($line = stream_get_line($this->input, $this->bufferSize, $this->delimiter))) { $line = rtrim($line, $this->delimiter); diff --git a/Civi/Pipe/PipeSession.php b/Civi/Pipe/PipeSession.php index 322942c95d..f15c1a4f98 100644 --- a/Civi/Pipe/PipeSession.php +++ b/Civi/Pipe/PipeSession.php @@ -32,10 +32,43 @@ class PipeSession { /** * @inheritDoc */ - protected function onConnect(): ?string { + protected function onConnect(string $negotiationFlags): ?string { \CRM_Core_Session::useFakeSession(); $this->methods = new PublicMethods(); - return json_encode(["Civi::pipe" => ['jsonrpc20']]); + + // Convention: Every negotiation-flag should produce exactly one output in the header line. + foreach (str_split($negotiationFlags) as $flag) { + switch ($flag) { + case 'v': + $flags[$flag] = \CRM_Utils_System::version(); + break; + + case 'j': + $flags[$flag] = ['jsonrpc-2.0']; + break; + + case 'l': + $flags[$flag] = function_exists('authx_login') ? ['login'] : ['nologin']; + break; + + case 't': + $this->setTrusted(TRUE); + $flags[$flag] = 'trusted'; + break; + + case 'u': + $this->setTrusted(FALSE); + $flags[$flag] = 'untrusted'; + break; + + default: + // What flags might exist in the future? We don't know! Communicate that we don't know. + $flags[$flag] = NULL; + break; + } + } + + return json_encode(["Civi::pipe" => $flags]); } /** diff --git a/tests/phpunit/Civi/Pipe/JsonRpcSessionTest.php b/tests/phpunit/Civi/Pipe/JsonRpcSessionTest.php index 7ed3218e00..5ac8f6ba6b 100644 --- a/tests/phpunit/Civi/Pipe/JsonRpcSessionTest.php +++ b/tests/phpunit/Civi/Pipe/JsonRpcSessionTest.php @@ -16,7 +16,7 @@ namespace Civi\Pipe; */ class JsonRpcSessionTest extends \CiviUnitTestCase { - protected $standardHeader = '{"Civi::pipe":["jsonrpc20"]}'; + protected $standardHeader = '{"Civi::pipe":{"t":"trusted"}}'; protected $input; protected $output; protected $server; @@ -26,7 +26,6 @@ class JsonRpcSessionTest extends \CiviUnitTestCase { $this->input = fopen('php://memory', 'w'); $this->output = fopen('php://memory', 'w'); $this->server = new PipeSession($this->input, $this->output); - $this->server->setTrusted(TRUE); } protected function tearDown(): void { @@ -209,7 +208,7 @@ class JsonRpcSessionTest extends \CiviUnitTestCase { } fseek($this->input, 0); - $this->server->run(); + $this->server->run("t"); fseek($this->output, 0); return explode("\n", stream_get_contents($this->output)); diff --git a/tests/phpunit/E2E/Extern/CliRunnerTest.php b/tests/phpunit/E2E/Extern/CliRunnerTest.php index 42cb6fa4d6..b7c65915f5 100644 --- a/tests/phpunit/E2E/Extern/CliRunnerTest.php +++ b/tests/phpunit/E2E/Extern/CliRunnerTest.php @@ -123,7 +123,7 @@ class E2E_Extern_CliRunnerTest extends CiviEndToEndTestCase { * @dataProvider getRunners */ public function testPipe($name, $runner) { - $cmd = strtr($runner, ['@PHP' => escapeshellarg('Civi::pipe("jsonrpc20");')]); + $cmd = strtr($runner, ['@PHP' => escapeshellarg('Civi::pipe("t");')]); $desc = [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'a']]; $process = proc_open($cmd, $desc, $pipes); @@ -136,7 +136,7 @@ class E2E_Extern_CliRunnerTest extends CiviEndToEndTestCase { return $decode; }; - $this->assertEquals(['Civi::pipe' => ['jsonrpc20']], $read(), "Expect standard Civi::pipe header when starting via $name"); + $this->assertEquals(['Civi::pipe' => ['t' => 'trusted']], $read(), "Expect standard Civi::pipe header when starting via $name"); $write('echo', ['a' => 123]); $this->assertEquals(['a' => 123], $read()['result']); -- 2.25.1