Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -0,0 +1,92 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2018 Artur Neumann artur@jankaritech.com
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers\Asserts;
use PHPUnit\Framework\Assert;
use SimpleXMLElement;
/**
* WebDAV related asserts
*/
class WebDav extends Assert {
/**
*
* @param string|null $element exception|message|reason
* @param string|null $expectedValue
* @param array|null $responseXmlArray
* @param string|null $extraErrorText
*
* @return void
*/
public static function assertDavResponseElementIs(
?string $element,
?string $expectedValue,
?array $responseXmlArray,
?string $extraErrorText = ''
): void {
if ($extraErrorText !== '') {
$extraErrorText = $extraErrorText . " ";
}
self::assertArrayHasKey(
'value',
$responseXmlArray,
$extraErrorText . "responseXml does not have key 'value'"
);
if ($element === "exception") {
$result = $responseXmlArray['value'][0]['value'];
} elseif ($element === "message") {
$result = $responseXmlArray['value'][1]['value'];
} elseif ($element === "reason") {
$result = $responseXmlArray['value'][3]['value'];
} else {
self::fail(
__METHOD__ . " element must be one of exception, response or reason. But '$element' was passed in."
);
}
self::assertEquals(
$expectedValue,
$result,
__METHOD__ . " " . $extraErrorText . "Expected '$expectedValue' in element $element got '$result'"
);
}
/**
*
* @param SimpleXMLElement $responseXmlObject
* @param array|null $expectedShareTypes
*
* @return void
*/
public static function assertResponseContainsShareTypes(
SimpleXMLElement $responseXmlObject,
?array $expectedShareTypes
): void {
foreach ($expectedShareTypes as $row) {
$xmlPart = $responseXmlObject->xpath(
"//d:prop/oc:share-types/oc:share-type[.=" . $row[0] . "]"
);
self::assertNotEmpty(
$xmlPart,
"cannot find share-type '" . $row[0] . "'"
);
}
}
}
@@ -0,0 +1,106 @@
<?php declare(strict_types=1);
/**
* @author Niraj Acharya <niraj@jankaritech.com>
* @copyright Copyright (c) 2024 Niraj Acharya niraj@jankaritech.com
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
use Psr\Http\Message\ResponseInterface;
/**
* A helper class for managing Auth App API requests
*/
class AuthAppHelper {
/**
* @return string
*/
public static function getAuthAppEndpoint(): string {
return "/auth-app/tokens";
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
*
* @return ResponseInterface
*/
public static function listAllAppAuthTokensForUser(
string $baseUrl,
string $user,
string $password
): ResponseInterface {
$url = $baseUrl . self::getAuthAppEndpoint();
return HttpRequestHelper::sendRequest(
$url,
null,
"GET",
$user,
$password,
);
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param array $params
*
* @return ResponseInterface
*/
public static function createAppAuthToken(
string $baseUrl,
string $user,
string $password,
array $params,
): ResponseInterface {
$url = $baseUrl . self::getAuthAppEndpoint() . "?"
. http_build_query($params);
return HttpRequestHelper::sendRequest(
$url,
null,
"POST",
$user,
$password,
);
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $token
*
* @return ResponseInterface
*/
public static function deleteAppAuthToken(
string $baseUrl,
string $user,
string $password,
string $token
): ResponseInterface {
$url = $baseUrl . self::getAuthAppEndpoint() . "?token=$token";
return HttpRequestHelper::sendRequest(
$url,
null,
"DELETE",
$user,
$password,
);
}
}
@@ -0,0 +1,55 @@
<?php declare(strict_types=1);
/**
* @author Sajan Gurung <sajan@jankaritech.com>
* @copyright Copyright (c) 2017 Artur Neumann artur@jankaritech.com
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
use Behat\Behat\Context\Context;
use Behat\Behat\Context\Environment\InitializedContextEnvironment;
use Behat\Behat\Context\Exception\ContextNotFoundException;
use Behat\Behat\Hook\Scope\ScenarioScope;
/**
* Helper for Behat environment configuration
*
*/
class BehatHelper {
/**
* @param ScenarioScope $scope
* @param InitializedContextEnvironment $environment
* @param string $class
*
* @return Context
*/
public static function getContext(
ScenarioScope $scope,
InitializedContextEnvironment $environment,
string $class
): Context {
try {
return $environment->getContext($class);
} catch (ContextNotFoundException $e) {
$context = new $class();
$environment->registerContext($context);
if (\method_exists($context, 'before')) {
$context->before($scope);
}
return $environment->getContext($class);
}
}
}
@@ -0,0 +1,41 @@
<?php declare(strict_types=1);
/**
* @author Sajan Gurung <sajan@jankaritech.com>
* @copyright Copyright (c) 2024 Sajan Gurung sajan@jankaritech.com
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\OcConfigHelper;
/**
* A helper class for running КуСфера CLI commands
*/
class CliHelper {
/**
* @param array $body
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function runCommand(array $body): ResponseInterface {
$url = OcConfigHelper::getWrapperUrl() . "/command";
return OcConfigHelper::sendRequest($url, "POST", \json_encode($body));
}
}
@@ -0,0 +1,95 @@
<?php declare(strict_types=1);
/**
* @author Amrita Shrestha <amrita@jankaritech.com>
* @copyright Copyright (c) 2024 Amrita Shrestha amrita@jankaritech.com
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\ResponseInterface;
/**
* A helper class for managing wopi requests
*/
class CollaborationHelper {
/**
* @param string $fileId
* @param string $app
* @param string $username
* @param string $password
* @param string $baseUrl
* @param string $xRequestId
* @param string|null $viewMode
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function sendPOSTRequestToAppOpen(
string $fileId,
string $app,
string $username,
string $password,
string $baseUrl,
string $xRequestId,
?string $viewMode = null,
): ResponseInterface {
$url = $baseUrl . "/app/open?app_name=$app&file_id=$fileId";
if ($viewMode) {
$url .= "&view_mode=$viewMode";
}
return HttpRequestHelper::post(
$url,
$xRequestId,
$username,
$password,
['Content-Type' => 'application/json']
);
}
/**
* @param string $baseUrl
* @param string $xRequestId
* @param string $user
* @param string $password
* @param string $parentContainerId
* @param string $file
* @param array|null $headers
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function createFile(
string $baseUrl,
string $xRequestId,
string $user,
string $password,
string $parentContainerId,
string $file,
?array $headers = null
): ResponseInterface {
$url = $baseUrl . "/app/new?parent_container_id=$parentContainerId&filename=$file";
return HttpRequestHelper::post(
$url,
$xRequestId,
$user,
$password,
$headers
);
}
}
@@ -0,0 +1,178 @@
<?php declare(strict_types=1);
/**
* @author Prajwol Amatya <prajwol@jankaritech.com>
* @copyright Copyright (c) 2023 Prajwol Amatya prajwol@jankaritech.com
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\ResponseInterface;
/**
* A helper class for managing emails
*/
class EmailHelper {
/**
* @param string $emailAddress
*
* @return string
*/
public static function getMailBoxFromEmail(string $emailAddress): string {
return explode("@", $emailAddress)[0];
}
/**
* Returns the host and port where Email messages can be read and deleted
* by the test runner.
*
* @return string
*/
public static function getLocalEmailUrl(): string {
$localEmailHost = self::getLocalEmailHost();
$emailPort = \getenv('EMAIL_PORT');
if ($emailPort === false) {
$emailPort = "9000";
}
return "http://$localEmailHost:$emailPort";
}
/**
* Returns the host name or address of the Email server as seen from the
* point of view of the system-under-test.
*
* @return string
*/
public static function getEmailHost(): string {
$emailHost = \getenv('EMAIL_HOST');
if ($emailHost === false) {
$emailHost = "127.0.0.1";
}
return $emailHost;
}
/**
* Returns the host name or address of the Email server as seen from the
* point of view of the test runner.
*
* @return string
*/
public static function getLocalEmailHost(): string {
$localEmailHost = \getenv('LOCAL_EMAIL_HOST');
if ($localEmailHost === false) {
$localEmailHost = self::getEmailHost();
}
return $localEmailHost;
}
/**
* Returns general response information about the provided mailbox
* A mailbox is created automatically in InBucket for every unique email sender|receiver
*
* @param string $mailBox
* @param string|null $xRequestId
*
* @return array
* @throws GuzzleException
*/
public static function getMailBoxInformation(string $mailBox, ?string $xRequestId = null): array {
$response = HttpRequestHelper::get(
self::getLocalEmailUrl() . "/api/v1/mailbox/" . $mailBox,
$xRequestId,
null,
null,
['Content-Type' => 'application/json']
);
return \json_decode($response->getBody()->getContents());
}
/**
* returns body content of a specific email (mailBox) with email ID (mailbox Id)
*
* @param string $mailBox
* @param string $mailboxId
* @param string|null $xRequestId
*
* @return object
* @throws GuzzleException
*/
public static function getBodyOfAnEmailById(
string $mailBox,
string $mailboxId,
?string $xRequestId = null
): object {
$response = HttpRequestHelper::get(
self::getLocalEmailUrl() . "/api/v1/mailbox/" . $mailBox . "/" . $mailboxId,
$xRequestId,
null,
null,
['Content-Type' => 'application/json']
);
return \json_decode($response->getBody()->getContents());
}
/**
* Returns the body of the last received email for the provided receiver
*
* @param string $emailAddress
* @param string $xRequestId
*
* @return string
* @throws GuzzleException
* @throws Exception
*/
public static function getBodyOfLastEmail(
string $emailAddress,
string $xRequestId,
): string {
$mailBox = self::getMailBoxFromEmail($emailAddress);
$emails = self::getMailboxInformation($mailBox, $xRequestId);
if (!empty($emails)) {
$emailId = \array_pop($emails)->id;
$response = self::getBodyOfAnEmailById($mailBox, $emailId, $xRequestId);
$body = \str_replace(
"\r\n",
"\n",
\quoted_printable_decode($response->body->text . "\n" . $response->body->html)
);
return $body;
}
return "";
}
/**
* Deletes all the emails for the provided mailbox
*
* @param string $url
* @param string $mailBox
* @param string $xRequestId
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function deleteAllEmails(
string $url,
string $mailBox,
string $xRequestId,
): ResponseInterface {
return HttpRequestHelper::delete(
$url . "/api/v1/mailbox/" . $mailBox,
$xRequestId,
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,133 @@
<?php declare(strict_types=1);
/**
* @author Sajan Gurung <sajan@jankaritech.com>
* @copyright Copyright (c) 2023, ownCloud GmbH
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Helper for logging HTTP requests and responses
*/
class HttpLogger {
/**
* @return string
*/
public static function getLogDir(): string {
return __DIR__ . '/../logs';
}
/**
* @return string
*/
public static function getFailedLogPath(): string {
return self::getLogDir() . "/failed.log";
}
/**
* @return string
*/
public static function getScenarioLogPath(): string {
return self::getLogDir() . "/scenario.log";
}
/**
* @param string $logFile
* @param string $logMessage
*
* @return void
*/
public static function writeLog(string $logFile, string $logMessage): void {
$file = \fopen($logFile, 'a+') or die('Cannot open file: ' . $logFile);
\fwrite($file, $logMessage);
\fclose($file);
}
/**
* @param RequestInterface $request
*
* @return void
*/
public static function logRequest(RequestInterface $request): void {
$method = $request->getMethod();
$path = $request->getUri()->getPath();
$query = $request->getUri()->getQuery();
$body = $request->getBody();
$headers = "";
foreach ($request->getHeaders() as $key => $value) {
$headers = $key . ": " . $value[0] . "\n";
}
$logMessage = "\t\t_______________________________________________________________________\n\n";
$logMessage .= "\t\t==> REQUEST [" . self::getCurrentDateTime() . "]\n";
$logMessage .= "\t\t$method $path\n";
$logMessage .= $query ? "\t\tQUERY: $query\n" : "";
$logMessage .= "\t\t$headers";
if ($body->getSize() > 0) {
$logMessage .= "\t\t==> REQ BODY\n";
$logMessage .= "\t\t$body\n";
}
$logMessage .= "\n";
self::writeLog(self::getScenarioLogPath(), $logMessage);
}
/**
* @param ResponseInterface $response
*
* @return void
*/
public static function logResponse(ResponseInterface $response): void {
$statusCode = $response->getStatusCode();
$statusMessage = $response->getReasonPhrase();
$body = $response->getBody();
$headers = "";
foreach ($response->getHeaders() as $key => $value) {
$headers = $key . ": " . $value[0] . "\n";
}
$logMessage = "\t\t<== RESPONSE [" . self::getCurrentDateTime() . "]\n";
$logMessage .= "\t\t$statusCode $statusMessage\n";
$logMessage .= "\t\t$headers";
if ($body->getSize() > 0) {
$logMessage .= "\t\t<== RES BODY\n";
foreach (\explode("\n", \strval($body)) as $line) {
$logMessage .= "\t\t$line\n";
}
}
// rewind the body stream so that later code can read from the start.
$response->getBody()->rewind();
$logMessage = \rtrim($logMessage) . "\n\n";
self::writeLog(self::getScenarioLogPath(), $logMessage);
}
/**
* Returns current date and time in format: 1999-01-31T23:01:59
*
* @return string
*/
public static function getCurrentDateTime(): string {
return date('Y-m-d\TG:i:s');
}
}
@@ -0,0 +1,766 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2017 Artur Neumann artur@jankaritech.com
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Psr7\Request;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use SimpleXMLElement;
use Sabre\Xml\LibXMLException;
use Sabre\Xml\Reader;
use GuzzleHttp\Pool;
use Symfony\Component\HttpFoundation\Response;
/**
* Helper for HTTP requests
*/
class HttpRequestHelper {
public const HTTP_TOO_EARLY = 425;
public const HTTP_CONFLICT = 409;
/**
* Some systems-under-test do async post-processing of operations like upload,
* move, etc. If a client does a request on the resource before the post-processing
* is finished, then the server should return HTTP_TOO_EARLY "425". Clients are
* expected to retry the request "some time later" (tm).
*
* On such systems, when HTTP_TOO_EARLY status is received, the test code will
* retry the request at 1-second intervals until either some other HTTP status
* is received or the retry-limit is reached.
*
* @return int
*/
public static function numRetriesOnHttpTooEarly(): int {
// Currently reva and КуСфера may return HTTP_TOO_EARLY
// So try up to 10 times before giving up.
return STANDARD_RETRY_COUNT;
}
/**
*
* @param string $url
* @param string|null $xRequestId
* @param string|null $method
* @param string|null $user
* @param string|null $password
* @param array|null $headers ['X-MyHeader' => 'value']
* @param mixed $body
* @param array|null $config
* @param CookieJar|null $cookies
* @param bool $stream Set to true to stream a response rather
* than download it all up-front.
* @param int|null $timeout
* @param Client|null $client
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function sendRequestOnce(
string $url,
?string $xRequestId = null,
?string $method = 'GET',
?string $user = null,
?string $password = null,
?array $headers = null,
$body = null,
?array $config = null,
?CookieJar $cookies = null,
bool $stream = false,
?int $timeout = 0,
?Client $client = null,
): ResponseInterface {
$bearerToken = null;
if (TokenHelper::useBearerToken() && $user && $user !== 'public') {
$bearerToken = TokenHelper::getTokens($user, $password, $url)['access_token'];
// check token is still valid
$parsedUrl = parse_url($url);
$baseUrl = $parsedUrl['scheme'] . '://' . $parsedUrl['host'];
$baseUrl .= isset($parsedUrl['port']) ? ':' . $parsedUrl['port'] : '';
$testUrl = $baseUrl . "/graph/v1.0/me";
if (OcHelper::isTestingOnReva()) {
$url = $baseUrl . "/ocs/v2.php/cloud/users/$user";
}
// check token validity with a GET request
$c = self::createClient(
$user,
$password,
$config,
$cookies,
$stream,
$timeout,
$bearerToken
);
$testReq = self::createRequest($testUrl, $xRequestId, 'GET');
try {
$testRes = $c->send($testReq);
} catch (RequestException $ex) {
$testRes = $ex->getResponse();
if ($testRes && $testRes->getStatusCode() === Response::HTTP_UNAUTHORIZED) {
// token is invalid or expired, get a new one
echo "[INFO] Bearer token expired or invalid, getting a new one...\n";
TokenHelper::clearAllTokens();
$bearerToken = TokenHelper::getTokens($user, $password, $url)['access_token'];
}
}
}
if ($client === null) {
$client = self::createClient(
$user,
$password,
$config,
$cookies,
$stream,
$timeout,
$bearerToken
);
}
if (WebdavHelper::isDAVRequest($url) && \str_starts_with($url, OcHelper::getServerUrl())) {
$urlHasRemotePhp = \str_contains($url, 'remote.php');
if (!WebDavHelper::withRemotePhp() && $urlHasRemotePhp) {
throw new Exception("remote.php is disabled but found in the URL: $url");
}
if (WebDavHelper::withRemotePhp() && !$urlHasRemotePhp) {
throw new Exception("remote.php is enabled but not found in the URL: $url");
}
if ($headers && \array_key_exists("Destination", $headers)) {
if (!WebDavHelper::withRemotePhp() && $urlHasRemotePhp) {
throw new Exception("remote.php is disabled but found in the URL: $url");
}
if (WebDavHelper::withRemotePhp() && !$urlHasRemotePhp) {
throw new Exception("remote.php is enabled but not found in the URL: $url");
}
}
}
$request = self::createRequest(
$url,
$xRequestId,
$method,
$headers,
$body
);
if ((\getenv('DEBUG_ACCEPTANCE_REQUESTS') !== false) || (\getenv('DEBUG_ACCEPTANCE_API_CALLS') !== false)) {
$debugRequests = true;
} else {
$debugRequests = false;
}
if ($debugRequests) {
self::debugRequest($request, $user, $password);
}
// The exceptions that might happen here include:
// ConnectException - in that case there is no response. Don't catch the exception.
// RequestException - if there is something in the response then pass it back.
// Otherwise, re-throw the exception.
// GuzzleException - something else unexpected happened. Don't catch the exception.
try {
$response = $client->send($request);
} catch (RequestException $ex) {
$response = $ex->getResponse();
//if the response was null for some reason do not return it but re-throw
if ($response === null) {
throw $ex;
}
}
HttpLogger::logResponse($response);
// wait for post-processing to finish if applicable
if (WebdavHelper::isDAVRequest($url)
&& \str_starts_with($url, OcHelper::getServerUrl())
&& \in_array($method, ["PUT", "MOVE", "COPY", "MKCOL"])
&& \in_array($response->getStatusCode(), [Response::HTTP_CREATED, Response::HTTP_NO_CONTENT])
&& OcConfigHelper::getPostProcessingDelay() === 0
) {
if (\in_array($method, ["MOVE", "COPY"])) {
$url = $headers['Destination'];
}
WebDavHelper::waitForPostProcessingToFinish(
$url,
$user,
$password,
$headers,
);
}
return $response;
}
/**
* @param string|null $url
* @param string|null $xRequestId
* @param string|null $method
* @param string|null $user
* @param string|null $password
* @param array|null $headers ['X-MyHeader' => 'value']
* @param mixed $body
* @param array|null $config
* @param CookieJar|null $cookies
* @param bool $stream Set to true to stream a response rather
* than download it all up-front.
* @param int|null $timeout
* @param Client|null $client
* @param bool|null $isGivenStep
*
* @return ResponseInterface
*
* @throws GuzzleException
*/
public static function sendRequest(
?string $url,
?string $xRequestId,
?string $method = 'GET',
?string $user = null,
?string $password = null,
?array $headers = null,
$body = null,
?array $config = null,
?CookieJar $cookies = null,
bool $stream = false,
?int $timeout = 0,
?Client $client = null,
?bool $isGivenStep = false
): ResponseInterface {
if ((\getenv('DEBUG_ACCEPTANCE_RESPONSES') !== false) || (\getenv('DEBUG_ACCEPTANCE_API_CALLS') !== false)) {
$debugResponses = true;
} else {
$debugResponses = false;
}
$sendRetryLimit = self::numRetriesOnHttpTooEarly();
$sendCount = 0;
$sendExceptionHappened = false;
do {
$response = self::sendRequestOnce(
$url,
$xRequestId,
$method,
$user,
$password,
$headers,
$body,
$config,
$cookies,
$stream,
$timeout,
$client,
);
if ($response->getStatusCode() >= 400
&& $response->getStatusCode() !== self::HTTP_TOO_EARLY
&& $response->getStatusCode() !== self::HTTP_CONFLICT
) {
$sendExceptionHappened = true;
}
if ($debugResponses) {
self::debugResponse($response);
}
$sendCount = $sendCount + 1;
// Here we check if the response has status code 425 or is a 409 gotten from a Given step
// HTTP_TOO_EARLY (425) can happen if async processing of a previous request is still happening.
// For example, if a test uploads a file and then immediately tries to download it.
// HTTP_CONFLICT (409) can happen if the user has just been created in the previous step.
// The OCS API might not "realize" yet that the user exists. A folder creation (MKCOL) or maybe even
// a file upload might return 409.
// In all these cases we can try the API request again after a short time.
$loopAgain = !$sendExceptionHappened && ($response->getStatusCode() === self::HTTP_TOO_EARLY ||
($response->getStatusCode() === self::HTTP_CONFLICT && $isGivenStep)) &&
$sendCount <= $sendRetryLimit;
if ($loopAgain) {
// we need to repeat the send request, because we got HTTP_TOO_EARLY or HTTP_CONFLICT
// wait 1 second before sending again, to give the server some time
// to finish whatever post-processing it might be doing.
echo "[INFO] Received '" . $response->getStatusCode() .
"' status code, retrying request ($sendCount)...\n";
\sleep(1);
}
} while ($loopAgain);
return $response;
}
/**
* Print details about the request.
*
* @param RequestInterface|null $request
* @param string|null $user
* @param string|null $password
*
* @return void
*/
private static function debugRequest(?RequestInterface $request, ?string $user, ?string $password): void {
print("### AUTH: $user:$password\n");
print("### REQUEST: " . $request->getMethod() . " " . $request->getUri() . "\n");
self::printHeaders($request->getHeaders());
self::printBody($request->getBody());
print("\n### END REQUEST\n");
}
/**
* Print details about the response.
*
* @param ResponseInterface|null $response
*
* @return void
*/
private static function debugResponse(?ResponseInterface $response): void {
print("### RESPONSE\n");
print("Status: " . $response->getStatusCode() . "\n");
self::printHeaders($response->getHeaders());
self::printBody($response->getBody());
print("\n### END RESPONSE\n");
}
/**
* Print details about the headers.
*
* @param array|null $headers
*
* @return void
*/
private static function printHeaders(?array $headers): void {
if ($headers) {
print("Headers:\n");
foreach ($headers as $header => $value) {
if (\is_array($value)) {
print($header . ": " . \implode(', ', $value) . "\n");
} else {
print($header . ": " . $value . "\n");
}
}
} else {
print("Headers: none\n");
}
}
/**
* Print details about the body.
*
* @param StreamInterface|null $body
*
* @return void
*/
private static function printBody(?StreamInterface $body): void {
print("Body:\n");
\var_dump($body->getContents());
// Rewind the stream so that later code can read from the start.
$body->rewind();
}
/**
* Send the requests to the server in parallel.
* This function takes an array of requests and an optional client.
* It will send all the requests to the server using the Pool object in guzzle.
*
* @param array|null $requests
* @param Client|null $client
*
* @return array
*/
public static function sendBatchRequest(
?array $requests,
?Client $client
): array {
return Pool::batch($client, $requests);
}
/**
* Create a Guzzle Client
* This creates a client object that can be used later to send a request object(s)
*
* @param string|null $user
* @param string|null $password
* @param array|null $config
* @param CookieJar|null $cookies
* @param bool $stream Set to true to stream a response rather
* than download it all up-front.
* @param int|null $timeout
* @param string|null $bearerToken
*
* @return Client
*/
public static function createClient(
?string $user = null,
?string $password = null,
?array $config = null,
?CookieJar $cookies = null,
?bool $stream = false,
?int $timeout = 0,
?string $bearerToken = null
): Client {
$options = [];
if ($bearerToken !== null) {
$options['headers']['Authorization'] = 'Bearer ' . $bearerToken;
} elseif ($user !== null) {
$options['auth'] = [$user, $password];
}
if ($config !== null) {
$options['config'] = $config;
}
if ($cookies !== null) {
$options['cookies'] = $cookies;
}
$options['stream'] = $stream;
$options['verify'] = false;
$options['timeout'] = $timeout ?: self::getRequestTimeout();
return new Client($options);
}
/**
* Create an HTTP request based on given parameters.
* This creates a RequestInterface object that can be used with a client to send a request.
* This enables us to create multiple requests in advance so that we can send them to the server at once in parallel.
*
* @param string|null $url
* @param string|null $xRequestId
* @param string|null $method
* @param array|null $headers ['X-MyHeader' => 'value']
* @param string|array $body either the actual string to send in the body,
* or an array of key-value pairs to be converted
* into a body with http_build_query.
*
* @return RequestInterface
*/
public static function createRequest(
?string $url,
?string $xRequestId = '',
?string $method = 'GET',
?array $headers = null,
$body = null
): RequestInterface {
if ($headers === null) {
$headers = [];
}
if ($xRequestId !== '') {
$headers['X-Request-ID'] = $xRequestId;
}
if (\is_array($body)) {
// When creating the client, it is possible to set 'form_params' and
// the Client constructor sorts out doing this http_build_query stuff.
// But 'new Request' does not have the flexibility to do that.
// So we need to do it here.
$body = \http_build_query($body, '', '&');
$headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
$request = new Request(
$method,
$url,
$headers,
$body
);
HttpLogger::logRequest($request);
return $request;
}
/**
* same as HttpRequestHelper::sendRequest() but with "GET" as method
*
* @param string|null $url
* @param string|null $xRequestId
* @param string|null $user
* @param string|null $password
* @param array|null $headers ['X-MyHeader' => 'value']
* @param mixed $body
* @param array|null $config
* @param CookieJar|null $cookies
* @param boolean $stream
*
* @return ResponseInterface
* @throws GuzzleException
* @see HttpRequestHelper::sendRequest()
*/
public static function get(
?string $url,
?string $xRequestId,
?string $user = null,
?string $password = null,
?array $headers = null,
$body = null,
?array $config = null,
?CookieJar $cookies = null,
?bool $stream = false
): ResponseInterface {
return self::sendRequest(
$url,
$xRequestId,
'GET',
$user,
$password,
$headers,
$body,
$config,
$cookies,
$stream
);
}
/**
* same as HttpRequestHelper::sendRequest() but with "POST" as method
*
* @param string|null $url
* @param string|null $xRequestId
* @param string|null $user
* @param string|null $password
* @param array|null $headers ['X-MyHeader' => 'value']
* @param mixed $body
* @param array|null $config
* @param CookieJar|null $cookies
* @param boolean $stream
*
* @return ResponseInterface
* @throws GuzzleException
* @see HttpRequestHelper::sendRequest()
*/
public static function post(
?string $url,
?string $xRequestId,
?string $user = null,
?string $password = null,
?array $headers = null,
$body = null,
?array $config = null,
?CookieJar $cookies = null,
?bool $stream = false
): ResponseInterface {
return self::sendRequest(
$url,
$xRequestId,
'POST',
$user,
$password,
$headers,
$body,
$config,
$cookies,
$stream
);
}
/**
* same as HttpRequestHelper::sendRequest() but with "PUT" as method
*
* @param string|null $url
* @param string|null $xRequestId
* @param string|null $user
* @param string|null $password
* @param array|null $headers ['X-MyHeader' => 'value']
* @param mixed $body
* @param array|null $config
* @param CookieJar|null $cookies
* @param boolean $stream
*
* @return ResponseInterface
* @throws GuzzleException
* @see HttpRequestHelper::sendRequest()
*/
public static function put(
?string $url,
?string $xRequestId,
?string $user = null,
?string $password = null,
?array $headers = null,
$body = null,
?array $config = null,
?CookieJar $cookies = null,
?bool $stream = false
): ResponseInterface {
return self::sendRequest(
$url,
$xRequestId,
'PUT',
$user,
$password,
$headers,
$body,
$config,
$cookies,
$stream
);
}
/**
* same as HttpRequestHelper::sendRequest() but with "DELETE" as method
*
* @param string|null $url
* @param string|null $xRequestId
* @param string|null $user
* @param string|null $password
* @param array|null $headers ['X-MyHeader' => 'value']
* @param mixed $body
* @param array|null $config
* @param CookieJar|null $cookies
* @param boolean $stream
*
* @return ResponseInterface
* @throws GuzzleException
* @see HttpRequestHelper::sendRequest()
*
*/
public static function delete(
?string $url,
?string $xRequestId,
?string $user = null,
?string $password = null,
?array $headers = null,
$body = null,
?array $config = null,
?CookieJar $cookies = null,
?bool $stream = false
): ResponseInterface {
return self::sendRequest(
$url,
$xRequestId,
'DELETE',
$user,
$password,
$headers,
$body,
$config,
$cookies,
$stream
);
}
/**
* Parses the response as XML and returns a SimpleXMLElement with these
* registered namespaces:
* | prefix | namespace |
* | d | DAV: |
* | oc | http://owncloud.org/ns |
* | ocs | http://open-collaboration-services.org/ns |
*
* @param ResponseInterface $response
* @param string|null $exceptionText text to put at the front of exception messages
*
* @return SimpleXMLElement
* @throws Exception
*/
public static function getResponseXml(ResponseInterface $response, ?string $exceptionText = ''): SimpleXMLElement {
// rewind just to make sure we can reparse it in case it was parsed already...
$response->getBody()->rewind();
$contents = $response->getBody()->getContents();
try {
$responseXmlObject = new SimpleXMLElement($contents);
$responseXmlObject->registerXPathNamespace(
'ocs',
'http://open-collaboration-services.org/ns'
);
$responseXmlObject->registerXPathNamespace(
'oc',
'http://owncloud.org/ns'
);
$responseXmlObject->registerXPathNamespace(
'd',
'DAV:'
);
return $responseXmlObject;
} catch (Exception $e) {
if ($exceptionText !== '') {
$exceptionText = $exceptionText . ' ';
}
if ($contents === '') {
throw new Exception($exceptionText . "Received empty response where XML was expected");
}
$message = $exceptionText . "Exception parsing response body: \"" . $contents . "\"";
throw new Exception($message, 0, $e);
}
}
/**
* parses the body content of $response and returns an array representing the XML
* This function returns an array with the following three elements:
* * name - The root element name.
* * value - The value for the root element.
* * attributes - An array of attributes.
*
* @param ResponseInterface $response
*
* @return array
*/
public static function parseResponseAsXml(ResponseInterface $response): array {
// rewind so that we can reparse it if it was parsed already
$response->getBody()->rewind();
$body = $response->getBody()->getContents();
$parsedResponse = [];
if ($body && \substr($body, 0, 1) === '<') {
try {
$reader = new Reader();
$reader->xml($body);
$parsedResponse = $reader->parse();
} catch (LibXMLException $e) {
// Sometimes the body can be a real page of HTML and text.
// So it may not be a complete ordinary piece of XML.
// The XML parse might fail with an exception message like:
// Opening and ending tag mismatch: link line 31 and head.
}
}
return $parsedResponse;
}
/**
* @return int
*/
public static function getRequestTimeout(): int {
$timeout = \getenv("REQUEST_TIMEOUT");
return (int)$timeout ?: HTTP_REQUEST_TIMEOUT;
}
/**
* returns json decoded body content of a json response as an object
*
* @param ResponseInterface $response
*
* @return mixed
*/
public static function getJsonDecodedResponseBodyContent(ResponseInterface $response): mixed {
return json_decode($response->getBody()->getContents(), null, 512, JSON_THROW_ON_ERROR);
}
/**
* @return bool
*/
public static function sendScenarioLineReferencesInXRequestId(): bool {
return (\getenv("SEND_SCENARIO_LINE_REFERENCES") === "true");
}
/**
* @return string
*/
public static function getXRequestIdRegex(): string {
if (self::sendScenarioLineReferencesInXRequestId()) {
return '/^[a-zA-Z]+\/[a-zA-Z]+\.feature:\d+(-\d+)?$/';
}
$host = gethostname();
return "/^$host\/.*$/";
}
}
@@ -0,0 +1,141 @@
<?php declare(strict_types=1);
/**
* @author Sajan Gurung <sajan@jankaritech.com>
* @copyright Copyright (c) 2023 Sajan Gurung sajan@jankaritech.com
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
use GuzzleHttp\Exception\ConnectException;
use TestHelpers\HttpRequestHelper;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Request;
use Psr\Http\Message\ResponseInterface;
/**
* A helper class for configuring КуСфера server
*/
class OcConfigHelper {
public static $postProcessingDelay = 0;
/**
* @return int
*/
public static function getPostProcessingDelay(): int {
return self::$postProcessingDelay;
}
/**
* @param string $postProcessingDelay
*
* @return void
*/
public static function setPostProcessingDelay(string $postProcessingDelay): void {
// extract number from string
$delay = (int) filter_var($postProcessingDelay, FILTER_SANITIZE_NUMBER_INT);
self::$postProcessingDelay = $delay;
}
/**
* @param string $url
* @param string $method
* @param ?string $body
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function sendRequest(
string $url,
string $method,
?string $body = ""
): ResponseInterface {
$client = HttpRequestHelper::createClient();
$request = new Request(
$method,
$url,
[],
$body
);
try {
$response = $client->send($request);
} catch (ConnectException $e) {
throw new \Error(
"Cannot connect to the ocwrapper at the moment,"
. "make sure that ocwrapper is running before proceeding with the test run.\n"
. $e->getMessage()
);
} catch (GuzzleException $ex) {
$response = $ex->getResponse();
if ($response === null) {
throw $ex;
}
}
return $response;
}
/**
* @return string
*/
public static function getWrapperUrl(): string {
$url = \getenv("OC_WRAPPER_URL");
if ($url === false) {
$url = "http://localhost:5200";
}
return $url;
}
/**
* @param array $envs
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function reConfigureOc(array $envs): ResponseInterface {
$url = self::getWrapperUrl() . "/config";
return self::sendRequest($url, "PUT", \json_encode($envs));
}
/**
* @return ResponseInterface
* @throws GuzzleException
*/
public static function rollbackOc(): ResponseInterface {
$url = self::getWrapperUrl() . "/rollback";
return self::sendRequest($url, "DELETE");
}
/**
* @return ResponseInterface
* @throws GuzzleException
*/
public static function stopQsfera(): ResponseInterface {
$url = self::getWrapperUrl() . "/stop";
return self::sendRequest($url, "POST");
}
/**
* @return ResponseInterface
* @throws GuzzleException
*/
public static function startQsfera(): ResponseInterface {
$url = self::getWrapperUrl() . "/start";
return self::sendRequest($url, "POST");
}
}
@@ -0,0 +1,323 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2020 Artur Neumann artur@jankaritech.com
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
/**
* Class StorageDriver
*
* @package TestHelpers
*/
abstract class StorageDriver {
public const DECOMPOSED = "DECOMPOSED";
public const EOS = "EOS";
public const OWNCLOUD = "OWNCLOUD";
public const DECOMPOSEDS3 = "DECOMPOSEDS3";
public const POSIX = "POSIX";
}
/**
* Class OcHelper
*
* Helper functions that are needed to run tests on КуСфера server
*
* @package TestHelpers
*/
class OcHelper {
public const STORAGE_DRIVERS = [
StorageDriver::DECOMPOSED,
StorageDriver::EOS,
StorageDriver::OWNCLOUD,
StorageDriver::DECOMPOSEDS3,
StorageDriver::POSIX
];
/**
* @return string
*/
public static function getServerUrl(): string {
if (\getenv('TEST_SERVER_URL')) {
return \getenv('TEST_SERVER_URL');
}
return 'https://localhost:9200';
}
/**
* @return string
*/
public static function getFederatedServerUrl(): string {
if (\getenv('TEST_SERVER_FED_URL')) {
return \getenv('TEST_SERVER_FED_URL');
}
return 'https://localhost:10200';
}
/**
* @return string
*/
public static function getCollaborationServiceUrl(): string {
if (\getenv("COLLABORATION_SERVICE_URL")) {
return \getenv("COLLABORATION_SERVICE_URL");
}
return "http://localhost:9300";
}
/**
* @return bool
*/
public static function isTestingOnReva(): bool {
return (\getenv("TEST_REVA") === "true");
}
/**
* @return bool
*/
public static function isUsingPreparedLdapUsers(): bool {
return (\getenv("USE_PREPARED_LDAP_USERS") === "true");
}
/**
* @return bool|string false if no command given or the command as string
*/
public static function getDeleteUserDataCommand() {
$cmd = \getenv("DELETE_USER_DATA_CMD");
if ($cmd === false || \trim($cmd) === "") {
return false;
}
return $cmd;
}
/**
* @return string
* @throws Exception
*/
public static function getStorageDriver(): string {
$storageDriver = (\getenv("STORAGE_DRIVER"));
if ($storageDriver === false) {
return StorageDriver::DECOMPOSED;
}
$storageDriver = \strtoupper($storageDriver);
if (!\in_array($storageDriver, self::STORAGE_DRIVERS)) {
throw new Exception(
"Invalid storage driver. " .
"STORAGE_DRIVER must be '" . \join(", ", self::STORAGE_DRIVERS) . "'"
);
}
return $storageDriver;
}
/**
* @param array $users
*
* @return void
* @throws Exception
*/
public static function deleteRevaUserData(?array $users = []): void {
$deleteCmd = self::getDeleteUserDataCommand();
if (self::getStorageDriver() === StorageDriver::POSIX) {
\exec($deleteCmd);
return;
}
foreach ($users as $user) {
if (\is_array($user)) {
$user = $user["actualUsername"];
}
if ($deleteCmd === false) {
if (self::getStorageDriver() === StorageDriver::OWNCLOUD) {
self::recurseRmdir(self::getOcRevaDataRoot() . $user);
}
continue;
} elseif (self::getStorageDriver() === StorageDriver::EOS) {
$deleteCmd = \str_replace(
"%s",
$user[0] . '/' . $user,
$deleteCmd
);
} else {
$deleteCmd = \sprintf($deleteCmd, $user);
}
\exec($deleteCmd);
}
}
/**
* Helper for Recursive Copy of file/folder
* For more info check this out https://gist.github.com/gserrano/4c9648ec9eb293b9377b
*
* @param string|null $source
* @param string|null $destination
*
* @return void
*/
public static function recurseCopy(?string $source, ?string $destination): void {
$dir = \opendir($source);
@\mkdir($destination);
while (($file = \readdir($dir)) !== false) {
if (($file != '.') && ($file != '..')) {
if (\is_dir($source . '/' . $file)) {
self::recurseCopy($source . '/' . $file, $destination . '/' . $file);
} else {
\copy($source . '/' . $file, $destination . '/' . $file);
}
}
}
\closedir($dir);
}
/**
* @return int
*/
public static function getLdapPort(): int {
$port = \getenv("REVA_LDAP_PORT");
return $port ? (int)$port : 636;
}
/**
* @return bool
*/
public static function useSsl(): bool {
$useSsl = \getenv("REVA_LDAP_USESSL");
if ($useSsl === false) {
return (self::getLdapPort() === 636);
} else {
return $useSsl === "true";
}
}
/**
* @return string
*/
public static function getBaseDN(): string {
$dn = \getenv("REVA_LDAP_BASE_DN");
return $dn ?: "dc=example,dc=org";
}
/**
* @return string
*/
public static function getGroupsOU(): string {
$ou = \getenv("REVA_LDAP_GROUPS_OU");
return $ou ?: "TestGroups";
}
/**
* @return string
*/
public static function getUsersOU(): string {
$ou = \getenv("REVA_LDAP_USERS_OU");
return $ou ?: "TestUsers";
}
/**
* @return string
*/
public static function getGroupSchema(): string {
$schema = \getenv("REVA_LDAP_GROUP_SCHEMA");
return $schema ?: "rfc2307";
}
/**
* @return string
*/
public static function getHostname(): string {
$hostname = \getenv("REVA_LDAP_HOSTNAME");
return $hostname ?: "localhost";
}
/**
* @return string
*/
public static function getBindDN(): string {
$dn = \getenv("REVA_LDAP_BIND_DN");
return $dn ?: "cn=admin,dc=example,dc=org";
}
/**
* @return string
*/
public static function getBindPassword(): string {
$pw = \getenv("REVA_LDAP_BIND_PASSWORD");
return $pw ?: "";
}
/**
* @return string
*/
private static function getOcRevaDataRoot(): string {
$root = \getenv("OC_REVA_DATA_ROOT");
if ($root === false || $root === "") {
$root = "/var/tmp/qsfera/qsfera/";
}
if (!\file_exists($root)) {
echo "WARNING: reva data root folder ($root) does not exist\n";
}
return $root;
}
/**
* @param string|null $dir
*
* @return bool
*/
private static function recurseRmdir(?string $dir): bool {
if (\file_exists($dir) === true) {
$files = \array_diff(\scandir($dir), ['.', '..']);
foreach ($files as $file) {
if (\is_dir("$dir/$file")) {
self::recurseRmdir("$dir/$file");
} else {
\unlink("$dir/$file");
}
}
return \rmdir($dir);
}
return true;
}
/**
* On Eos storage backend when the user data is cleared after test run
* Running another test immediately fails. So Send this request to create user home directory
*
* @param string|null $baseUrl
* @param string|null $user
* @param string|null $password
* @param string|null $xRequestId
*
* @return void
* @throws GuzzleException
*/
public static function createEOSStorageHome(
?string $baseUrl,
?string $user,
?string $password,
?string $xRequestId = ''
): void {
HttpRequestHelper::get(
$baseUrl . "/ocs/v2.php/apps/notifications/api/v1/notifications",
$xRequestId,
$user,
$password
);
}
}
@@ -0,0 +1,205 @@
<?php declare(strict_types=1);
/**
* @author Viktor Scharf <scharf.vi@gmail.com>
* @copyright Copyright (c) 2024 Viktor Scharf <scharf.vi@gmail.com>
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\ResponseInterface;
/**
* A helper class for managing federation server requests
*/
class OcmHelper {
/**
* @return string[]
*/
private static function getRequestHeaders(): array {
return [
'Content-Type' => 'application/json',
];
}
/**
* @param string $baseUrl
* @param string $path
*
* @return string
*/
public static function getFullUrl(string $baseUrl, string $path): string {
$fullUrl = $baseUrl;
if (\substr($fullUrl, -1) !== '/') {
$fullUrl .= '/';
}
$fullUrl .= 'sciencemesh/' . $path;
return $fullUrl;
}
/**
* @param string $baseUrl
* @param string $xRequestId
* @param string $user
* @param string $password
* @param string|null $email
* @param string|null $description
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function createInvitation(
string $baseUrl,
string $xRequestId,
string $user,
string $password,
?string $email = null,
?string $description = null
): ResponseInterface {
$body = [
"description" => $description,
"recipient" => $email
];
$url = self::getFullUrl($baseUrl, 'generate-invite');
return HttpRequestHelper::post(
$url,
$xRequestId,
$user,
$password,
self::getRequestHeaders(),
\json_encode($body)
);
}
/**
* @param string $baseUrl
* @param string $xRequestId
* @param string $user
* @param string $password
* @param string $token
* @param string $providerDomain
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function acceptInvitation(
string $baseUrl,
string $xRequestId,
string $user,
string $password,
string $token,
string $providerDomain
): ResponseInterface {
$body = [
"token" => $token,
"providerDomain" => $providerDomain
];
$url = self::getFullUrl($baseUrl, 'accept-invite');
return HttpRequestHelper::post(
$url,
$xRequestId,
$user,
$password,
self::getRequestHeaders(),
\json_encode($body)
);
}
/**
* @param string $baseUrl
* @param string $xRequestId
* @param string $user
* @param string $password
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function findAcceptedUsers(
string $baseUrl,
string $xRequestId,
string $user,
string $password
): ResponseInterface {
$url = self::getFullUrl($baseUrl, 'find-accepted-users');
return HttpRequestHelper::get(
$url,
$xRequestId,
$user,
$password,
self::getRequestHeaders()
);
}
/**
* @param string $baseUrl
* @param string $xRequestId
* @param string $user
* @param string $password
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function listInvite(
string $baseUrl,
string $xRequestId,
string $user,
string $password
): ResponseInterface {
$url = self::getFullUrl($baseUrl, 'list-invite');
return HttpRequestHelper::get(
$url,
$xRequestId,
$user,
$password,
self::getRequestHeaders()
);
}
/**
* @param string $baseUrl
* @param string $xRequestId
* @param string $user
* @param string $password
* @param string $userId
* @param string $idp
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function deleteConnection(
string $baseUrl,
string $xRequestId,
string $user,
string $password,
string $userId,
string $idp
): ResponseInterface {
$url = self::getFullUrl($baseUrl, 'delete-accepted-user');
$body = [
"idp" => $idp,
"user_id" => $userId
];
return HttpRequestHelper::delete(
$url,
$xRequestId,
$user,
$password,
self::getRequestHeaders(),
\json_encode($body)
);
}
}
@@ -0,0 +1,101 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2017 Artur Neumann artur@jankaritech.com
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Helper to make requests to the OCS API
*
* @author Artur Neumann <artur@jankaritech.com>
*
*/
class OcsApiHelper {
/**
* @param string|null $baseUrl
* @param string|null $user if set to null no authentication header will be sent
* @param string|null $password
* @param string|null $method HTTP Method
* @param string|null $path
* @param string|null $xRequestId
* @param mixed $body array of key, value pairs e.g ['value' => 'yes']
* @param int|null $ocsApiVersion (1|2) default 2
* @param array|null $headers
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function sendRequest(
?string $baseUrl,
?string $user,
?string $password,
?string $method,
?string $path,
?string $xRequestId = '',
$body = [],
?int $ocsApiVersion = 2,
?array $headers = []
): ResponseInterface {
$fullUrl = $baseUrl;
if (\substr($fullUrl, -1) !== '/') {
$fullUrl .= '/';
}
$fullUrl .= "ocs/v$ocsApiVersion.php" . $path;
$headers['OCS-APIREQUEST'] = true;
return HttpRequestHelper::sendRequest($fullUrl, $xRequestId, $method, $user, $password, $headers, $body);
}
/**
*
* @param string|null $baseUrl
* @param string|null $method HTTP Method
* @param string|null $path
* @param string|null $xRequestId
* @param mixed $body array of key, value pairs e.g ['value' => 'yes']
* @param int|null $ocsApiVersion (1|2) default 2
* @param array|null $headers
*
* @return RequestInterface
*/
public static function createOcsRequest(
?string $baseUrl,
?string $method,
?string $path,
?string $xRequestId = '',
$body = [],
?int $ocsApiVersion = 2,
?array $headers = []
): RequestInterface {
$fullUrl = $baseUrl;
if (\substr($fullUrl, -1) !== '/') {
$fullUrl .= '/';
}
$fullUrl .= "ocs/v$ocsApiVersion.php" . $path;
return HttpRequestHelper::createRequest(
$fullUrl,
$xRequestId,
$method,
$headers,
$body
);
}
}
@@ -0,0 +1,335 @@
<?php declare(strict_types=1);
/**
* @author Sajan Gurung <sajan@jankaritech.com>
* @copyright Copyright (c) 2024 Sajan Gurung sajan@jankaritech.com
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
use TestHelpers\HttpRequestHelper;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\ResponseInterface;
use PHPUnit\Framework\Assert;
/**
* A helper class for КуСфера settings
*/
class SettingsHelper {
private static string $settingsEndpoint = '/api/v0/settings/';
/**
* @param string $baseUrl
* @param string $path
*
* @return string
*/
public static function buildFullUrl(string $baseUrl, string $path): string {
return $baseUrl . self::$settingsEndpoint . $path;
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $xRequestId
* @param array $headers
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public static function getBundlesList(
string $baseUrl,
string $user,
string $password,
string $xRequestId,
array $headers = []
): ResponseInterface {
$fullUrl = self::buildFullUrl($baseUrl, "bundles-list");
return HttpRequestHelper::post(
$fullUrl,
$xRequestId,
$user,
$password,
$headers,
"{}"
);
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $bundleName
* @param string $xRequestId
*
* @return array
*
* @throws GuzzleException
* @throws Exception
*/
public static function getBundleByName(
string $baseUrl,
string $user,
string $password,
string $bundleName,
string $xRequestId
): array {
$response = self::getBundlesList($baseUrl, $user, $password, $xRequestId);
Assert::assertEquals(201, $response->getStatusCode(), "Failed to get bundles list");
$bundlesList = HttpRequestHelper::getJsonDecodedResponseBodyContent($response);
foreach ($bundlesList->bundles as $value) {
if ($value->displayName === $bundleName) {
return json_decode(json_encode($value), true);
}
}
return [];
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $xRequestId
* @param array $headers
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public static function getRolesList(
string $baseUrl,
string $user,
string $password,
string $xRequestId,
array $headers = []
): ResponseInterface {
$fullUrl = self::buildFullUrl($baseUrl, "roles-list");
return HttpRequestHelper::post(
$fullUrl,
$xRequestId,
$user,
$password,
$headers,
"{}"
);
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $assigneeId
* @param string $roleId
* @param string $xRequestId
* @param array $headers
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public static function assignRoleToUser(
string $baseUrl,
string $user,
string $password,
string $assigneeId,
string $roleId,
string $xRequestId,
array $headers = []
): ResponseInterface {
$fullUrl = self::buildFullUrl($baseUrl, "assignments-add");
$body = json_encode(["account_uuid" => $assigneeId, "role_id" => $roleId], JSON_THROW_ON_ERROR);
return HttpRequestHelper::post(
$fullUrl,
$xRequestId,
$user,
$password,
$headers,
$body
);
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $userId
* @param string $xRequestId
* @param array $headers
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public static function getAssignmentsList(
string $baseUrl,
string $user,
string $password,
string $userId,
string $xRequestId,
array $headers = []
): ResponseInterface {
$fullUrl = self::buildFullUrl($baseUrl, "assignments-list");
$body = json_encode(["account_uuid" => $userId], JSON_THROW_ON_ERROR);
return HttpRequestHelper::post(
$fullUrl,
$xRequestId,
$user,
$password,
$headers,
$body
);
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $xRequestId
* @param array $headers
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public static function getValuesList(
string $baseUrl,
string $user,
string $password,
string $xRequestId,
array $headers = []
): ResponseInterface {
$fullUrl = self::buildFullUrl($baseUrl, "values-list");
$body = json_encode(["account_uuid" => "me"], JSON_THROW_ON_ERROR);
return HttpRequestHelper::post(
$fullUrl,
$xRequestId,
$user,
$password,
$headers,
$body
);
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $xRequestId
*
* @return bool
*
* @throws GuzzleException
* @throws Exception
*/
public static function getAutoAcceptSharesSettingValue(
string $baseUrl,
string $user,
string $password,
string $xRequestId
): bool {
$response = self::getValuesList($baseUrl, $user, $password, $xRequestId);
Assert::assertEquals(201, $response->getStatusCode(), "Failed to get values list");
$valuesList = HttpRequestHelper::getJsonDecodedResponseBodyContent($response);
if (empty($valuesList)) {
return true;
}
foreach ($valuesList->values as $value) {
if ($value->identifier->setting === "auto-accept-shares") {
return $value->value->boolValue;
}
}
return true;
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $xRequestId
*
* @return string
*
* @throws GuzzleException
* @throws Exception
*/
public static function getLanguageSettingValue(
string $baseUrl,
string $user,
string $password,
string $xRequestId
): string {
$response = self::getValuesList($baseUrl, $user, $password, $xRequestId);
Assert::assertEquals(201, $response->getStatusCode(), "Failed to get values list");
$valuesList = HttpRequestHelper::getJsonDecodedResponseBodyContent($response);
// if no language is set, the request body is empty return English as the default language
if (empty($valuesList)) {
return "en";
}
foreach ($valuesList->values as $value) {
if ($value->identifier->setting === "language") {
return $value->value->listValue->values[0]->stringValue;
}
}
// if a language setting was still not found, return English
return "en";
}
/**
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $body
* @param string $xRequestId
* @param array $headers
*
* @return ResponseInterface
*
* @throws GuzzleException
* @throws Exception
*/
public static function updateSettings(
string $baseUrl,
string $user,
string $password,
string $body,
string $xRequestId,
array $headers = []
): ResponseInterface {
$fullUrl = self::buildFullUrl($baseUrl, "values-save");
return HttpRequestHelper::post(
$fullUrl,
$xRequestId,
$user,
$password,
$headers,
$body
);
}
}
@@ -0,0 +1,406 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2017 Artur Neumann artur@jankaritech.com
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
use Behat\Testwork\Hook\Scope\HookScope;
use GuzzleHttp\Exception\GuzzleException;
use Exception;
use SimpleXMLElement;
/**
* Helper to set up UI / Integration tests
*
* @author Artur Neumann <artur@jankaritech.com>
*
*/
class SetupHelper extends \PHPUnit\Framework\Assert {
private static ?string $baseUrl = null;
private static ?string $adminUsername = null;
private static ?string $adminPassword = null;
/**
*
* @param HookScope $scope
*
* @return array of suite context parameters
*/
public static function getSuiteParameters(HookScope $scope): array {
return $scope->getEnvironment()->getSuite()
->getSettings() ['context'] ['parameters'];
}
/**
*
* @param string|null $adminUsername
* @param string|null $adminPassword
* @param string|null $baseUrl
*
* @return void
*/
public static function init(
?string $adminUsername,
?string $adminPassword,
?string $baseUrl
): void {
foreach (\func_get_args() as $variableToCheck) {
if (!\is_string($variableToCheck)) {
throw new \InvalidArgumentException(
"mandatory argument missing or wrong type ($variableToCheck => "
. \gettype($variableToCheck) . ")"
);
}
}
self::$adminUsername = $adminUsername;
self::$adminPassword = $adminPassword;
self::$baseUrl = \rtrim($baseUrl, '/');
}
/**
*
* @param string|null $baseUrl
* @param string|null $adminUsername
* @param string|null $adminPassword
* @param string|null $xRequestId
*
* @return SimpleXMLElement
* @throws GuzzleException
* @throws Exception
*/
public static function getSysInfo(
?string $baseUrl,
?string $adminUsername,
?string $adminPassword,
?string $xRequestId = ''
): SimpleXMLElement {
$result = OcsApiHelper::sendRequest(
$baseUrl,
$adminUsername,
$adminPassword,
"GET",
"/apps/testing/api/v1/sysinfo",
$xRequestId
);
if ($result->getStatusCode() !== 200) {
throw new \Exception(
"could not get sysinfo " . $result->getReasonPhrase()
);
}
return HttpRequestHelper::getResponseXml($result, __METHOD__)->data;
}
/**
*
* @param string|null $baseUrl
* @param string|null $adminUsername
* @param string|null $adminPassword
* @param string|null $xRequestId
*
* @return string
* @throws GuzzleException
*/
public static function getServerRoot(
?string $baseUrl,
?string $adminUsername,
?string $adminPassword,
?string $xRequestId = ''
): string {
$sysInfo = self::getSysInfo(
$baseUrl,
$adminUsername,
$adminPassword,
$xRequestId
);
// server_root is a SimpleXMLElement object that "wraps" a string.
/// We want the bare string.
return (string) $sysInfo->server_root;
}
/**
* @param string|null $adminUsername
* @param string|null $callerName
*
* @return string
* @throws Exception
*/
private static function checkAdminUsername(?string $adminUsername, ?string $callerName): ?string {
if (self::$adminUsername === null
&& $adminUsername === null
) {
throw new Exception(
"$callerName called without adminUsername - pass the username or call SetupHelper::init"
);
}
if ($adminUsername === null) {
$adminUsername = self::$adminUsername;
}
return $adminUsername;
}
/**
* @param string|null $adminPassword
* @param string|null $callerName
*
* @return string
* @throws Exception
*/
private static function checkAdminPassword(?string $adminPassword, ?string $callerName): string {
if (self::$adminPassword === null
&& $adminPassword === null
) {
throw new Exception(
"$callerName called without adminPassword - pass the password or call SetupHelper::init"
);
}
if ($adminPassword === null) {
$adminPassword = self::$adminPassword;
}
return $adminPassword;
}
/**
* @param string|null $baseUrl
* @param string|null $callerName
*
* @return string
* @throws Exception
*/
private static function checkBaseUrl(?string $baseUrl, ?string $callerName): ?string {
if (self::$baseUrl === null
&& $baseUrl === null
) {
throw new Exception(
"$callerName called without baseUrl - pass the baseUrl or call SetupHelper::init"
);
}
if ($baseUrl === null) {
$baseUrl = self::$baseUrl;
}
return $baseUrl;
}
/**
*
* @param string|null $dirPathFromServerRoot e.g. 'apps2/myapp/appinfo'
* @param string|null $xRequestId
* @param string|null $baseUrl
* @param string|null $adminUsername
* @param string|null $adminPassword
*
* @return void
* @throws GuzzleException
* @throws Exception
*/
public static function mkDirOnServer(
?string $dirPathFromServerRoot,
?string $xRequestId = '',
?string $baseUrl = null,
?string $adminUsername = null,
?string $adminPassword = null
): void {
$baseUrl = self::checkBaseUrl($baseUrl, "mkDirOnServer");
$adminUsername = self::checkAdminUsername($adminUsername, "mkDirOnServer");
$adminPassword = self::checkAdminPassword($adminPassword, "mkDirOnServer");
$result = OcsApiHelper::sendRequest(
$baseUrl,
$adminUsername,
$adminPassword,
"POST",
"/apps/testing/api/v1/dir",
$xRequestId,
['dir' => $dirPathFromServerRoot]
);
if ($result->getStatusCode() !== 200) {
throw new \Exception(
"could not create directory $dirPathFromServerRoot " . $result->getReasonPhrase()
);
}
}
/**
*
* @param string|null $dirPathFromServerRoot e.g. 'apps2/myapp/appinfo'
* @param string|null $xRequestId
* @param string|null $baseUrl
* @param string|null $adminUsername
* @param string|null $adminPassword
*
* @return void
* @throws GuzzleException
* @throws Exception
*/
public static function rmDirOnServer(
?string $dirPathFromServerRoot,
?string $xRequestId = '',
?string $baseUrl = null,
?string $adminUsername = null,
?string $adminPassword = null
): void {
$baseUrl = self::checkBaseUrl($baseUrl, "rmDirOnServer");
$adminUsername = self::checkAdminUsername($adminUsername, "rmDirOnServer");
$adminPassword = self::checkAdminPassword($adminPassword, "rmDirOnServer");
$result = OcsApiHelper::sendRequest(
$baseUrl,
$adminUsername,
$adminPassword,
"DELETE",
"/apps/testing/api/v1/dir",
$xRequestId,
['dir' => $dirPathFromServerRoot]
);
if ($result->getStatusCode() !== 200) {
throw new \Exception(
"could not delete directory $dirPathFromServerRoot " . $result->getReasonPhrase()
);
}
}
/**
*
* @param string|null $filePathFromServerRoot e.g. 'app2/myapp/appinfo/info.xml'
* @param string|null $content
* @param string|null $xRequestId
* @param string|null $baseUrl
* @param string|null $adminUsername
* @param string|null $adminPassword
*
* @return void
* @throws GuzzleException
* @throws Exception
*/
public static function createFileOnServer(
?string $filePathFromServerRoot,
?string $content,
?string $xRequestId = '',
?string $baseUrl = null,
?string $adminUsername = null,
?string $adminPassword = null
): void {
$baseUrl = self::checkBaseUrl($baseUrl, "createFileOnServer");
$adminUsername = self::checkAdminUsername($adminUsername, "createFileOnServer");
$adminPassword = self::checkAdminPassword($adminPassword, "createFileOnServer");
$result = OcsApiHelper::sendRequest(
$baseUrl,
$adminUsername,
$adminPassword,
"POST",
"/apps/testing/api/v1/file",
$xRequestId,
[
'file' => $filePathFromServerRoot,
'content' => $content
]
);
if ($result->getStatusCode() !== 200) {
throw new \Exception(
"could not create file $filePathFromServerRoot " . $result->getReasonPhrase()
);
}
}
/**
*
* @param string|null $filePathFromServerRoot e.g. 'app2/myapp/appinfo/info.xml'
* @param string|null $xRequestId
* @param string|null $baseUrl
* @param string|null $adminUsername
* @param string|null $adminPassword
*
* @return void
* @throws GuzzleException
* @throws Exception
*/
public static function deleteFileOnServer(
?string $filePathFromServerRoot,
?string $xRequestId = '',
?string $baseUrl = null,
?string $adminUsername = null,
?string $adminPassword = null
): void {
$baseUrl = self::checkBaseUrl($baseUrl, "deleteFileOnServer");
$adminUsername = self::checkAdminUsername($adminUsername, "deleteFileOnServer");
$adminPassword = self::checkAdminPassword($adminPassword, "deleteFileOnServer");
$result = OcsApiHelper::sendRequest(
$baseUrl,
$adminUsername,
$adminPassword,
"DELETE",
"/apps/testing/api/v1/file",
$xRequestId,
[
'file' => $filePathFromServerRoot
]
);
if ($result->getStatusCode() !== 200) {
throw new \Exception(
"could not delete file $filePathFromServerRoot " . $result->getReasonPhrase()
);
}
}
/**
* @param string|null $fileInCore e.g. 'app2/myapp/appinfo/info.xml'
* @param string|null $xRequestId
* @param string|null $baseUrl
* @param string|null $adminUsername
* @param string|null $adminPassword
*
* @return string
* @throws GuzzleException
* @throws Exception
*/
public static function readFileFromServer(
?string $fileInCore,
?string $xRequestId = '',
?string $baseUrl = null,
?string $adminUsername = null,
?string $adminPassword = null
): string {
$baseUrl = self::checkBaseUrl($baseUrl, "readFile");
$adminUsername = self::checkAdminUsername(
$adminUsername,
"readFile"
);
$adminPassword = self::checkAdminPassword(
$adminPassword,
"readFile"
);
$response = OcsApiHelper::sendRequest(
$baseUrl,
$adminUsername,
$adminPassword,
'GET',
"/apps/testing/api/v1/file?file=$fileInCore",
$xRequestId
);
self::assertSame(
200,
$response->getStatusCode(),
"Failed to read the file $fileInCore"
);
$localContent = HttpRequestHelper::getResponseXml($response, __METHOD__);
$localContent = (string)$localContent->data->element->contentUrlEncoded;
return \urldecode($localContent);
}
}
@@ -0,0 +1,268 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2017 Artur Neumann artur@jankaritech.com
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
use Exception;
use GuzzleHttp\Exception\GuzzleException;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use SimpleXMLElement;
/**
* manage Shares via OCS API
*
* @author Artur Neumann <artur@jankaritech.com>
*
*/
class SharingHelper {
public const PERMISSION_TYPES = [
'read' => 1,
'update' => 2,
'create' => 4,
'delete' => 8,
'invite' => 0
];
public const SHARE_TYPES = [
'user' => 0,
'group' => 1,
'public_link' => 3,
'federated' => 6,
];
public const SHARE_STATES = [
'accepted' => 0,
'pending' => 1,
'rejected' => 2,
'declined' => 2, // declined is a synonym for rejected.
];
/**
*
* @param string $baseUrl baseURL of the КуСфера installation without /ocs.
* @param string $user user that creates the share.
* @param string $password password of the user that creates the share.
* @param string $path The path to the file or folder which should be shared.
* @param string|int $shareType The type of the share. This can be one of:
* 0 = user, 1 = group, 3 = public (link),
* 6 = federated (cloud share).
* Pass either the number or the keyword.
* @param string $xRequestId
* @param string|null $shareWith The user or group id with which the file should
* be shared.
* @param boolean|null $publicUpload Whether to allow public upload to a public
* shared folder.
* @param string|null $sharePassword The password to protect the public link
* share with.
* @param string|int|string[]|int[]|null $permissions The permissions to set on the share.
* 1 = read; 2 = update; 4 = create;
* 8 = delete; 16 = share
* (default: 31, for public shares: 1)
* Pass either the (total) number or array of numbers,
* or any of the above keywords or array of keywords.
* @param string|null $linkName A (human-readable) name for the share,
* which can be up to 64 characters in length.
* @param string|null $expireDate An expire date for public link shares.
* This argument expects a date string
* in the format 'YYYY-MM-DD'.
* @param string|null $space_ref
* @param int $ocsApiVersion
* @param int $sharingApiVersion
* @param string $sharingApp
*
* @return ResponseInterface
* @throws InvalidArgumentException|GuzzleException
*/
public static function createShare(
string $baseUrl,
string $user,
string $password,
string $path,
$shareType,
string $xRequestId = '',
?string $shareWith = null,
?bool $publicUpload = false,
?string $sharePassword = null,
$permissions = null,
?string $linkName = null,
?string $expireDate = null,
?string $space_ref = null,
int $ocsApiVersion = 1,
int $sharingApiVersion = 1,
string $sharingApp = 'files_sharing'
): ResponseInterface {
$fd = [];
foreach ([$path, $baseUrl, $user, $password] as $variableToCheck) {
if (!\is_string($variableToCheck)) {
throw new InvalidArgumentException(
"mandatory argument missing or wrong type ($variableToCheck => "
. \gettype($variableToCheck) . ")"
);
}
}
if ($permissions !== null) {
$fd['permissions'] = self::getPermissionSum($permissions);
} elseif ($shareType !== "public_link") {
// sharing without permissions should automatically set the permission to 15 for share and 1 for public link
$fd['permissions'] = 15;
}
if (!\in_array($ocsApiVersion, [1, 2], true)) {
throw new InvalidArgumentException(
"invalid ocsApiVersion ($ocsApiVersion)"
);
}
if (!\in_array($sharingApiVersion, [1, 2], true)) {
throw new InvalidArgumentException(
"invalid sharingApiVersion ($sharingApiVersion)"
);
}
$fullUrl = $baseUrl;
if (\substr($fullUrl, -1) !== '/') {
$fullUrl .= '/';
}
$fullUrl .= "ocs/v$ocsApiVersion.php/apps/$sharingApp/api/v$sharingApiVersion/shares";
$fd['path'] = $path;
$fd['shareType'] = self::getShareType($shareType);
if ($shareWith !== null) {
$fd['shareWith'] = $shareWith;
}
if ($publicUpload !== null) {
$fd['publicUpload'] = $publicUpload;
}
if ($sharePassword !== null) {
$fd['password'] = $sharePassword;
}
if ($linkName !== null) {
$fd['name'] = $linkName;
}
if ($expireDate !== null) {
$fd['expireDate'] = $expireDate;
}
if ($space_ref !== null) {
$fd['space_ref'] = $space_ref;
}
$headers = ['OCS-APIREQUEST' => 'true'];
return HttpRequestHelper::post(
$fullUrl,
$xRequestId,
$user,
$password,
$headers,
$fd
);
}
/**
* Calculates the permission sum (int) from the given permissions.
* Permissions can be passed in as int, string or array of int or string
* 'read' => 1
* 'update' => 2
* 'create' => 4
* 'delete' => 8
* 'share' => 16
* 'invite' => 0
*
* @param string[]|string|int|int[] $permissions
*
* @return int
* @throws InvalidArgumentException
*
*/
public static function getPermissionSum($permissions): int {
if (\is_numeric($permissions)) {
// Allow any permission number so that test scenarios can
// specifically test invalid permission values
return (int) $permissions;
}
if (!\is_array($permissions)) {
$permissions = [$permissions];
}
$permissionSum = 0;
foreach ($permissions as $permission) {
if (\array_key_exists($permission, self::PERMISSION_TYPES)) {
$permissionSum += self::PERMISSION_TYPES[$permission];
} elseif (\in_array($permission, self::PERMISSION_TYPES, true)) {
$permissionSum += $permission;
} else {
throw new InvalidArgumentException(
"invalid permission type ($permission)"
);
}
}
if ($permissionSum < 0 || $permissionSum > 31) {
throw new InvalidArgumentException(
"invalid permission total ($permissionSum)"
);
}
return $permissionSum;
}
/**
* returns the share type number corresponding to the share type keyword
*
* @param string|int $shareType a keyword from SHARE_TYPES or a share type number
*
* @return int
* @throws InvalidArgumentException
*
*/
public static function getShareType($shareType): int {
if (\array_key_exists($shareType, self::SHARE_TYPES)) {
return self::SHARE_TYPES[$shareType];
} else {
if (\ctype_digit($shareType)) {
$shareType = (int) $shareType;
}
$key = \array_search($shareType, self::SHARE_TYPES, true);
if ($key !== false) {
return self::SHARE_TYPES[$key];
}
throw new InvalidArgumentException(
"invalid share type ($shareType)"
);
}
}
/**
*
* @param SimpleXMLElement $responseXmlObject
* @param string $errorMessage
*
* @return string
* @throws Exception
*
*/
public static function getLastShareIdFromResponse(
SimpleXMLElement $responseXmlObject,
string $errorMessage = "cannot find share id in response"
): string {
$xmlPart = $responseXmlObject->xpath("//data/element[last()]/id");
if (!\is_array($xmlPart) || (\count($xmlPart) === 0)) {
throw new Exception($errorMessage);
}
return $xmlPart[0]->__toString();
}
}
@@ -0,0 +1,14 @@
<?php
/**
* @author Sagar Gurung <sagar@jankatitech.com>
*
*/
namespace TestHelpers;
use Exception;
/**
* Class SpaceNotFoundException
* Exception when space id for a user is not found
*/
class SpaceNotFoundException extends Exception {
}
@@ -0,0 +1,373 @@
<?php
/**
* @author Viktor Scharf <v.scharf@qsfera.eu>
* @copyright Copyright (c) 2025 Viktor Scharf <v.scharf@qsfera.eu>
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\Exception\GuzzleException;
use PHPUnit\Framework\Assert;
/**
* Helper for obtaining bearer tokens for users
*/
class TokenHelper {
private const LOGON_URL = '/signin/v1/identifier/_/logon';
private const REDIRECT_URL = '/oidc-callback.html';
private const TOKEN_URL = '/konnect/v1/token';
// Static cache [username => token_data]
private static array $tokenCache = [];
/**
* @return bool
*/
public static function useBearerToken(): bool {
return \getenv('USE_BEARER_TOKEN') === 'true';
}
/**
* Extracts base URL from a full URL
*
* @param string $url
*
* @return string the base URL
*/
private static function extractBaseUrl(string $url): string {
return preg_replace('#(https?://[^/]+).*#', '$1', $url);
}
/**
* Get access and refresh tokens for a user
* Uses cache to avoid unnecessary requests
*
* @param string $username
* @param string $password
* @param string $url
*
* @return array ['access_token' => string, 'refresh_token' => string, 'expires_at' => int]
* @throws GuzzleException
* @throws Exception
*/
public static function getTokens(string $username, string $password, string $url): array {
// Extract base URL. I need to send $url to get correct server in case of multiple servers (ocm suite)
$baseUrl = self::extractBaseUrl($url);
$cacheKey = $username . '|' . $baseUrl;
// Check cache
if (isset(self::$tokenCache[$cacheKey])) {
$cachedToken = self::$tokenCache[$cacheKey];
// Check if access token has expired
if (time() < $cachedToken['expires_at']) {
return $cachedToken;
}
$refreshedToken = self::refreshToken($cachedToken['refresh_token'], $baseUrl);
$tokenData = [
'access_token' => $refreshedToken['access_token'],
'refresh_token' => $refreshedToken['refresh_token'],
// set expiry to 240 (4 minutes) seconds to allow for some buffer
// token actually expires in 300 seconds (5 minutes)
'expires_at' => time() + 240
];
self::$tokenCache[$cacheKey] = $tokenData;
return $tokenData;
}
// Get new tokens
$cookieJar = new CookieJar();
$continueUrl = self::getAuthorizedEndPoint($username, $password, $baseUrl, $cookieJar);
$code = self::getCode($continueUrl, $baseUrl, $cookieJar);
$tokens = self::getToken($code, $baseUrl, $cookieJar);
$tokenData = [
'access_token' => $tokens['access_token'],
'refresh_token' => $tokens['refresh_token'],
// set expiry to 240 (4 minutes) seconds to allow for some buffer
// token actually expires in 300 seconds (5 minutes)
'expires_at' => time() + 240
];
// Save to cache
self::$tokenCache[$cacheKey] = $tokenData;
return $tokenData;
}
/**
* Refresh token
*
* @param string $refreshToken
* @param string $baseUrl
*
* @return array
* @throws GuzzleException
* @throws Exception
*/
private static function refreshToken(string $refreshToken, string $baseUrl): array {
$client = new Client(
[
'verify' => false,
'http_errors' => false,
'allow_redirects' => false
]
);
$response = $client->post(
$baseUrl . self::TOKEN_URL,
[
'form_params' => [
'client_id' => 'web',
'refresh_token' => $refreshToken,
'grant_type' => 'refresh_token'
]
]
);
Assert::assertEquals(
200,
$response->getStatusCode(),
'Token refresh failed: Expected status code 200 but received ' . $response->getStatusCode()
);
$data = json_decode($response->getBody()->getContents(), true);
Assert::assertArrayHasKey('access_token', $data, 'Missing access_token in refresh response');
Assert::assertArrayHasKey('refresh_token', $data, 'Missing refresh_token in refresh response');
return [
'access_token' => $data['access_token'],
'refresh_token' => $data['refresh_token']
];
}
/**
* Clear cached tokens for a specific user
*
* @param string $username
* @param string $url
*
* @return void
*/
public static function clearUserTokens(string $username, string $url): void {
$baseUrl = self::extractBaseUrl($url);
$cacheKey = $username . '|' . $baseUrl;
unset(self::$tokenCache[$cacheKey]);
}
/**
* Clear all cached tokens
*
* @return void
*/
public static function clearAllTokens(): void {
self::$tokenCache = [];
}
/**
* @param string $username
* @param string $password
* @param string $baseUrl
* @param CookieJar $cookieJar
*
* @return \Psr\Http\Message\ResponseInterface
* @throws GuzzleException
*/
public static function makeLoginRequest(
string $username,
string $password,
string $baseUrl,
CookieJar $cookieJar
): \Psr\Http\Message\ResponseInterface {
$client = new Client(
[
'verify' => false,
'http_errors' => false,
'allow_redirects' => false,
'cookies' => $cookieJar
]
);
return $client->post(
$baseUrl . self::LOGON_URL,
[
'headers' => [
'Kopano-Konnect-XSRF' => '1',
'Referer' => $baseUrl,
'Content-Type' => 'application/json'
],
'json' => [
'params' => [$username, $password, '1'],
'hello' => [
'scope' => 'openid profile offline_access email',
'client_id' => 'web',
'redirect_uri' => $baseUrl . self::REDIRECT_URL,
'flow' => 'oidc'
]
]
]
);
}
/**
* Step 1: Login and get continue_uri
*
* @param string $username
* @param string $password
* @param string $baseUrl
* @param CookieJar $cookieJar
*
* @return string
* @throws GuzzleException
* @throws Exception
*/
private static function getAuthorizedEndPoint(
string $username,
string $password,
string $baseUrl,
CookieJar $cookieJar
): string {
$response = self::makeLoginRequest($username, $password, $baseUrl, $cookieJar);
Assert::assertEquals(
200,
$response->getStatusCode(),
'Logon failed: Expected status code 200 but received: ' . $response->getStatusCode()
);
$data = json_decode($response->getBody()->getContents(), true);
Assert::assertArrayHasKey(
'hello',
$data,
'Logon response does not contain "hello" object'
);
Assert::assertArrayHasKey(
'continue_uri',
$data['hello'],
'Missing continue_uri in logon response'
);
return $data['hello']['continue_uri'];
}
/**
* Step 2: Authorization and get code
*
* @param string $continueUrl
* @param string $baseUrl
* @param CookieJar $cookieJar
*
* @return string
* @throws GuzzleException
* @throws Exception
*/
private static function getCode(string $continueUrl, string $baseUrl, CookieJar $cookieJar): string {
$client = new Client(
[
'verify' => false,
'http_errors' => false,
'allow_redirects' => false, // Disable automatic redirects
'cookies' => $cookieJar
]
);
$params = [
'client_id' => 'web',
'prompt' => 'none',
'redirect_uri' => $baseUrl . self::REDIRECT_URL,
'response_mode' => 'query',
'response_type' => 'code',
'scope' => 'openid profile offline_access email'
];
$response = $client->get(
$continueUrl,
[
'query' => $params
]
);
Assert::assertEquals(
302,
$response->getStatusCode(),
'Authorization request failed: Expected status code 302 but received: ' . $response->getStatusCode()
);
$location = $response->getHeader('Location')[0] ?? '';
Assert::assertNotEmpty($location, 'Missing Location header in authorization response');
parse_str(parse_url($location, PHP_URL_QUERY), $queryParams);
Assert::assertArrayHasKey('code', $queryParams, 'Missing code parameter in redirect URL');
return $queryParams['code'];
}
/**
* Step 3: Get token
*
* @param string $code
* @param string $baseUrl
* @param CookieJar $cookieJar
*
* @return array
*
* @throws GuzzleException
* @throws Exception
*
*/
private static function getToken(string $code, string $baseUrl, CookieJar $cookieJar): array {
$client = new Client(
[
'verify' => false,
'http_errors' => false,
'allow_redirects' => false,
'cookies' => $cookieJar
]
);
$response = $client->post(
$baseUrl . self::TOKEN_URL,
[
'form_params' => [
'client_id' => 'web',
'code' => $code,
'redirect_uri' => $baseUrl . self::REDIRECT_URL,
'grant_type' => 'authorization_code'
]
]
);
Assert::assertEquals(
200,
$response->getStatusCode(),
'Token request failed: Expected status code 200 but received: ' . $response->getStatusCode()
);
$data = json_decode($response->getBody()->getContents(), true);
Assert::assertArrayHasKey('access_token', $data, 'Missing access_token in token response');
Assert::assertArrayHasKey('refresh_token', $data, 'Missing refresh_token in token response');
return [
'access_token' => $data['access_token'],
'refresh_token' => $data['refresh_token']
];
}
}
@@ -0,0 +1,44 @@
<?php declare(strict_types=1);
/**
* @author Talank Baral <talank@jankaritech.com>
* @copyright Copyright (c) 2021 Talank Baral talank@jankaritech.com
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
/**
* Class TranslationHelper
*
* Helper functions that are needed to run tests in different languages
*
* @package TestHelpers
*/
class TranslationHelper {
/**
* @param string|null $language
*
* @return string|null
*/
public static function getLanguage(?string $language): ?string {
if (!isset($language)) {
if (\getenv('OC_LANGUAGE') !== false) {
$language = \getenv('OC_LANGUAGE');
}
}
return $language;
}
}
@@ -0,0 +1,214 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2017 Artur Neumann artur@jankaritech.com
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
use GuzzleHttp\Exception\GuzzleException;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ResponseInterface;
/**
* Helper for Uploads
*
* @author Artur Neumann <artur@jankaritech.com>
*
*/
class UploadHelper extends Assert {
/**
*
* @param string|null $baseUrl URL of КуСфера
* @param string|null $user
* @param string|null $password
* @param string|null $source
* @param string|null $destination
* @param string|null $xRequestId
* @param array|null $headers
* @param int|null $davPathVersionToUse (1|2)
* @param bool $doChunkUpload
* @param int|null $noOfChunks how many chunks to upload
* @param bool|null $isGivenStep
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function upload(
?string $baseUrl,
?string $user,
?string $password,
?string $source,
?string $destination,
?string $xRequestId = '',
?array $headers = [],
?int $davPathVersionToUse = 1,
bool $doChunkUpload = false,
?int $noOfChunks = 1,
?bool $isGivenStep = false,
): ResponseInterface {
if (!$doChunkUpload) {
$data = \file_get_contents($source);
return WebDavHelper::makeDavRequest(
$baseUrl,
$user,
$password,
"PUT",
$destination,
$headers,
null,
$xRequestId,
$data,
$davPathVersionToUse,
"files",
null,
"basic",
false,
0,
null,
[],
null,
$isGivenStep
);
}
//prepare chunking
$chunks = self::chunkFile($source, $noOfChunks);
$chunkingId = 'chunking-' . \rand(1000, 9999);
$result = null;
//upload chunks
foreach ($chunks as $index => $chunk) {
$filename = $destination . "-" . $chunkingId . "-" . \count($chunks) . '-' . $index;
$result = WebDavHelper::makeDavRequest(
$baseUrl,
$user,
$password,
"PUT",
$filename,
$headers,
null,
$xRequestId,
$chunk,
$davPathVersionToUse,
"files",
null,
"basic",
false,
0,
null,
[],
null,
$isGivenStep
);
if ($result->getStatusCode() >= 400) {
return $result;
}
}
self::assertNotNull($result, __METHOD__ . " chunking was requested but no upload was done.");
return $result;
}
/**
* cut the file in multiple chunks
* returns an array of chunks with the content of the file
*
* @param string|null $file
* @param int|null $noOfChunks
*
* @return array $string
*/
public static function chunkFile(?string $file, ?int $noOfChunks = 1): array {
$size = \filesize($file);
$chunkSize = \ceil($size / $noOfChunks);
$chunks = [];
$fp = \fopen($file, 'r');
while (!\feof($fp) && \ftell($fp) < $size) {
$chunks[] = \fread($fp, (int)$chunkSize);
}
\fclose($fp);
if (\count($chunks) === 0) {
// chunk an empty file
$chunks[] = '';
}
return $chunks;
}
/**
* creates a File with a specific size
*
* @param string|null $name full path of the file to create
* @param int|null $size
*
* @return void
*/
public static function createFileSpecificSize(?string $name, ?int $size): void {
if (\file_exists($name)) {
\unlink($name);
}
$file = \fopen($name, 'w');
\fseek($file, \max($size - 1, 0), SEEK_CUR);
if ($size) {
\fwrite($file, 'a'); // write a dummy char at SIZE position
}
\fclose($file);
self::assertEquals(
1,
\file_exists($name)
);
self::assertEquals(
$size,
\filesize($name)
);
}
/**
* creates a File with a specific text content
*
* @param string|null $name full path of the file to create
* @param string|null $text
*
* @return void
*/
public static function createFileWithText(?string $name, ?string $text): void {
$file = \fopen($name, 'w');
\fwrite($file, $text);
\fclose($file);
self::assertEquals(
1,
\file_exists($name)
);
}
/**
* get the path of the acceptance tests directory
*
* @return string
*/
public static function getAcceptanceTestsDir(): string {
return \dirname(__FILE__) . "/../";
}
/**
* get the path of the filesForUpload directory
*
* @return string
*/
public static function getFilesForUploadDir(): string {
return \dirname(__FILE__) . "/../filesForUpload/";
}
}
@@ -0,0 +1,317 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2017 Artur Neumann artur@jankaritech.com
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
use Exception;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\ResponseInterface;
/**
* Helper to administrate users (and groups) through the provisioning API
*
* @author Artur Neumann <artur@jankaritech.com>
*
*/
class UserHelper {
/**
*
* @param string|null $baseUrl
* @param string $user
* @param string $key
* @param string $value
* @param string $adminUser
* @param string $adminPassword
* @param string $xRequestId
* @param int|null $ocsApiVersion
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function editUser(
?string $baseUrl,
string $user,
string $key,
string $value,
string $adminUser,
string $adminPassword,
string $xRequestId = '',
?int $ocsApiVersion = 2
): ResponseInterface {
return OcsApiHelper::sendRequest(
$baseUrl,
$adminUser,
$adminPassword,
"PUT",
"/cloud/users/" . $user,
$xRequestId,
["key" => $key, "value" => $value],
$ocsApiVersion
);
}
/**
* Send batch requests to edit the user.
* This will send multiple requests in parallel to the server which will be faster in comparison to waiting for each request to complete.
*
* @param string|null $baseUrl
* @param array|null $editData ['user' => '', 'key' => '', 'value' => '']
* @param string|null $adminUser
* @param string|null $adminPassword
* @param string|null $xRequestId
* @param int|null $ocsApiVersion
*
* @return array
* @throws Exception
*/
public static function editUserBatch(
?string $baseUrl,
?array $editData,
?string $adminUser,
?string $adminPassword,
?string $xRequestId = '',
?int $ocsApiVersion = 2
): array {
$requests = [];
$client = HttpRequestHelper::createClient(
$adminUser,
$adminPassword
);
foreach ($editData as $data) {
$path = "/cloud/users/" . $data['user'];
$body = ["key" => $data['key'], 'value' => $data["value"]];
// Create the OCS API requests and push them to an array.
$requests[] = OcsApiHelper::createOcsRequest(
$baseUrl,
'PUT',
$path,
$xRequestId,
$body
);
}
// Send the array of requests at once in parallel.
$results = HttpRequestHelper::sendBatchRequest($requests, $client);
foreach ($results as $e) {
if ($e instanceof ClientException) {
$httpStatusCode = $e->getResponse()->getStatusCode();
$reasonPhrase = $e->getResponse()->getReasonPhrase();
throw new Exception(
"Unexpected failure when editing a user: HTTP status $httpStatusCode HTTP reason $reasonPhrase"
);
}
}
return $results;
}
/**
*
* @param string|null $baseUrl
* @param string|null $userName
* @param string|null $adminUser
* @param string|null $adminPassword
* @param string|null $xRequestId
* @param int|null $ocsApiVersion
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function getUser(
?string $baseUrl,
?string $userName,
?string $adminUser,
?string $adminPassword,
?string $xRequestId = '',
?int $ocsApiVersion = 2
): ResponseInterface {
return OcsApiHelper::sendRequest(
$baseUrl,
$adminUser,
$adminPassword,
"GET",
"/cloud/users/" . $userName,
$xRequestId,
[],
$ocsApiVersion
);
}
/**
*
* @param string|null $baseUrl
* @param string|null $userName
* @param string|null $adminUser
* @param string|null $adminPassword
* @param string|null $xRequestId
* @param int|null $ocsApiVersion
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function deleteUser(
?string $baseUrl,
?string $userName,
?string $adminUser,
?string $adminPassword,
?string $xRequestId = '',
?int $ocsApiVersion = 2
): ResponseInterface {
return OcsApiHelper::sendRequest(
$baseUrl,
$adminUser,
$adminPassword,
"DELETE",
"/cloud/users/" . $userName,
$xRequestId,
[],
$ocsApiVersion
);
}
/**
*
* @param string|null $baseUrl
* @param string|null $user
* @param string|null $group
* @param string|null $adminUser
* @param string|null $adminPassword
* @param string|null $xRequestId
* @param int|null $ocsApiVersion (1|2)
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function addUserToGroup(
?string $baseUrl,
?string $user,
?string $group,
?string $adminUser,
?string $adminPassword,
?string $xRequestId = '',
?int $ocsApiVersion = 2
): ResponseInterface {
return OcsApiHelper::sendRequest(
$baseUrl,
$adminUser,
$adminPassword,
"POST",
"/cloud/users/" . $user . "/groups",
$xRequestId,
['groupid' => $group],
$ocsApiVersion
);
}
/**
*
* @param string|null $baseUrl
* @param string|null $user
* @param string|null $group
* @param string|null $adminUser
* @param string|null $adminPassword
* @param string|null $xRequestId
* @param int|null $ocsApiVersion (1|2)
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function removeUserFromGroup(
?string $baseUrl,
?string $user,
?string $group,
?string $adminUser,
?string $adminPassword,
?string $xRequestId,
?int $ocsApiVersion = 2
): ResponseInterface {
return OcsApiHelper::sendRequest(
$baseUrl,
$adminUser,
$adminPassword,
"DELETE",
"/cloud/users/" . $user . "/groups",
$xRequestId,
['groupid' => $group],
$ocsApiVersion
);
}
/**
*
* @param string|null $baseUrl
* @param string|null $adminUser
* @param string|null $adminPassword
* @param string|null $xRequestId
* @param string|null $search
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function getGroups(
?string $baseUrl,
?string $adminUser,
?string $adminPassword,
?string $xRequestId = '',
?string $search =""
): ResponseInterface {
return OcsApiHelper::sendRequest(
$baseUrl,
$adminUser,
$adminPassword,
"GET",
"/cloud/groups?search=" . $search,
$xRequestId
);
}
/**
*
* @param string|null $baseUrl
* @param string|null $adminUser
* @param string|null $adminPassword
* @param string|null $xRequestId
* @param string|null $search
*
* @return string[]
* @throws Exception|GuzzleException
*/
public static function getGroupsAsArray(
?string $baseUrl,
?string $adminUser,
?string $adminPassword,
?string $xRequestId = '',
?string $search = ""
): array {
$result = self::getGroups(
$baseUrl,
$adminUser,
$adminPassword,
$xRequestId,
$search
);
$groups = HttpRequestHelper::getResponseXml($result, __METHOD__)->xpath(".//groups")[0];
$return = [];
foreach ($groups as $group) {
$return[] = $group->__toString();
}
return $return;
}
}
@@ -0,0 +1,967 @@
<?php declare(strict_types=1);
/**
* @author Artur Neumann <artur@jankaritech.com>
* @copyright Copyright (c) 2017 Artur Neumann artur@jankaritech.com
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
use Exception;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use InvalidArgumentException;
use PHPUnit\Framework\Assert;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use DateTime;
/**
* Helper to make WebDav Requests
*
* @author Artur Neumann <artur@jankaritech.com>
*
*/
class WebDavHelper {
public const DAV_VERSION_OLD = 1;
public const DAV_VERSION_NEW = 2;
public const DAV_VERSION_SPACES = 3;
/**
* @var array of users with their different space ids
*/
public static array $spacesIdRef = [];
/**
* @return bool
*/
public static function withRemotePhp(): bool {
// use remote.php by default
return \getenv("WITH_REMOTE_PHP") !== "false";
}
/**
* @param string $urlPath
*
* @return string
*/
public static function prefixRemotePhp(string $urlPath): string {
if (self::withRemotePhp()) {
return "remote.php/$urlPath";
}
return $urlPath;
}
/**
* @param string $url
*
* @return bool
*/
public static function isDAVRequest(string $url): bool {
$found = \preg_match("/(\bwebdav\b|\bdav\b)/", $url);
return (bool)$found;
}
/**
* clear space id reference for user
*
* @param string|null $user
*
* @return void
* @throws Exception
*/
public static function removeSpaceIdReferenceForUser(
?string $user
): void {
if (\array_key_exists($user, self::$spacesIdRef)) {
unset(self::$spacesIdRef[$user]);
}
}
/**
* @param string $namespaceString
*
* @return object
*/
public static function parseNamespace(string $namespaceString): object {
//calculate the namespace prefix and namespace
$matches = [];
\preg_match("/^(.*)='(.*)'$/", $namespaceString, $matches);
return (object)["namespace" => $matches[2], "prefix" => $matches[1]];
}
/**
* returns the id of a file
*
* @param string|null $baseUrl
* @param string|null $user
* @param string|null $password
* @param string|null $path
* @param string|null $spaceId
* @param string|null $xRequestId
* @param int|null $davPathVersionToUse
*
* @return string
* @throws Exception|GuzzleException
*/
public static function getFileIdForPath(
?string $baseUrl,
?string $user,
?string $password,
?string $path,
?string $spaceId = null,
?string $xRequestId = '',
?int $davPathVersionToUse = self::DAV_VERSION_NEW
): string {
$body
= '<?xml version="1.0"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:prop>
<oc:fileid />
</d:prop>
</d:propfind>';
$response = self::makeDavRequest(
$baseUrl,
$user,
$password,
"PROPFIND",
$path,
null,
$spaceId,
$xRequestId,
$body,
$davPathVersionToUse
);
\preg_match(
'/\<oc:fileid\>([^\<]*)\<\/oc:fileid\>/',
$response->getBody()->getContents(),
$matches
);
if (!isset($matches[1])) {
throw new Exception("could not find fileId of $path");
}
return $matches[1];
}
/**
* returns body for propfind
*
* @param array|null $properties
*
* @return string
* @throws Exception
*/
public static function getBodyForPropfind(?array $properties): string {
$propertyBody = "";
$extraNamespaces = "";
foreach ($properties as $namespaceString => $property) {
if (\is_int($namespaceString)) {
//default namespace prefix if the property has no array key
//also used if no prefix is given in the property value
$namespacePrefix = null;
} else {
$ns = self::parseNamespace($namespaceString);
$namespacePrefix = $ns->prefix;
$extraNamespaces .= " xmlns:$namespacePrefix=\"$ns->namespace\" ";
}
//if a namespace prefix is given in the property value use that
if (\strpos($property, ":") !== false) {
$propertyParts = \explode(":", $property);
$namespacePrefix = $propertyParts[0];
$property = $propertyParts[1];
}
if ($namespacePrefix) {
$propertyBody .= "<$namespacePrefix:$property/>";
} else {
$propertyBody .= "<$property/>";
}
}
$body = "<?xml version=\"1.0\"?>
<d:propfind
xmlns:d=\"DAV:\"
xmlns:oc=\"http://owncloud.org/ns\"
xmlns:ocs=\"http://open-collaboration-services.org/ns\"
$extraNamespaces>
<d:prop>$propertyBody</d:prop>
</d:propfind>";
return $body;
}
/**
* sends a PROPFIND request
* with these registered namespaces:
* | prefix | namespace |
* | d | DAV: |
* | oc | http://owncloud.org/ns |
* | ocs | http://open-collaboration-services.org/ns |
*
* @param string|null $baseUrl
* @param string|null $user
* @param string|null $password
* @param string|null $path
* @param string[] $properties
* string can contain namespace prefix,
* if no prefix is given 'd:' is used as prefix
* if an associative array is used, then the key will be used as namespace
* @param string|null $xRequestId
* @param string|null $folderDepth
* @param string|null $spaceId
* @param string|null $type
* @param int|null $davPathVersionToUse
* @param string|null $doDavRequestAsUser
* @param array|null $headers
*
* @return ResponseInterface
* @throws Exception
* @throws GuzzleException
*/
public static function propfind(
?string $baseUrl,
?string $user,
?string $password,
?string $path,
?array $properties,
?string $xRequestId = '',
?string $folderDepth = '1',
?string $spaceId = null,
?string $type = "files",
?int $davPathVersionToUse = self::DAV_VERSION_NEW,
?string $doDavRequestAsUser = null,
?array $headers = []
): ResponseInterface {
$body = self::getBodyForPropfind($properties);
$folderDepth = (string) $folderDepth;
if ($folderDepth !== '0' && $folderDepth !== '1' && $folderDepth !== 'infinity') {
if ($folderDepth !== '') {
throw new InvalidArgumentException('Invalid depth value ' . $folderDepth);
}
$folderDepth = '1'; // КуСфера server's default value
}
$headers['Depth'] = $folderDepth;
return self::makeDavRequest(
$baseUrl,
$user,
$password,
"PROPFIND",
$path,
$headers,
$spaceId,
$xRequestId,
$body,
$davPathVersionToUse,
$type,
null,
null,
false,
null,
null,
[],
$doDavRequestAsUser
);
}
/**
*
* @param string|null $baseUrl
* @param string|null $user
* @param string|null $password
* @param string|null $path
* @param string|null $propertyName
* @param string|null $propertyValue
* @param string|null $xRequestId
* @param string|null $namespaceString string containing prefix and namespace
* e.g "x1='http://whatever.org/ns'"
* @param int|null $davPathVersionToUse
* @param string|null $type
* @param string|null $spaceId
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function proppatch(
?string $baseUrl,
?string $user,
?string $password,
?string $path,
?string $propertyName,
?string $propertyValue,
?string $xRequestId = '',
?string $namespaceString = null,
?int $davPathVersionToUse = self::DAV_VERSION_NEW,
?string $type="files",
?string $spaceId = null,
): ResponseInterface {
if ($namespaceString !== null) {
$ns = self::parseNamespace($namespaceString);
$propertyBody = "<$ns->prefix:$propertyName" .
" xmlns:$ns->prefix=\"$ns->namespace\">" .
"$propertyValue" .
"</$ns->prefix:$propertyName>";
} else {
$propertyBody = "<$propertyName>$propertyValue</$propertyName>";
}
$body = "<?xml version=\"1.0\"?>
<d:propertyupdate xmlns:d=\"DAV:\"
xmlns:oc=\"http://owncloud.org/ns\">
<d:set>
<d:prop>$propertyBody</d:prop>
</d:set>
</d:propertyupdate>";
return self::makeDavRequest(
$baseUrl,
$user,
$password,
"PROPPATCH",
$path,
[],
$spaceId,
$xRequestId,
$body,
$davPathVersionToUse,
$type
);
}
/**
* gets namespace-prefix, namespace url and propName from provided namespaceString or property
* or otherwise use default
*
* @param string $namespaceString
* @param string $property
*
* @return array
*/
public static function getPropertyWithNamespaceInfo(string $namespaceString = "", string $property = ""): array {
$namespace = "";
$namespacePrefix = "";
if (\is_int($namespaceString)) {
//default namespace prefix if the property has no array key
//also used if no prefix is given in the property value
$namespacePrefix = "d";
$namespace = "DAV:";
} elseif ($namespaceString) {
$ns = self::parseNamespace($namespaceString);
$namespacePrefix = $ns->prefix;
$namespace = $ns->namespace;
}
//if a namespace prefix is given in the property value use that
if ($property && \strpos($property, ":")) {
$propertyParts = \explode(":", $property);
$namespacePrefix = $propertyParts[0];
$property = $propertyParts[1];
}
return [$namespacePrefix, $namespace, $property];
}
/**
* sends HTTP request PROPPATCH method with multiple properties
*
* @param string|null $baseUrl
* @param string|null $user
* @param string|null $password
* @param string $path
* @param array|null $propertiesArray
* @param string|null $xRequestId
* @param int|null $davPathVersion
* @param string|null $namespaceString
* @param string|null $type
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function proppatchWithMultipleProps(
?string $baseUrl,
?string $user,
?string $password,
string $path,
?array $propertiesArray,
?string $xRequestId = '',
?int $davPathVersion = null,
?string $namespaceString = null,
?string $type="files"
): ResponseInterface {
$propertyBody = "";
foreach ($propertiesArray as $propertyArray) {
$property = $propertyArray["propertyName"];
$value = $propertyArray["propertyValue"];
if ($namespaceString !== null) {
$matches = [];
[$namespacePrefix, $namespace, $property] = self::getPropertyWithNamespaceInfo(
$namespaceString,
$property
);
$propertyBody .= "\n\t<$namespacePrefix:$property>" .
"$value" .
"</$namespacePrefix:$property>";
} else {
$propertyBody .= "<$property>$value</$property>";
}
}
$body = "<?xml version=\"1.0\"?>
<d:propertyupdate xmlns:d=\"DAV:\"
xmlns:oc=\"http://owncloud.org/ns\">
<d:set>
<d:prop>$propertyBody
</d:prop>
</d:set>
</d:propertyupdate>";
return self::makeDavRequest(
$baseUrl,
$user,
$password,
"PROPPATCH",
$path,
[],
null,
$xRequestId,
$body,
$davPathVersion,
$type
);
}
/**
* returns the response to listing a folder (collection)
*
* @param string|null $baseUrl
* @param string|null $user
* @param string|null $password
* @param string|null $path
* @param string|null $folderDepth
* @param string|null $spaceId
* @param string|null $xRequestId
* @param string[] $properties
* @param string|null $type
* @param int|null $davPathVersionToUse
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function listFolder(
?string $baseUrl,
?string $user,
?string $password,
?string $path,
?string $folderDepth,
?string $spaceId = null,
?string $xRequestId = '',
?array $properties = null,
?string $type = "files",
?int $davPathVersionToUse = self::DAV_VERSION_NEW
): ResponseInterface {
if (!$properties) {
$properties = [
'd:getetag', 'd:resourcetype'
];
}
return self::propfind(
$baseUrl,
$user,
$password,
$path,
$properties,
$xRequestId,
$folderDepth,
$spaceId,
$type,
$davPathVersionToUse
);
}
/**
* Generates UUIDv4
* Example: 123e4567-e89b-12d3-a456-426614174000
*
* @return string
* @throws Exception
*/
public static function generateUUIDv4(): string {
// generate 16 bytes (128 bits) of random data or use the data passed into the function.
$data = random_bytes(16);
\assert(\strlen($data) == 16);
$data[6] = \chr(\ord($data[6]) & 0x0f | 0x40); // set version to 0100
$data[8] = \chr(\ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
/**
* fetches personal space id for provided user
*
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $xRequestId
*
* @return string
* @throws GuzzleException
* @throws Exception
*/
public static function getPersonalSpaceIdForUser(
string $baseUrl,
string $user,
string $password,
string $xRequestId
): string {
if (\array_key_exists($user, self::$spacesIdRef) && \array_key_exists("personal", self::$spacesIdRef[$user])) {
return self::$spacesIdRef[$user]["personal"];
}
$personalSpaceId = '';
if (!OcHelper::isTestingOnReva()) {
$response = GraphHelper::getMySpaces($baseUrl, $user, $password, '', $xRequestId);
Assert::assertEquals(200, $response->getStatusCode(), "Cannot list drives for user '$user'");
$drives = HttpRequestHelper::getJsonDecodedResponseBodyContent($response);
foreach ($drives->value as $drive) {
if ($drive->driveType === "personal") {
$personalSpaceId = $drive->id;
break;
}
}
}
if (!$personalSpaceId) {
// the graph endpoint did not give a useful answer
// try getting the information from the webdav endpoint
$fullUrl = "$baseUrl/" . self::getDavPath(self::DAV_VERSION_NEW, $user);
$response = HttpRequestHelper::sendRequest(
$fullUrl,
$xRequestId,
'PROPFIND',
$user,
$password
);
Assert::assertEquals(
207,
$response->getStatusCode(),
"PROPFIND for user '$user' failed so the personal space id cannot be discovered"
);
$responseXmlObject = HttpRequestHelper::getResponseXml(
$response,
__METHOD__
);
$xmlPart = $responseXmlObject->xpath("/d:multistatus/d:response[1]/d:propstat/d:prop/oc:spaceid");
Assert::assertNotEmpty(
$xmlPart,
"The 'oc:spaceid' for user '$user' was not found in the PROPFIND response"
);
$personalSpaceId = $xmlPart[0]->__toString();
}
Assert::assertNotEmpty($personalSpaceId, "The personal space id for user '$user' was not found");
self::$spacesIdRef[$user] = [];
self::$spacesIdRef[$user]["personal"] = $personalSpaceId;
return $personalSpaceId;
}
/**
* First checks if a user exists to return its space ID
* In case of any exception, it returns a fake space ID
*
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $xRequestId
*
* @return string
* @throws Exception|GuzzleException
*/
public static function getPersonalSpaceIdForUserOrFakeIfNotFound(
string $baseUrl,
string $user,
string $password,
string $xRequestId
): string {
if (\str_starts_with($user, "non-exist") || \str_starts_with($user, "nonexist")) {
return self::generateUUIDv4();
}
return self::getPersonalSpaceIdForUser(
$baseUrl,
$user,
$password,
$xRequestId,
);
}
/**
* sends a DAV request
*
* @param string|null $baseUrl URL of КуСфера e.g. http://localhost:8080
* @param string|null $user
* @param string|null $password or token when bearer auth is used
* @param string|null $method PUT, GET, DELETE, etc.
* @param string|null $path
* @param array|null $headers
* @param string|null $spaceId
* @param string|null $xRequestId
* @param string|null|resource|StreamInterface $body
* @param int|null $davPathVersionToUse (1|2|3)
* @param string|null $type of request
* @param string|null $sourceIpAddress to initiate the request from
* @param string|null $authType basic|bearer
* @param bool $stream Set to true to stream a response rather
* than download it all up-front.
* @param int|null $timeout
* @param Client|null $client
* @param array|null $urlParameter to concatenate with path
* @param string|null $doDavRequestAsUser run the DAV as this user, if null it is the same as $user
* @param bool $isGivenStep is set to true if makeDavRequest is called from a "given" step
*
* @return ResponseInterface
* @throws GuzzleException
* @throws Exception
*/
public static function makeDavRequest(
?string $baseUrl,
?string $user,
?string $password,
?string $method,
?string $path,
?array $headers,
?string $spaceId = null,
?string $xRequestId = '',
$body = null,
?int $davPathVersionToUse = self::DAV_VERSION_OLD,
?string $type = "files",
?string $sourceIpAddress = null,
?string $authType = "basic",
?bool $stream = false,
?int $timeout = 0,
?Client $client = null,
?array $urlParameter = [],
?string $doDavRequestAsUser = null,
?bool $isGivenStep = false,
): ResponseInterface {
$baseUrl = self::sanitizeUrl($baseUrl, true);
// We need to manipulate and use path as a string.
// So ensure that it is a string to avoid any type-conversion errors.
if ($path === null) {
$path = "";
}
// get space id if testing with spaces dav
if ($spaceId === null && $davPathVersionToUse === self::DAV_VERSION_SPACES
&& !\in_array($type, ["public-files", "versions"])
) {
$path = \ltrim($path, "/");
if (\str_starts_with($path, "Shares/")) {
$spaceId = GraphHelper::SHARES_SPACE_ID;
$path = "/" . preg_replace("/^Shares\//", "", $path);
} else {
$spaceId = self::getPersonalSpaceIdForUserOrFakeIfNotFound(
$baseUrl,
$user,
$password,
$xRequestId
);
}
}
$suffixPath = $user;
if ($davPathVersionToUse === self::DAV_VERSION_SPACES
&& !\in_array($type, ["archive", "versions", "public-files"])
) {
$suffixPath = $spaceId;
} elseif ($type === "versions") {
// $path is file-id in case of versions
$suffixPath = $path;
}
$davPath = self::getDavPath($davPathVersionToUse, $suffixPath, $type);
//replace %, # and ? and in the path, Guzzle will not encode them
$urlSpecialChar = [['%', '#', '?'], ['%25', '%23', '%3F']];
$path = \str_replace($urlSpecialChar[0], $urlSpecialChar[1], $path);
if (!empty($urlParameter)) {
$urlParameter = \http_build_query($urlParameter, '', '&');
$path .= '?' . $urlParameter;
}
$fullUrl = self::sanitizeUrl("$baseUrl/$davPath");
// NOTE: no need to append path for archive and versions endpoints
if (!\in_array($type, ["archive", "versions"])) {
$fullUrl .= "/" . \ltrim($path, "/");
}
if ($authType === 'bearer') {
$headers['Authorization'] = 'Bearer ' . $password;
$user = null;
$password = null;
}
if ($type === "public-files") {
if ($password === null || $password === "") {
$user = null;
} else {
$user = "public";
}
}
$config = null;
if ($sourceIpAddress !== null) {
$config = [ 'curl' => [ CURLOPT_INTERFACE => $sourceIpAddress ]];
}
if ($headers !== null) {
foreach ($headers as $key => $value) {
//? and # need to be encoded in the Destination URL
if ($key === "Destination") {
$headers[$key] = \str_replace(
$urlSpecialChar[0],
$urlSpecialChar[1],
$value
);
break;
}
}
}
return HttpRequestHelper::sendRequest(
$fullUrl,
$xRequestId,
$method,
$doDavRequestAsUser ?? $user,
$password,
$headers,
$body,
$config,
null,
$stream,
$timeout,
$client,
$isGivenStep
);
}
/**
* get the dav path
*
* @param int $davPathVersion (1|2|3)
* @param string|null $userOrItemIdOrSpaceIdOrToken 'user' or 'file-id' or 'space-id' or 'public-token'
* @param string|null $type
*
* @return string
*/
public static function getDavPath(
int $davPathVersion,
?string $userOrItemIdOrSpaceIdOrToken = null,
?string $type = "files"
): string {
switch ($type) {
case 'archive':
return self::prefixRemotePhp("dav/archive/$userOrItemIdOrSpaceIdOrToken/files");
case 'versions':
return self::prefixRemotePhp("dav/meta/$userOrItemIdOrSpaceIdOrToken/v");
case 'comments':
return self::prefixRemotePhp("dav/comments/files");
default:
break;
}
if ($davPathVersion === self::DAV_VERSION_SPACES) {
if ($type === "trash-bin") {
if ($userOrItemIdOrSpaceIdOrToken === null) {
throw new InvalidArgumentException("Space ID is required for trash-bin endpoint");
}
return self::prefixRemotePhp("dav/spaces/trash-bin/$userOrItemIdOrSpaceIdOrToken");
} elseif ($type === "public-files") {
// spaces DAV path doesn't have own public-files endpoint
return self::prefixRemotePhp("dav/public-files/$userOrItemIdOrSpaceIdOrToken");
}
// return spaces root path if spaceid is null
// REPORT request uses spaces root path
if ($userOrItemIdOrSpaceIdOrToken === null) {
return self::prefixRemotePhp("dav/spaces");
}
return self::prefixRemotePhp("dav/spaces/$userOrItemIdOrSpaceIdOrToken");
} else {
if ($type === "trash-bin") {
// Since there is no trash bin endpoint for old dav version,
// new dav version's endpoint is used here.
return self::prefixRemotePhp("dav/trash-bin/$userOrItemIdOrSpaceIdOrToken");
}
if ($davPathVersion === self::DAV_VERSION_OLD) {
return self::prefixRemotePhp("webdav");
} elseif ($davPathVersion === self::DAV_VERSION_NEW) {
if ($type === "files") {
return self::prefixRemotePhp("dav/files/$userOrItemIdOrSpaceIdOrToken");
} elseif ($type === "public-files") {
return self::prefixRemotePhp("dav/public-files/$userOrItemIdOrSpaceIdOrToken");
}
return self::prefixRemotePhp("dav");
}
throw new InvalidArgumentException("Invalid DAV path: $davPathVersion");
}
}
/**
* make sure there are no double-slashes in the URL
*
* @param string|null $url
* @param bool|null $trailingSlash forces a trailing slash
*
* @return string
*/
public static function sanitizeUrl(?string $url, ?bool $trailingSlash = false): string {
if ($trailingSlash === true) {
$url = $url . "/";
} else {
$url = \rtrim($url, "/");
}
return \preg_replace("/([^:]\/)\/+/", '$1', $url);
}
/**
* get Mtime of File in a public link share
*
* @param string|null $baseUrl
* @param string|null $fileName
* @param string|null $token
* @param string|null $xRequestId
* @param int|null $davVersionToUse
*
* @return string
* @throws Exception|GuzzleException
*/
public static function getMtimeOfFileInPublicLinkShare(
?string $baseUrl,
?string $fileName,
?string $token,
?string $xRequestId = '',
?int $davVersionToUse = self::DAV_VERSION_NEW
): string {
$response = self::propfind(
$baseUrl,
null,
null,
"$token/$fileName",
['d:getlastmodified'],
$xRequestId,
'1',
null,
"public-files",
$davVersionToUse
);
$responseXmlObject = HttpRequestHelper::getResponseXml(
$response,
__METHOD__
);
$xmlPart = $responseXmlObject->xpath("//d:getlastmodified");
return $xmlPart[0]->__toString();
}
/**
* get Mtime of a resource
*
* @param string|null $user
* @param string|null $password
* @param string|null $baseUrl
* @param string|null $resource
* @param string|null $xRequestId
* @param int|null $davPathVersionToUse
* @param string|null $spaceId
*
* @return string
* @throws Exception
* @throws GuzzleException
*/
public static function getMtimeOfResource(
?string $user,
?string $password,
?string $baseUrl,
?string $resource,
?string $xRequestId = '',
?int $davPathVersionToUse = self::DAV_VERSION_NEW,
?string $spaceId = null,
): string {
$response = self::propfind(
$baseUrl,
$user,
$password,
$resource,
["d:getlastmodified"],
$xRequestId,
"0",
$spaceId,
"files",
$davPathVersionToUse
);
$responseXmlObject = HttpRequestHelper::getResponseXml(
$response,
__METHOD__
);
$xmlPart = $responseXmlObject->xpath("//d:getlastmodified");
Assert::assertArrayHasKey(
0,
$xmlPart,
__METHOD__
. " XML part does not have key 0. Expected a value at index 0 of 'xmlPart' but, found: "
. json_encode($xmlPart)
);
$mtime = new DateTime($xmlPart[0]->__toString());
return $mtime->format('U');
}
/**
* wait until the reqeust doesn't return 425 anymore
*
* @param string $url
* @param ?string $user
* @param ?string $password
* @param ?array $headers
*
* @return void
*/
public static function waitForPostProcessingToFinish(
string $url,
?string $user = null,
?string $password = null,
?array $headers = [],
): void {
$retried = 0;
do {
$response = HttpRequestHelper::sendRequest(
$url,
'check-425-status',
'GET',
$user,
$password,
$headers,
);
$statusCode = $response->getStatusCode();
if ($statusCode !== 425) {
return;
}
$tryAgain = $retried < HttpRequestHelper::numRetriesOnHttpTooEarly();
if ($tryAgain) {
$retried += 1;
echo "[INFO] Waiting for post processing to finish, attempt ($retried)...\n";
// wait 1s and try again
\sleep(1);
}
} while ($tryAgain);
echo "[ERROR] 10 seconds timeout! Post processing did not finish in time.\n";
}
}