Skip to content

Commit bfb057b

Browse files
committed
Merge branch 'develop' into enhancement/nativeEnums
# Conflicts: # composer.json # src/Parameters/CreateMeetingParameters.php # src/Parameters/JoinMeetingParameters.php # tests/BigBlueButtonTest.php # tests/Parameters/CreateMeetingParametersTest.php
2 parents b8044ed + 15d87af commit bfb057b

79 files changed

Lines changed: 1805 additions & 469 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

composer.json

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
"name": "Ghazi Triki",
1515
"email": "ghazi.triki@riadvice.tn",
1616
"role": "Developer"
17+
},
18+
{
19+
"name": "Tim Korn",
20+
"email": "korn@aufKurs.de",
21+
"role": "Developer"
1722
}
1823
],
1924
"repositories": {
@@ -25,24 +30,24 @@
2530
"require": {
2631
"php": ">=8.1",
2732
"ext-curl": "*",
28-
"ext-simplexml": "*",
33+
"ext-json": "*",
2934
"ext-mbstring": "*",
30-
"ext-json": "*"
35+
"ext-simplexml": "*"
3136
},
3237
"require-dev": {
33-
"phpunit/phpunit": "^10.5",
38+
"bmitch/churn-php": "^1.7",
39+
"captainhook/captainhook": "^5.23",
40+
"captainhook/hook-installer": "^1.0",
3441
"fakerphp/faker": "^1.23",
3542
"friendsofphp/php-cs-fixer": "^3.54",
36-
"squizlabs/php_codesniffer": "^3.9",
3743
"nunomaduro/phpinsights": "^2.11",
38-
"bmitch/churn-php": "^1.7",
39-
"wapmorgan/php-deprecation-detector": "^2.0",
4044
"phpstan/phpstan": "^1.10",
45+
"phpunit/php-code-coverage": "^10.1",
46+
"phpunit/phpunit": "^10.5",
47+
"squizlabs/php_codesniffer": "^3.9",
4148
"tracy/tracy": "^2.10",
4249
"vlucas/phpdotenv": "^5.6",
43-
"phpunit/php-code-coverage": "^10.1",
44-
"captainhook/captainhook": "^5.23",
45-
"captainhook/hook-installer": "^1.0"
50+
"wapmorgan/php-deprecation-detector": "^2.0"
4651
},
4752
"scripts": {
4853
"code-check": "./vendor/bin/phpstan analyse",
@@ -70,6 +75,7 @@
7075
}
7176
},
7277
"config": {
78+
"sort-packages": true,
7379
"allow-plugins": {
7480
"dealerdirect/phpcodesniffer-composer-installer": true,
7581
"captainhook/captainhook-phar": true,

phpunit.xml.dist

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
<directory>tests</directory>
2323
</testsuite>
2424
</testsuites>
25+
<groups>
26+
<exclude>
27+
<group>ignore</group>
28+
</exclude>
29+
</groups>
2530
<source>
2631
<include>
2732
<directory suffix=".php">src</directory>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
/*
4+
* BigBlueButton open source conferencing system - https://www.bigbluebutton.org/.
5+
*
6+
* Copyright (c) 2016-2024 BigBlueButton Inc. and by respective authors (see below).
7+
*
8+
* This program is free software; you can redistribute it and/or modify it under the
9+
* terms of the GNU Lesser General Public License as published by the Free Software
10+
* Foundation; either version 3.0 of the License, or (at your option) any later
11+
* version.
12+
*
13+
* BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
14+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
15+
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Lesser General Public License along
18+
* with BigBlueButton; if not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
namespace BigBlueButton\Attribute;
22+
23+
#[\Attribute(\Attribute::TARGET_METHOD)]
24+
class ApiParameterMapper
25+
{
26+
private string $attributeName;
27+
28+
public function __construct(string $attributeName)
29+
{
30+
$this->attributeName = $attributeName;
31+
}
32+
33+
public function getAttributeName(): string
34+
{
35+
return $this->attributeName;
36+
}
37+
}

src/BigBlueButton.php

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,13 @@ public function __construct(?string $baseUrl = null, ?string $secret = null, ?ar
9494
{
9595
// Provide an early error message if configuration is wrong
9696
if (is_null($baseUrl) && false === getenv('BBB_SERVER_BASE_URL')) {
97-
throw new \RuntimeException('No BBB-Server-Url found! Please provide it either in constructor ' .
98-
"(1st argument) or by environment variable 'BBB_SERVER_BASE_URL'!");
97+
throw new \RuntimeException('No BBB-Server-Url found! Please provide it either in constructor '
98+
. "(1st argument) or by environment variable 'BBB_SERVER_BASE_URL'!");
9999
}
100100

101101
if (is_null($secret) && false === getenv('BBB_SECRET') && false === getenv('BBB_SECURITY_SALT')) {
102-
throw new \RuntimeException('No BBB-Secret (or BBB-Salt) found! Please provide it either in constructor ' .
103-
"(2nd argument) or by environment variable 'BBB_SECRET' (or 'BBB_SECURITY_SALT')!");
102+
throw new \RuntimeException('No BBB-Secret (or BBB-Salt) found! Please provide it either in constructor '
103+
. "(2nd argument) or by environment variable 'BBB_SECRET' (or 'BBB_SECURITY_SALT')!");
104104
}
105105

106106
// Keeping backward compatibility with older deployed versions
@@ -302,11 +302,9 @@ public function getRecordingsUrl(GetRecordingsParameters $recordingsParams): str
302302
}
303303

304304
/**
305-
* @param mixed $recordingParams
306-
*
307305
* @throws BadResponseException|\RuntimeException
308306
*/
309-
public function getRecordings($recordingParams): GetRecordingsResponse
307+
public function getRecordings(GetRecordingsParameters $recordingParams): GetRecordingsResponse
310308
{
311309
$xml = $this->processXmlResponse($this->getUrlBuilder()->getRecordingsUrl($recordingParams));
312310

@@ -414,11 +412,9 @@ public function getHooksCreateUrl(HooksCreateParameters $hookCreateParams): stri
414412
}
415413

416414
/**
417-
* @param mixed $hookCreateParams
418-
*
419415
* @throws BadResponseException
420416
*/
421-
public function hooksCreate($hookCreateParams): HooksCreateResponse
417+
public function hooksCreate(HooksCreateParameters $hookCreateParams): HooksCreateResponse
422418
{
423419
$xml = $this->processXmlResponse($this->getUrlBuilder()->getHooksCreateUrl($hookCreateParams));
424420

@@ -452,11 +448,9 @@ public function getHooksDestroyUrl(HooksDestroyParameters $hooksDestroyParams):
452448
}
453449

454450
/**
455-
* @param mixed $hooksDestroyParams
456-
*
457451
* @throws BadResponseException
458452
*/
459-
public function hooksDestroy($hooksDestroyParams): HooksDestroyResponse
453+
public function hooksDestroy(HooksDestroyParameters $hooksDestroyParams): HooksDestroyResponse
460454
{
461455
$xml = $this->processXmlResponse($this->getUrlBuilder()->getHooksDestroyUrl($hooksDestroyParams));
462456

@@ -538,10 +532,9 @@ private function sendRequest(string $url, string $payload = '', string $contentT
538532
$ch = curl_init();
539533
$cookieFile = tmpfile();
540534

541-
if (!$ch) { // @phpstan-ignore-line
542-
throw new \RuntimeException('Unhandled curl error: ' . curl_error($ch));
535+
if (false === $ch) {
536+
throw new \RuntimeException('Failed to initialize cURL');
543537
}
544-
545538
// JSESSIONID
546539
if ($cookieFile) {
547540
$cookieFilePath = stream_get_meta_data($cookieFile)['uri'];
@@ -550,26 +543,34 @@ private function sendRequest(string $url, string $payload = '', string $contentT
550543
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookieFilePath);
551544
curl_setopt($ch, CURLOPT_COOKIEJAR, $cookieFilePath);
552545

553-
if ($cookies) {
554-
if (false !== mb_strpos($cookies, 'JSESSIONID')) {
555-
preg_match('/(?:JSESSIONID\s*)(?<JSESSIONID>.*)/', $cookies, $output_array);
556-
$this->setJSessionId($output_array['JSESSIONID']);
546+
if ($cookies && false !== mb_strpos($cookies, 'JSESSIONID')) {
547+
if (preg_match('/JSESSIONID\s*(?<JSESSIONID>[^;\s]*)/', $cookies, $output_array)) {
548+
// No need for isset() - we know JSESSIONID exists if preg_match returns true
549+
$this->setJSessionId(mb_trim($output_array['JSESSIONID']));
550+
} else {
551+
throw new \RuntimeException('JSESSIONID found but could not be extracted');
557552
}
558553
}
559554
}
560555

556+
// Initialise headers array with mandatory Content-type
557+
$headers = [
558+
'Content-type: ' . $contentType,
559+
];
560+
561561
// PAYLOAD
562562
if (!empty($payload)) {
563563
curl_setopt($ch, CURLOPT_HEADER, 0);
564564
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
565565
curl_setopt($ch, CURLOPT_POST, 1);
566566
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
567-
curl_setopt($ch, CURLOPT_HTTPHEADER, [
568-
'Content-type: ' . $contentType,
569-
'Content-length: ' . strlen($payload),
570-
]);
567+
// Add Content-length header if payload is present
568+
$headers[] = 'Content-length: ' . strlen($payload);
571569
}
572570

571+
// Set HTTP headers
572+
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
573+
573574
// OTHERS
574575
foreach ($this->curlOpts as $opt => $value) {
575576
curl_setopt($ch, $opt, $value);

src/Core/Track.php

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,7 @@ class Track
3535

3636
private string $source;
3737

38-
/**
39-
* @param mixed $track
40-
*/
41-
public function __construct($track)
38+
public function __construct(mixed $track)
4239
{
4340
$this->href = $track->href;
4441
$this->kind = $track->kind;

src/Parameters/BaseParameters.php

Lines changed: 114 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,128 @@
2020

2121
namespace BigBlueButton\Parameters;
2222

23+
use BigBlueButton\Attribute\ApiParameterMapper;
24+
2325
/**
2426
* Class BaseParameters.
2527
*/
2628
abstract class BaseParameters
2729
{
28-
abstract public function getHTTPQuery(): string;
30+
public function getHTTPQuery(): string
31+
{
32+
$apiData = $this->toApiDataArray();
33+
34+
// No need for null checks anymore since toApiDataArray() filters them out
35+
foreach ($apiData as $value) {
36+
if (!is_string($value)) {
37+
throw new \RuntimeException(sprintf(
38+
'Invalid API parameter type: %s',
39+
gettype($value)
40+
));
41+
}
42+
}
43+
44+
return $this->buildHTTPQuery($apiData);
45+
}
46+
47+
/**
48+
* @return array<string, null|string> // Keys are strings, values are strings or null
49+
*/
50+
public function toApiDataArray(): array
51+
{
52+
$result = [];
53+
$classReflection = new \ReflectionClass($this);
54+
55+
foreach ($classReflection->getMethods() as $method) {
56+
foreach ($method->getAttributes(ApiParameterMapper::class) as $attribute) {
57+
/** @var ApiParameterMapper $attributeObject */
58+
$attributeObject = $attribute->newInstance();
59+
$key = $attributeObject->getAttributeName();
60+
$value = $this->strictConvertToApiValue($this->{$method->getName()}());
61+
62+
// Only include non-null values
63+
if (null !== $value) {
64+
$result[$key] = $value;
65+
}
66+
}
67+
}
68+
69+
return $result;
70+
}
71+
72+
/**
73+
* @param array<string, null|string> $array // Keys and values are both strings
74+
*/
75+
protected function buildHTTPQuery(array $array): string
76+
{
77+
return str_replace(
78+
['%20', '!', "'", '(', ')', '*'],
79+
['+', '%21', '%27', '%28', '%29', '%2A'],
80+
http_build_query($array, '', '&', \PHP_QUERY_RFC3986)
81+
);
82+
}
2983

3084
/**
31-
* @param mixed $array
85+
* Converts any value to API string format with strict type enforcement.
86+
*
87+
* @param mixed $value
3288
*/
33-
protected function buildHTTPQuery($array): string
89+
private function strictConvertToApiValue($value): ?string
3490
{
35-
return str_replace(['%20', '!', "'", '(', ')', '*'], ['+', '%21', '%27', '%28', '%29', '%2A'], http_build_query(array_filter($array), '', '&', \PHP_QUERY_RFC3986));
91+
if (null === $value) {
92+
return null;
93+
}
94+
95+
// Handle BackedEnum cases
96+
if ($value instanceof \BackedEnum) {
97+
$enumValue = $value->value;
98+
if (!is_scalar($enumValue)) {
99+
throw new \RuntimeException(sprintf(
100+
'Enum value for %s must be scalar, got %s',
101+
get_class($value),
102+
gettype($enumValue)
103+
));
104+
}
105+
106+
return (string) $enumValue;
107+
}
108+
109+
// Handle arrays
110+
if (is_array($value)) {
111+
return $this->convertArrayToApiString($value);
112+
}
113+
114+
// Handle all other cases with strict string conversion
115+
if (is_bool($value)) {
116+
return $value ? 'true' : 'false';
117+
}
118+
119+
if (is_object($value)) {
120+
throw new \RuntimeException(sprintf(
121+
'Cannot convert object of type %s to API value',
122+
get_class($value)
123+
));
124+
}
125+
126+
// Force string conversion for all scalar values
127+
return (string) $value;
128+
}
129+
130+
/**
131+
* Converts array values to comma-separated string with strict typing.
132+
*
133+
* @param array<mixed> $values // Array of mixed values that will be converted
134+
*/
135+
private function convertArrayToApiString(array $values): string
136+
{
137+
$converted = [];
138+
foreach ($values as $item) {
139+
$convertedItem = $this->strictConvertToApiValue($item);
140+
if (null !== $convertedItem) {
141+
$converted[] = $convertedItem;
142+
}
143+
}
144+
145+
return implode(',', $converted);
36146
}
37147
}

0 commit comments

Comments
 (0)