From 4df5ddd461287bd0e4b1b1a9b1bd93466aa09ed7 Mon Sep 17 00:00:00 2001 From: cdosoftei Date: Thu, 16 May 2024 18:05:57 -0400 Subject: [PATCH 1/8] :lock: Add phar signature --- .github/workflows/publish.yaml | 5 +++-- composer.json | 2 +- src/App.php | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 9fb9998..c6aa744 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -19,11 +19,12 @@ jobs: - run: chmod +x phar-composer-latest.phar - run: composer install --no-dev - run: ./phar-composer-latest.phar build . + - run: echo "${{ secrets.SIGNING_GPG_PRIVATE_KEY }}" | gpg --import --no-tty --batch --yes + - run: echo "${{ secrets.SIGNING_GPG_PASSPHRASE }}" | gpg --command-fd 0 --passphrase-fd 0 --pinentry-mode loopback -u 8636CB4D9CC1CD41EFC1EDC315ECDC2092AA50C1 --batch --detach-sign --armor --output eqivo.phar.asc eqivo.phar - uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: eqivo.phar - asset_name: eqivo.phar + file: eqivo.phar* tag: ${{ github.ref }} prerelease: true overwrite: true diff --git a/composer.json b/composer.json index 6967294..1eb3b88 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "rtckit/eqivo", "description": "Telephony API Platform", - "version": "0.6.1", + "version": "0.6.2", "keywords": [ "telecommunications", "voip", diff --git a/src/App.php b/src/App.php index c31e35c..9bc5416 100644 --- a/src/App.php +++ b/src/App.php @@ -13,7 +13,7 @@ class App extends AbstractApp { - public const VERSION = '0.6.1'; + public const VERSION = '0.6.2'; public HttpClientInterface $httpClient; From 0491356bca3ad54bb1e4132ea3ef3652d5d37fbf Mon Sep 17 00:00:00 2001 From: cdosoftei Date: Thu, 16 May 2024 18:36:57 -0400 Subject: [PATCH 2/8] :safety_vest: Removed possibly falsy string comparisons --- etc/phpstan.neon | 2 +- etc/psalm.xml | 1 + src/Config/CliArguments.php | 10 +++++----- src/Config/EnvironmentVars.php | 6 +++--- src/Config/LegacyConfigFile.php | 6 +++--- src/HttpClient.php | 2 +- src/Rest/Controller/AuthenticatedTrait.php | 2 +- src/Rest/Inquiry/V0_1/BulkCall.php | 22 ++++++++++++++++------ src/Rest/Inquiry/V0_1/Call.php | 9 ++++++--- src/Rest/Inquiry/V0_1/GroupCall.php | 22 ++++++++++++++++------ src/Rest/Server.php | 2 +- 11 files changed, 54 insertions(+), 30 deletions(-) diff --git a/etc/phpstan.neon b/etc/phpstan.neon index ae9257c..f92eb41 100644 --- a/etc/phpstan.neon +++ b/etc/phpstan.neon @@ -1,6 +1,6 @@ includes: - ../vendor/phpstan/phpstan/conf/bleedingEdge.neon parameters: - checkGenericClassInNonGenericObjectType: false ignoreErrors: + - identifier: missingType.generics - '#Parameter \#1 \$onFulfilled of method React\\Promise\\PromiseInterface::then\(\) expects \(callable\(mixed\):#' diff --git a/etc/psalm.xml b/etc/psalm.xml index 01306b1..473214b 100644 --- a/etc/psalm.xml +++ b/etc/psalm.xml @@ -20,6 +20,7 @@ + diff --git a/src/Config/CliArguments.php b/src/Config/CliArguments.php index c5d8402..3530406 100644 --- a/src/Config/CliArguments.php +++ b/src/Config/CliArguments.php @@ -67,7 +67,7 @@ public function resolve(AbstractSet $config): void $configFile = $args['c']; } - if ($configFile) { + if (is_string($configFile)) { switch (pathinfo($configFile, PATHINFO_EXTENSION)) { case 'json': case 'yml': @@ -168,7 +168,7 @@ function (string $value): bool { if (isset($args['rest-bind-address']) && is_string($args['rest-bind-address'])) { $err = Set::parseSocketAddr($args['rest-bind-address'], $ip, $port); - if ($err) { + if (is_string($err)) { fwrite(STDERR, 'Malformed --rest-bind-address argument: ip:port required' . PHP_EOL); fwrite(STDERR, $err . PHP_EOL); } else { @@ -245,7 +245,7 @@ function (string $value): bool { if (isset($args['outbound-bind-address']) && is_string($args['outbound-bind-address'])) { $err = Set::parseSocketAddr($args['outbound-bind-address'], $ip, $port); - if ($err) { + if (is_string($err)) { fwrite(STDERR, 'Malformed --outbound-bind-address argument: ip:port required' . PHP_EOL); fwrite(STDERR, $err . PHP_EOL); } else { @@ -263,7 +263,7 @@ function (string $value): bool { } else { $err = Set::parseSocketAddr($args['outbound-advertised-address'], $ip, $port); - if ($err) { + if (is_string($err)) { fwrite(STDERR, 'Malformed --outbound-advertised-address argument: ip:port or `inbound_socket_address` required' . PHP_EOL); fwrite(STDERR, $err . PHP_EOL); } else { @@ -313,7 +313,7 @@ function (string $value): bool { protected function help(): never { - $cmd = !isset($_SERVER['argv']) || !is_array($_SERVER['argv']) || empty($_SERVER['argv'][0]) + $cmd = !isset($_SERVER['argv']) || !is_array($_SERVER['argv']) || !is_string($_SERVER['argv'][0]) || !strlen($_SERVER['argv'][0]) ? './bin/ficore' : $_SERVER['argv'][0]; diff --git a/src/Config/EnvironmentVars.php b/src/Config/EnvironmentVars.php index 40dd7e2..a99101a 100644 --- a/src/Config/EnvironmentVars.php +++ b/src/Config/EnvironmentVars.php @@ -96,7 +96,7 @@ function (string $value): bool { if (isset($env[self::PREFIX . 'REST_BIND_ADDRESS'])) { $err = Set::parseSocketAddr($env[self::PREFIX . 'REST_BIND_ADDRESS'], $ip, $port); - if ($err) { + if (is_string($err)) { fwrite(STDERR, 'Malformed ' . self::PREFIX . 'REST_BIND_ADDRESS environment variable' . PHP_EOL); fwrite(STDERR, $err . PHP_EOL); } else { @@ -173,7 +173,7 @@ function (string $value): bool { if (isset($env[self::PREFIX . 'OUTBOUND_BIND_ADDRESS'])) { $err = Set::parseSocketAddr($env[self::PREFIX . 'OUTBOUND_BIND_ADDRESS'], $ip, $port); - if ($err) { + if (is_string($err)) { fwrite(STDERR, 'Malformed ' . self::PREFIX . 'OUTBOUND_BIND_ADDRESS environment variable' . PHP_EOL); fwrite(STDERR, $err . PHP_EOL); } else { @@ -191,7 +191,7 @@ function (string $value): bool { } else { $err = Set::parseSocketAddr($env[self::PREFIX . 'OUTBOUND_ADVERTISED_ADDRESS'], $ip, $port); - if ($err) { + if (is_string($err)) { fwrite(STDERR, 'Malformed ' . self::PREFIX . 'OUTBOUND_ADVERTISED_ADDRESS environment variable' . PHP_EOL); fwrite(STDERR, $err . PHP_EOL); } else { diff --git a/src/Config/LegacyConfigFile.php b/src/Config/LegacyConfigFile.php index c26bcde..ae71e67 100644 --- a/src/Config/LegacyConfigFile.php +++ b/src/Config/LegacyConfigFile.php @@ -102,7 +102,7 @@ function (string $value): bool { if (isset($legacy['rest_server']['HTTP_ADDRESS']) && is_string($legacy['rest_server']['HTTP_ADDRESS'])) { $err = Set::parseSocketAddr($legacy['rest_server']['HTTP_ADDRESS'], $ip, $port); - if ($err) { + if (is_string($err)) { fwrite(STDERR, 'Malformed HTTP_ADDRESS (rest_server) line in legacy configuration file' . PHP_EOL); fwrite(STDERR, $err . PHP_EOL); } else { @@ -117,7 +117,7 @@ function (string $value): bool { if (isset($legacy['rest_server']['FS_INBOUND_ADDRESS']) && is_string($legacy['rest_server']['FS_INBOUND_ADDRESS'])) { $err = Set::parseHostPort($legacy['rest_server']['FS_INBOUND_ADDRESS'], $host, $port); - if ($err) { + if (is_string($err)) { fwrite(STDERR, 'Malformed FS_INBOUND_ADDRESS (rest_server) line in legacy configuration file' . PHP_EOL); fwrite(STDERR, $err . PHP_EOL); } else { @@ -182,7 +182,7 @@ function (string $value): bool { if (isset($legacy['outbound_server']['FS_OUTBOUND_ADDRESS']) && is_string($legacy['outbound_server']['FS_OUTBOUND_ADDRESS'])) { $err = Set::parseSocketAddr($legacy['outbound_server']['FS_OUTBOUND_ADDRESS'], $ip, $port); - if ($err) { + if (is_string($err)) { fwrite(STDERR, 'Malformed FS_OUTBOUND_ADDRESS (outbound_server) line in legacy configuration file' . PHP_EOL); fwrite(STDERR, $err . PHP_EOL); } else { diff --git a/src/HttpClient.php b/src/HttpClient.php index 85d9172..1d6c1f2 100644 --- a/src/HttpClient.php +++ b/src/HttpClient.php @@ -63,7 +63,7 @@ public function makeRequest(string $url, string $method = 'POST', array $params throw new HttpClientException("Cannot send {$url}, cannot parse url"); } - if (!empty($parsed['query'])) { + if (isset($parsed['query']) && strlen($parsed['query'])) { parse_str($parsed['query'], $extra); $params = array_merge($params, $extra); } diff --git a/src/Rest/Controller/AuthenticatedTrait.php b/src/Rest/Controller/AuthenticatedTrait.php index f516034..d2e6fc4 100644 --- a/src/Rest/Controller/AuthenticatedTrait.php +++ b/src/Rest/Controller/AuthenticatedTrait.php @@ -101,7 +101,7 @@ protected function authenticate(ServerRequestInterface $request): PromiseInterfa $decoded = base64_decode($parts[1], true); - if (!$decoded) { + if ($decoded === false) { return reject(new AuthException('Cannot decode authentication payload')); } diff --git a/src/Rest/Inquiry/V0_1/BulkCall.php b/src/Rest/Inquiry/V0_1/BulkCall.php index 5fe5bd7..c2b1076 100644 --- a/src/Rest/Inquiry/V0_1/BulkCall.php +++ b/src/Rest/Inquiry/V0_1/BulkCall.php @@ -256,25 +256,35 @@ public function export(): RequestInterface /** @psalm-suppress PropertyTypeCoercion */ $request->gateways[$destIdx] = []; $gateways = explode(',', $destGateways); - $gatewayCodecs = !empty($this->gwCodecsList[$destIdx]) + $gatewayCodecs = + (isset($this->gwCodecsList[$destIdx]) && strlen($this->gwCodecsList[$destIdx])) ? str_getcsv($this->gwCodecsList[$destIdx], ',', "'") : []; - $gatewayTimeouts = !empty($this->gwTimeoutsList[$destIdx]) ? explode(',', $this->gwTimeoutsList[$destIdx]) : []; - $gatewayRetries = !empty($this->gwRetriesList[$destIdx]) ? explode(',', $this->gwRetriesList[$destIdx]) : []; + $gatewayTimeouts = + (isset($this->gwTimeoutsList[$destIdx]) && strlen($this->gwTimeoutsList[$destIdx])) + ? explode(',', $this->gwTimeoutsList[$destIdx]) + : []; + $gatewayRetries = + (isset($this->gwRetriesList[$destIdx]) && strlen($this->gwRetriesList[$destIdx])) + ? explode(',', $this->gwRetriesList[$destIdx]) + : []; foreach ($gateways as $gwIdx => $gateway) { $gw = new Gateway(); $gw->name = $gateway; - if (!empty($gatewayCodecs[$gwIdx])) { + if (isset($gatewayCodecs[$gwIdx]) && strlen($gatewayCodecs[$gwIdx])) { $gw->codecs = $gatewayCodecs[$gwIdx]; } - if (!empty($gatewayTimeouts[$gwIdx])) { + if (isset($gatewayTimeouts[$gwIdx]) && strlen($gatewayTimeouts[$gwIdx])) { $gw->timeout = intval($gatewayTimeouts[$gwIdx]); } - $gw->tries = empty($gatewayRetries[$gwIdx]) ? 1 : (int)$gatewayRetries[$gwIdx]; + $gw->tries = + (isset($gatewayRetries[$gwIdx]) && strlen($gatewayRetries[$gwIdx])) + ? (int)$gatewayRetries[$gwIdx] + : 1; /** @psalm-suppress PropertyTypeCoercion */ $request->gateways[$destIdx][] = $gw; diff --git a/src/Rest/Inquiry/V0_1/Call.php b/src/Rest/Inquiry/V0_1/Call.php index 7d820c6..538d0de 100644 --- a/src/Rest/Inquiry/V0_1/Call.php +++ b/src/Rest/Inquiry/V0_1/Call.php @@ -329,15 +329,18 @@ public function export(): RequestInterface $gw = new Gateway(); $gw->name = $gateway; - if (!empty($gatewayCodecs[$gwIdx])) { + if (isset($gatewayCodecs[$gwIdx]) && strlen($gatewayCodecs[$gwIdx])) { $gw->codecs = $gatewayCodecs[$gwIdx]; } - if (!empty($gatewayTimeouts[$gwIdx])) { + if (isset($gatewayTimeouts[$gwIdx]) && strlen($gatewayTimeouts[$gwIdx])) { $gw->timeout = intval($gatewayTimeouts[$gwIdx]); } - $gw->tries = empty($gatewayRetries[$gwIdx]) ? 1 : (int)$gatewayRetries[$gwIdx]; + $gw->tries = + (isset($gatewayRetries[$gwIdx]) && strlen($gatewayRetries[$gwIdx])) + ? (int)$gatewayRetries[$gwIdx] + : 1; $request->gateways[0][] = $gw; } diff --git a/src/Rest/Inquiry/V0_1/GroupCall.php b/src/Rest/Inquiry/V0_1/GroupCall.php index e0807b9..1fa00ae 100644 --- a/src/Rest/Inquiry/V0_1/GroupCall.php +++ b/src/Rest/Inquiry/V0_1/GroupCall.php @@ -284,25 +284,35 @@ public function export(): RequestInterface /** @psalm-suppress PropertyTypeCoercion */ $request->gateways[$destIdx] = []; $gateways = explode(',', $destGateways); - $gatewayCodecs = !empty($this->gwCodecsList[$destIdx]) + $gatewayCodecs = + (isset($this->gwCodecsList[$destIdx]) && strlen($this->gwCodecsList[$destIdx])) ? str_getcsv($this->gwCodecsList[$destIdx], ',', "'") : []; - $gatewayTimeouts = !empty($this->gwTimeoutsList[$destIdx]) ? explode(',', $this->gwTimeoutsList[$destIdx]) : []; - $gatewayRetries = !empty($this->gwRetriesList[$destIdx]) ? explode(',', $this->gwRetriesList[$destIdx]) : []; + $gatewayTimeouts = + (isset($this->gwTimeoutsList[$destIdx]) && strlen($this->gwTimeoutsList[$destIdx])) + ? explode(',', $this->gwTimeoutsList[$destIdx]) + : []; + $gatewayRetries = + (isset($this->gwRetriesList[$destIdx]) && strlen($this->gwRetriesList[$destIdx])) + ? explode(',', $this->gwRetriesList[$destIdx]) + : []; foreach ($gateways as $gwIdx => $gateway) { $gw = new Gateway(); $gw->name = $gateway; - if (!empty($gatewayCodecs[$gwIdx])) { + if (isset($gatewayCodecs[$gwIdx]) && strlen($gatewayCodecs[$gwIdx])) { $gw->codecs = $gatewayCodecs[$gwIdx]; } - if (!empty($gatewayTimeouts[$gwIdx])) { + if (isset($gatewayTimeouts[$gwIdx]) && strlen($gatewayTimeouts[$gwIdx])) { $gw->timeout = intval($gatewayTimeouts[$gwIdx]); } - $gw->tries = empty($gatewayRetries[$gwIdx]) ? 1 : (int)$gatewayRetries[$gwIdx]; + $gw->tries = + (isset($gatewayRetries[$gwIdx]) && strlen($gatewayRetries[$gwIdx])) + ? (int)$gatewayRetries[$gwIdx] + : 1; /** @psalm-suppress PropertyTypeCoercion */ $request->gateways[$destIdx][] = $gw; diff --git a/src/Rest/Server.php b/src/Rest/Server.php index c23c682..f75a3d7 100644 --- a/src/Rest/Server.php +++ b/src/Rest/Server.php @@ -100,7 +100,7 @@ public function run(): void if (!isset($this->config->restServerAdvertisedHost)) { $hostname = gethostname(); - if (!$hostname) { + if ($hostname === false || !strlen($hostname)) { $this->config->restServerAdvertisedHost = $this->config->appPrefix; } else { $this->config->restServerAdvertisedHost = $hostname; From effbdc23941867159053daecd61a949f25e90031 Mon Sep 17 00:00:00 2001 From: cdosoftei Date: Thu, 16 May 2024 18:44:04 -0400 Subject: [PATCH 3/8] :safety_vest: Typed class constants --- src/App.php | 1 + src/Config/EnvironmentVars.php | 1 + src/HttpClient.php | 1 + src/Plan/Dial/Handler.php | 1 + src/Plan/Parser/Conference.php | 4 ++++ src/Plan/Parser/Dial.php | 4 ++++ 6 files changed, 12 insertions(+) diff --git a/src/App.php b/src/App.php index 9bc5416..234f4a7 100644 --- a/src/App.php +++ b/src/App.php @@ -13,6 +13,7 @@ class App extends AbstractApp { + /** @var string */ public const VERSION = '0.6.2'; public HttpClientInterface $httpClient; diff --git a/src/Config/EnvironmentVars.php b/src/Config/EnvironmentVars.php index a99101a..7741d9c 100644 --- a/src/Config/EnvironmentVars.php +++ b/src/Config/EnvironmentVars.php @@ -15,6 +15,7 @@ class EnvironmentVars implements ResolverInterface { + /** @var string */ public const PREFIX = 'EQIVO_'; public function resolve(AbstractSet $config): void diff --git a/src/HttpClient.php b/src/HttpClient.php index 1d6c1f2..7f66243 100644 --- a/src/HttpClient.php +++ b/src/HttpClient.php @@ -22,6 +22,7 @@ class HttpClient implements HttpClientInterface protected string $signatureHeader; + /** @var int */ public const TIMEOUT = 60; public function setApp(App $app): static diff --git a/src/Plan/Dial/Handler.php b/src/Plan/Dial/Handler.php index 9f2cc1a..33b342a 100644 --- a/src/Plan/Dial/Handler.php +++ b/src/Plan/Dial/Handler.php @@ -38,6 +38,7 @@ class Handler implements HandlerInterface { use HandlerTrait; + /** @var int */ public const EVENT_TIMEOUT = 30; public function execute(Channel $channel, AbstractElement $element): PromiseInterface diff --git a/src/Plan/Parser/Conference.php b/src/Plan/Parser/Conference.php index c67b44b..126dcd1 100644 --- a/src/Plan/Parser/Conference.php +++ b/src/Plan/Parser/Conference.php @@ -20,12 +20,16 @@ class Conference implements ParserInterface /** @var string */ public const ELEMENT_TYPE = 'Conference'; + /** @var int */ public const DEFAULT_TIMELIMIT = 0; + /** @var int */ public const DEFAULT_MAXMEMBERS = 200; + /** @var list */ public const RECORD_FILE_FORMATS = ['wav', 'mp3']; + /** @var list */ public const ALLOWED_METHODS = ['GET', 'POST']; public function parse(RestXmlElement $xmlElement, Channel $channel): PromiseInterface diff --git a/src/Plan/Parser/Dial.php b/src/Plan/Parser/Dial.php index 86ab5ff..d2b833a 100644 --- a/src/Plan/Parser/Dial.php +++ b/src/Plan/Parser/Dial.php @@ -29,12 +29,16 @@ class Dial implements ParserInterface /** @var string */ public const ELEMENT_TYPE = 'Dial'; + /** @var bool */ public const NO_ANSWER = true; + /** @var list */ public const NESTABLES = ['Number']; + /** @var int */ public const DEFAULT_TIMELIMIT = 14400; + /** @var int */ public const DEFAULT_TIMEOUT = -1; public function parse(RestXmlElement $xmlElement, Channel $channel): PromiseInterface From 7521892e7968a9b28f333a31961e63d05b7be055 Mon Sep 17 00:00:00 2001 From: cdosoftei Date: Thu, 16 May 2024 18:52:56 -0400 Subject: [PATCH 4/8] :safety_vest: Typed class constants --- etc/psalm.xml | 1 + src/Plan/Parser/GetDigits.php | 3 +++ src/Plan/Parser/GetSpeech.php | 2 ++ src/Plan/Parser/Play.php | 1 + src/Plan/Parser/PreAnswer.php | 2 ++ src/Plan/Parser/Record.php | 3 +++ src/Plan/Parser/Redirect.php | 1 + src/Plan/Parser/SipTransfer.php | 2 ++ src/Plan/Parser/Speak.php | 3 +++ src/Plan/Parser/Wait.php | 1 + 10 files changed, 19 insertions(+) diff --git a/etc/psalm.xml b/etc/psalm.xml index 473214b..c2a74ce 100644 --- a/etc/psalm.xml +++ b/etc/psalm.xml @@ -21,6 +21,7 @@ + diff --git a/src/Plan/Parser/GetDigits.php b/src/Plan/Parser/GetDigits.php index fa29850..1fa5790 100644 --- a/src/Plan/Parser/GetDigits.php +++ b/src/Plan/Parser/GetDigits.php @@ -24,10 +24,13 @@ class GetDigits implements ParserInterface /** @var string */ public const ELEMENT_TYPE = 'GetDigits'; + /** @var list */ public const NESTABLES = ['Speak', 'Play', 'Wait']; + /** @var int */ public const DEFAULT_MAX_TONES = 99; + /** @var int */ public const DEFAULT_TIMEOUT = 5; public function parse(RestXmlElement $xmlElement, Channel $channel): PromiseInterface diff --git a/src/Plan/Parser/GetSpeech.php b/src/Plan/Parser/GetSpeech.php index a283291..d558fcf 100644 --- a/src/Plan/Parser/GetSpeech.php +++ b/src/Plan/Parser/GetSpeech.php @@ -24,8 +24,10 @@ class GetSpeech implements ParserInterface /** @var string */ public const ELEMENT_TYPE = 'GetSpeech'; + /** @var list */ public const NESTABLES = ['Speak', 'Play', 'Wait']; + /** @var int */ public const DEFAULT_TIMEOUT = 5; public function parse(RestXmlElement $xmlElement, Channel $channel): PromiseInterface diff --git a/src/Plan/Parser/Play.php b/src/Plan/Parser/Play.php index 8e6ba78..e9d8d45 100644 --- a/src/Plan/Parser/Play.php +++ b/src/Plan/Parser/Play.php @@ -20,6 +20,7 @@ class Play implements ParserInterface /** @var string */ public const ELEMENT_TYPE = 'Play'; + /** @var int */ public const MAX_LOOPS = 10000; public function parse(RestXmlElement $xmlElement, Channel $channel): PromiseInterface diff --git a/src/Plan/Parser/PreAnswer.php b/src/Plan/Parser/PreAnswer.php index ed1dd55..7ad2588 100644 --- a/src/Plan/Parser/PreAnswer.php +++ b/src/Plan/Parser/PreAnswer.php @@ -18,8 +18,10 @@ class PreAnswer implements ParserInterface /** @var string */ public const ELEMENT_TYPE = 'PreAnswer'; + /** @var bool */ public const NO_ANSWER = true; + /** @var list */ public const NESTABLES = [ 'GetDigits', 'GetSpeech', diff --git a/src/Plan/Parser/Record.php b/src/Plan/Parser/Record.php index c6f3e14..c15fbfb 100644 --- a/src/Plan/Parser/Record.php +++ b/src/Plan/Parser/Record.php @@ -20,10 +20,13 @@ class Record implements ParserInterface /** @var string */ public const ELEMENT_TYPE = 'Record'; + /** @var list */ public const RECORD_FILE_FORMATS = ['wav', 'mp3']; + /** @var string */ public const DEFAULT_RECORD_FORMAT = 'mp3'; + /** @var list */ public const ALLOWED_METHODS = ['GET', 'POST']; public function parse(RestXmlElement $xmlElement, Channel $channel): PromiseInterface diff --git a/src/Plan/Parser/Redirect.php b/src/Plan/Parser/Redirect.php index 049a162..c7e43ee 100644 --- a/src/Plan/Parser/Redirect.php +++ b/src/Plan/Parser/Redirect.php @@ -23,6 +23,7 @@ class Redirect implements ParserInterface /** @var string */ public const ELEMENT_TYPE = 'Redirect'; + /** @var bool */ public const NO_ANSWER = true; public function parse(RestXmlElement $xmlElement, Channel $channel): PromiseInterface diff --git a/src/Plan/Parser/SipTransfer.php b/src/Plan/Parser/SipTransfer.php index 3c3b8ef..2078f24 100644 --- a/src/Plan/Parser/SipTransfer.php +++ b/src/Plan/Parser/SipTransfer.php @@ -21,8 +21,10 @@ class SipTransfer implements ParserInterface /** @var string */ public const ELEMENT_TYPE = 'SIPTransfer'; + /** @var bool */ public const NO_ANSWER = true; + /** @var list */ public const ALLOWED_SCHEMES = ['sip', 'sips']; public function parse(RestXmlElement $xmlElement, Channel $channel): PromiseInterface diff --git a/src/Plan/Parser/Speak.php b/src/Plan/Parser/Speak.php index 0d017eb..c6751e2 100644 --- a/src/Plan/Parser/Speak.php +++ b/src/Plan/Parser/Speak.php @@ -20,8 +20,10 @@ class Speak implements ParserInterface /** @var string */ public const ELEMENT_TYPE = 'Speak'; + /** @var int */ public const MAX_LOOPS = 10000; + /** @var list */ public const METHODS = [ 'N/A', 'PRONOUNCED', @@ -30,6 +32,7 @@ class Speak implements ParserInterface 'PRONOUNCED_YEAR' ]; + /** @var list */ public const TYPES = [ 'NUMBER', 'ITEMS', diff --git a/src/Plan/Parser/Wait.php b/src/Plan/Parser/Wait.php index a220dd2..34dd17b 100644 --- a/src/Plan/Parser/Wait.php +++ b/src/Plan/Parser/Wait.php @@ -20,6 +20,7 @@ class Wait implements ParserInterface /** @var string */ public const ELEMENT_TYPE = 'Wait'; + /** @var bool */ public const NO_ANSWER = true; public function parse(RestXmlElement $xmlElement, Channel $channel): PromiseInterface From f6aea9f844fe3f3171190d4e1af00d23cbf7fa0e Mon Sep 17 00:00:00 2001 From: cdosoftei Date: Thu, 16 May 2024 18:57:14 -0400 Subject: [PATCH 5/8] :green_heart: Update publishing workflow --- .github/workflows/publish.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index c6aa744..5261a80 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -24,6 +24,7 @@ jobs: - uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} + file_glob: true file: eqivo.phar* tag: ${{ github.ref }} prerelease: true From fb4c489270bbabbb8ebcf688880b7d496db516ff Mon Sep 17 00:00:00 2001 From: cdosoftei Date: Thu, 23 May 2024 18:42:25 -0400 Subject: [PATCH 6/8] :sparkles: ConferenceHold/Unhold --- bin/eqivo | 2 + composer.json | 2 +- src/Rest/Controller/V0_1/ConferenceHold.php | 104 ++++++++++++++++++ src/Rest/Controller/V0_1/ConferenceUnhold.php | 104 ++++++++++++++++++ src/Rest/Inquiry/V0_1/ConferenceHold.php | 64 +++++++++++ src/Rest/Inquiry/V0_1/ConferenceUnhold.php | 52 +++++++++ src/Rest/Response/V0_1/ConferenceHold.php | 75 +++++++++++++ src/Rest/Response/V0_1/ConferenceUnhold.php | 75 +++++++++++++ src/Rest/View/V0_1/ConferenceHold.php | 11 ++ src/Rest/View/V0_1/ConferenceUnhold.php | 11 ++ 10 files changed, 499 insertions(+), 1 deletion(-) create mode 100644 src/Rest/Controller/V0_1/ConferenceHold.php create mode 100644 src/Rest/Controller/V0_1/ConferenceUnhold.php create mode 100644 src/Rest/Inquiry/V0_1/ConferenceHold.php create mode 100644 src/Rest/Inquiry/V0_1/ConferenceUnhold.php create mode 100644 src/Rest/Response/V0_1/ConferenceHold.php create mode 100644 src/Rest/Response/V0_1/ConferenceUnhold.php create mode 100644 src/Rest/View/V0_1/ConferenceHold.php create mode 100644 src/Rest/View/V0_1/ConferenceUnhold.php diff --git a/bin/eqivo b/bin/eqivo index 852fa65..2fa020c 100755 --- a/bin/eqivo +++ b/bin/eqivo @@ -144,6 +144,7 @@ $eqivo->setRestServer( ->setRouteController('POST', '/v0.1/CancelScheduledPlay/', new Eqivo\Rest\Controller\V0_1\CancelScheduledPlay) ->setRouteController('POST', '/v0.1/ConferenceDeaf/', new Eqivo\Rest\Controller\V0_1\ConferenceDeaf) ->setRouteController('POST', '/v0.1/ConferenceHangup/', new Eqivo\Rest\Controller\V0_1\ConferenceHangup) + ->setRouteController('POST', '/v0.1/ConferenceHold/', new Eqivo\Rest\Controller\V0_1\ConferenceHold) ->setRouteController('POST', '/v0.1/ConferenceKick/', new Eqivo\Rest\Controller\V0_1\ConferenceKick) ->setRouteController('POST', '/v0.1/ConferenceList/', new Eqivo\Rest\Controller\V0_1\ConferenceList) ->setRouteController('POST', '/v0.1/ConferenceListMembers/', new Eqivo\Rest\Controller\V0_1\ConferenceListMembers) @@ -153,6 +154,7 @@ $eqivo->setRestServer( ->setRouteController('POST', '/v0.1/ConferenceRecordStop/', new Eqivo\Rest\Controller\V0_1\ConferenceRecordStop) ->setRouteController('POST', '/v0.1/ConferenceSpeak/', new Eqivo\Rest\Controller\V0_1\ConferenceSpeak) ->setRouteController('POST', '/v0.1/ConferenceUndeaf/', new Eqivo\Rest\Controller\V0_1\ConferenceUndeaf) + ->setRouteController('POST', '/v0.1/ConferenceUnhold/', new Eqivo\Rest\Controller\V0_1\ConferenceUnhold) ->setRouteController('POST', '/v0.1/ConferenceUnmute/', new Eqivo\Rest\Controller\V0_1\ConferenceUnmute) ->setRouteController('POST', '/v0.1/GroupCall/', new Eqivo\Rest\Controller\V0_1\GroupCall) ->setRouteController('POST', '/v0.1/HangupAllCalls/', new Eqivo\Rest\Controller\V0_1\HangupAllCalls) diff --git a/composer.json b/composer.json index 1eb3b88..e4a4e06 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ "react/http": "^1.9", "react/promise": "^3.1", "rtckit/esl": "^0.8", - "rtckit/ficore": "v0.0.3", + "rtckit/ficore": "v0.0.4.x-dev", "rtckit/sip": "^0.7", "symfony/yaml": "^6.3", "wikimedia/ip-set": "^4.0" diff --git a/src/Rest/Controller/V0_1/ConferenceHold.php b/src/Rest/Controller/V0_1/ConferenceHold.php new file mode 100644 index 0000000..42adbb0 --- /dev/null +++ b/src/Rest/Controller/V0_1/ConferenceHold.php @@ -0,0 +1,104 @@ +view = new ConferenceHoldView(); + } + + public function execute(ServerRequestInterface $request): PromiseInterface + { + $response = new ConferenceHoldResponse(); + $inquiry = ConferenceHoldInquiry::factory($request); + + return $this->doExecute($request, $response, $inquiry); + } + + /** + * Validates API call parameters + * + * @param AbstractInquiry $inquiry + * @param AbstractResponse $response + */ + public function validate(AbstractInquiry $inquiry, AbstractResponse $response): void + { + assert($inquiry instanceof ConferenceHoldInquiry); + assert($response instanceof ConferenceHoldResponse); + + if (!isset($inquiry->ConferenceName)) { + $response->Message = ConferenceHoldResponse::MESSAGE_NO_CONFERENCE_NAME; + $response->Success = false; + + return; + } + + if (!isset($inquiry->MemberID)) { + $response->Message = ConferenceHoldResponse::MESSAGE_NO_MEMBER_ID; + $response->Success = false; + + return; + } + + $conference = $this->app->getConferenceByRoom($inquiry->ConferenceName); + + if (!isset($conference)) { + $response->Message = ConferenceHoldResponse::MESSAGE_NOT_FOUND; + $response->Success = false; + + return; + } + + $inquiry->core = $conference->core; + } +} diff --git a/src/Rest/Controller/V0_1/ConferenceUnhold.php b/src/Rest/Controller/V0_1/ConferenceUnhold.php new file mode 100644 index 0000000..6b411f9 --- /dev/null +++ b/src/Rest/Controller/V0_1/ConferenceUnhold.php @@ -0,0 +1,104 @@ +view = new ConferenceUnholdView(); + } + + public function execute(ServerRequestInterface $request): PromiseInterface + { + $response = new ConferenceUnholdResponse(); + $inquiry = ConferenceUnholdInquiry::factory($request); + + return $this->doExecute($request, $response, $inquiry); + } + + /** + * Validates API call parameters + * + * @param AbstractInquiry $inquiry + * @param AbstractResponse $response + */ + public function validate(AbstractInquiry $inquiry, AbstractResponse $response): void + { + assert($inquiry instanceof ConferenceUnholdInquiry); + assert($response instanceof ConferenceUnholdResponse); + + if (!isset($inquiry->ConferenceName)) { + $response->Message = ConferenceUnholdResponse::MESSAGE_NO_CONFERENCE_NAME; + $response->Success = false; + + return; + } + + if (!isset($inquiry->MemberID)) { + $response->Message = ConferenceUnholdResponse::MESSAGE_NO_MEMBER_ID; + $response->Success = false; + + return; + } + + $conference = $this->app->getConferenceByRoom($inquiry->ConferenceName); + + if (!isset($conference)) { + $response->Message = ConferenceUnholdResponse::MESSAGE_NOT_FOUND; + $response->Success = false; + + return; + } + + $inquiry->core = $conference->core; + } +} diff --git a/src/Rest/Inquiry/V0_1/ConferenceHold.php b/src/Rest/Inquiry/V0_1/ConferenceHold.php new file mode 100644 index 0000000..95eee00 --- /dev/null +++ b/src/Rest/Inquiry/V0_1/ConferenceHold.php @@ -0,0 +1,64 @@ +core->getConferenceByRoom($this->ConferenceName); + $request = new Member\Request(); + $request->action = Member\ActionEnum::ExtHold; + $request->members = explode(',', $this->MemberID); + + if (isset($this->FilePath)) { + $request->medium = $this->FilePath; + } + + if (isset($conference)) { + $request->conference = $conference; + } + + return $request; + } +} diff --git a/src/Rest/Inquiry/V0_1/ConferenceUnhold.php b/src/Rest/Inquiry/V0_1/ConferenceUnhold.php new file mode 100644 index 0000000..98ce6bb --- /dev/null +++ b/src/Rest/Inquiry/V0_1/ConferenceUnhold.php @@ -0,0 +1,52 @@ +core->getConferenceByRoom($this->ConferenceName); + $request = new Member\Request(); + $request->action = Member\ActionEnum::ExtUnhold; + $request->members = explode(',', $this->MemberID); + + if (isset($conference)) { + $request->conference = $conference; + } + + return $request; + } +} diff --git a/src/Rest/Response/V0_1/ConferenceHold.php b/src/Rest/Response/V0_1/ConferenceHold.php new file mode 100644 index 0000000..9d8af7d --- /dev/null +++ b/src/Rest/Response/V0_1/ConferenceHold.php @@ -0,0 +1,75 @@ + + * @OA\Property( + * @OA\Items(type="string"), + * example={"13", "42"} + * ) + */ + public array $Members = []; + + /** + * Whether the request was successful or not + * + * @OA\Property( + * example=true + * ) + */ + public bool $Success; + + public function import(ResponseInterface $response): static + { + assert($response instanceof Member\Response); + + $this->Success = true; + $this->Message = $response->successful ? self::MESSAGE_SUCCESS : self::MESSAGE_FAILED; + $this->Members = $response->members; + + return $this; + } +} diff --git a/src/Rest/Response/V0_1/ConferenceUnhold.php b/src/Rest/Response/V0_1/ConferenceUnhold.php new file mode 100644 index 0000000..8fe6569 --- /dev/null +++ b/src/Rest/Response/V0_1/ConferenceUnhold.php @@ -0,0 +1,75 @@ + + * @OA\Property( + * @OA\Items(type="string"), + * example={"13", "42"} + * ) + */ + public array $Members = []; + + /** + * Whether the request was successful or not + * + * @OA\Property( + * example=true + * ) + */ + public bool $Success; + + public function import(ResponseInterface $response): static + { + assert($response instanceof Member\Response); + + $this->Success = true; + $this->Message = $response->successful ? self::MESSAGE_SUCCESS : self::MESSAGE_FAILED; + $this->Members = $response->members; + + return $this; + } +} diff --git a/src/Rest/View/V0_1/ConferenceHold.php b/src/Rest/View/V0_1/ConferenceHold.php new file mode 100644 index 0000000..b9af755 --- /dev/null +++ b/src/Rest/View/V0_1/ConferenceHold.php @@ -0,0 +1,11 @@ + Date: Fri, 24 May 2024 10:57:17 -0400 Subject: [PATCH 7/8] :arrow_up: Update dependencies (ficore) --- composer.json | 2 +- src/Plan/Parser/Dial.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index e4a4e06..a27c93e 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ "react/http": "^1.9", "react/promise": "^3.1", "rtckit/esl": "^0.8", - "rtckit/ficore": "v0.0.4.x-dev", + "rtckit/ficore": "0.0.4", "rtckit/sip": "^0.7", "symfony/yaml": "^6.3", "wikimedia/ip-set": "^4.0" diff --git a/src/Plan/Parser/Dial.php b/src/Plan/Parser/Dial.php index d2b833a..fd83ba1 100644 --- a/src/Plan/Parser/Dial.php +++ b/src/Plan/Parser/Dial.php @@ -231,7 +231,6 @@ public function parse(RestXmlElement $xmlElement, Channel $channel): PromiseInte return all($promises) ->then(function (array $playbackArray) use ($element) { if (!empty($playbackArray['confirmSounds'])) { - /** @phpstan-ignore-next-line */ $element->confirmSounds = $playbackArray['confirmSounds']; } From 5d92b66e403684be322afbfce215cde58e716985 Mon Sep 17 00:00:00 2001 From: cdosoftei Date: Fri, 24 May 2024 11:19:36 -0400 Subject: [PATCH 8/8] :memo: Update OpenAPI specification --- openapi.yaml | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/openapi.yaml b/openapi.yaml index 65c3833..c7819f4 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -136,6 +136,28 @@ paths: security: - basicAuth: [] + /v0.1/ConferenceHold/: + post: + tags: + - Conference + summary: /v0.1/ConferenceHold/ + description: 'Put one or more conference members on hold' + requestBody: + description: 'POST parameters' + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ConferenceHoldParameters' + responses: + '200': + description: Response + content: + application/json: + schema: + $ref: '#/components/schemas/ConferenceHoldResponse' + security: + - + basicAuth: [] /v0.1/ConferenceKick/: post: tags: @@ -334,6 +356,28 @@ paths: security: - basicAuth: [] + /v0.1/ConferenceUnhold/: + post: + tags: + - Conference + summary: /v0.1/ConferenceUnhold/ + description: 'Resume one or more members in the conference context' + requestBody: + description: 'POST parameters' + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ConferenceUnholdParameters' + responses: + '200': + description: Response + content: + application/json: + schema: + $ref: '#/components/schemas/ConferenceUnholdResponse' + security: + - + basicAuth: [] /v0.1/ConferenceUnmute/: post: tags: @@ -901,6 +945,25 @@ components: type: string example: '13,42' type: object + ConferenceHoldParameters: + required: + - ConferenceName + - FilePath + - MemberID + properties: + ConferenceName: + description: 'Name of the conference in question' + type: string + example: Room402 + FilePath: + description: 'Path/URI of the media file to be played as music-on-hold' + type: string + example: /var/local/media/sample.wav + MemberID: + description: 'List of comma separated member IDs to be affected; `all` shorthand is available however it is not capable of playing back the music-on-hold (i.e. use specific member IDs when MOH is necessary)' + type: string + example: '13,42' + type: object ConferenceKickParameters: required: - ConferenceName @@ -1070,6 +1133,20 @@ components: type: string example: '13,42' type: object + ConferenceUnholdParameters: + required: + - ConferenceName + - MemberID + properties: + ConferenceName: + description: 'Name of the conference in question' + type: string + example: Room402 + MemberID: + description: 'List of comma separated member IDs to be affected; `all` shorthand is available too but it is not capable of stopping the music-oh-hold.' + type: string + example: '13,42' + type: object ConferenceUnmuteParameters: required: - ConferenceName @@ -1606,6 +1683,33 @@ components: type: boolean example: true type: object + ConferenceHoldResponse: + required: + - Message + - Success + properties: + Message: + description: 'Response message' + type: string + enum: + - 'Conference Hold Executed' + - 'ConferenceName Parameter must be present' + - 'MemberID Parameter must be present' + - 'Conference Hold Failed -- Conference not found' + example: 'Conference Hold Executed' + Members: + description: 'List of affected members' + type: array + items: + type: string + example: + - '13' + - '42' + Success: + description: 'Whether the request was successful or not' + type: boolean + example: true + type: object ConferenceKickResponse: required: - Message @@ -1864,6 +1968,33 @@ components: type: boolean example: true type: object + ConferenceUnholdResponse: + required: + - Message + - Success + properties: + Message: + description: 'Response message' + type: string + enum: + - 'Conference Unhold Executed' + - 'ConferenceName Parameter must be present' + - 'MemberID Parameter must be present' + - 'Conference Unhold Failed -- Conference not found' + example: 'Conference Unhold Executed' + Members: + description: 'List of affected members' + type: array + items: + type: string + example: + - '13' + - '42' + Success: + description: 'Whether the request was successful or not' + type: boolean + example: true + type: object ConferenceUnmuteResponse: required: - Message