reorganize test folders within the acceptance directory

Signed-off-by: prashant-gurung899 <prasantgrg777@gmail.com>
This commit is contained in:
prashant-gurung899
2024-08-28 14:54:45 +05:45
parent 90f66d707d
commit d8d7b700cd
51 changed files with 13 additions and 13 deletions
@@ -0,0 +1,92 @@
<?php declare(strict_types=1);
/**
* ownCloud
*
* @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 $responseXml
* @param string|null $extraErrorText
*
* @return void
*/
public static function assertDavResponseElementIs(
?string $element,
?string $expectedValue,
?array $responseXml,
?string $extraErrorText = ''
):void {
if ($extraErrorText !== '') {
$extraErrorText = $extraErrorText . " ";
}
self::assertArrayHasKey(
'value',
$responseXml,
$extraErrorText . "responseXml does not have key 'value'"
);
if ($element === "exception") {
$result = $responseXml['value'][0]['value'];
} elseif ($element === "message") {
$result = $responseXml['value'][1]['value'];
} elseif ($element === "reason") {
$result = $responseXml['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,45 @@
<?php declare(strict_types=1);
/**
* ownCloud
*
* @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\ConnectException;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Request;
use Psr\Http\Message\ResponseInterface;
use TestHelpers\OcisConfigHelper;
/**
* A helper class for running oCIS CLI commands
*/
class CliHelper {
/**
* @param array $body
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function runCommand(array $body): ResponseInterface {
$url = OcisConfigHelper::getWrapperUrl() . "/command";
return OcisConfigHelper::sendRequest($url, "POST", \json_encode($body));
}
}
@@ -0,0 +1,173 @@
<?php declare(strict_types=1);
/**
* ownCloud
*
* @author Prajwol Amatya <prajwol@jankaritech.com>
* @copyright Copyright (c) 2023 Prajwol Amatya prajwol@jankaritech.com
*/
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 according to the provided email address and the serial number
* For email number, 1 means the latest one
*
* @param string $emailAddress
* @param string|null $xRequestId
* @param int|null $emailNumber For email number, 1 means the latest one
* @param int|null $waitTimeSec Time to wait for the email if the email has been delivered
*
* @return string
* @throws GuzzleException
* @throws Exception
*/
public static function getBodyOfLastEmail(
string $emailAddress,
string $xRequestId,
?int $emailNumber = 1,
?int $waitTimeSec = EMAIL_WAIT_TIMEOUT_SEC
):string {
$currentTime = \time();
$endTime = $currentTime + $waitTimeSec;
$mailBox = self::getMailBoxFromEmail($emailAddress);
while ($currentTime <= $endTime) {
$mailboxResponse = self::getMailboxInformation($mailBox, $xRequestId);
if (!empty($mailboxResponse) && \sizeof($mailboxResponse) >= $emailNumber) {
$mailboxId = $mailboxResponse[\sizeof($mailboxResponse) - $emailNumber]->id;
$response = self::getBodyOfAnEmailById($mailBox, $mailboxId, $xRequestId);
$body = \str_replace(
"\r\n",
"\n",
\quoted_printable_decode($response->body->text . "\n" . $response->body->html)
);
return $body;
}
\usleep(STANDARD_SLEEP_TIME_MICROSEC * 50);
$currentTime = \time();
}
throw new Exception("Could not find the email to the address: " . $emailAddress);
}
/**
* Deletes all the emails for the provided mailbox
*
* @param string $localInbucketUrl
* @param string|null $xRequestId
* @param string $mailBox
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function deleteAllEmailsForAMailbox(
string $localInbucketUrl,
?string $xRequestId,
string $mailBox
):ResponseInterface {
return HttpRequestHelper::delete(
$localInbucketUrl . "/api/v1/mailbox/" . $mailBox,
$xRequestId
);
}
}
File diff suppressed because it is too large Load Diff
+126
View File
@@ -0,0 +1,126 @@
<?php declare(strict_types=1);
/**
* ownCloud
*
* @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\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\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);
}
}
@@ -0,0 +1,699 @@
<?php declare(strict_types=1);
/**
* ownCloud
*
* @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;
/**
* Helper for HTTP requests
*/
class HttpRequestHelper {
public const HTTP_TOO_EARLY = 425;
public const HTTP_CONFLICT = 409;
private static ?string $oCSelectorCookie = null;
/**
* @return string
*/
public static function getOCSelectorCookie(): string {
return self::$oCSelectorCookie;
}
/**
* @param string $oCSelectorCookie "owncloud-selector=oc10;path=/;"
*
* @return void
*/
public static function setOCSelectorCookie(string $oCSelectorCookie): void {
self::$oCSelectorCookie = $oCSelectorCookie;
}
/**
* 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 oCIS may return HTTP_TOO_EARLY
// So try up to 10 times before giving up.
return 10;
}
/**
*
* @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
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function sendRequestOnce(
?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
):ResponseInterface {
if ($client === null) {
$client = self::createClient(
$user,
$password,
$config,
$cookies,
$stream,
$timeout
);
}
$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);
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.
self::debugResponse($response);
\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
*
* @return Client
*/
public static function createClient(
?string $user = null,
?string $password = null,
?array $config = null,
?CookieJar $cookies = null,
?bool $stream = false,
?int $timeout = 0
):Client {
$options = [];
if ($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';
}
if (OcisHelper::isTestingParallelDeployment()) {
// oCIS cannot handle '/apps/testing' endpoints
// so those requests must be redirected to oC10 server
// change server to oC10 if the request url has `/apps/testing`
if (strpos($url, "/apps/testing") !== false) {
$oCISServerUrl = \getenv('TEST_SERVER_URL');
$oC10ServerUrl = \getenv('TEST_OC10_URL');
$url = str_replace($oCISServerUrl, $oC10ServerUrl, $url);
} else {
// set 'owncloud-server' selector cookie for oCIS requests
$headers['Cookie'] = self::getOCSelectorCookie();
}
}
$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 ?: 60;
}
/**
* 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);
}
}
@@ -0,0 +1,119 @@
<?php declare(strict_types=1);
/**
* ownCloud
*
* @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 oCIS server
*/
class OcisConfigHelper {
/**
* @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 ociswrapper at the moment, make sure that ociswrapper 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("OCIS_WRAPPER_URL");
if ($url === false) {
$url = "http://localhost:5200";
}
return $url;
}
/**
* @param array $envs
*
* @return ResponseInterface
* @throws GuzzleException
*/
public static function reConfigureOcis(array $envs): ResponseInterface {
$url = self::getWrapperUrl() . "/config";
return self::sendRequest($url, "PUT", \json_encode($envs));
}
/**
* @return ResponseInterface
* @throws GuzzleException
*/
public static function rollbackOcis(): ResponseInterface {
$url = self::getWrapperUrl() . "/rollback";
return self::sendRequest($url, "DELETE");
}
/**
* @return ResponseInterface
* @throws GuzzleException
*/
public static function stopOcis(): ResponseInterface {
$url = self::getWrapperUrl() . "/stop";
return self::sendRequest($url, "POST");
}
/**
* @return ResponseInterface
* @throws GuzzleException
*/
public static function startOcis(): ResponseInterface {
$url = self::getWrapperUrl() . "/start";
return self::sendRequest($url, "POST");
}
}
+336
View File
@@ -0,0 +1,336 @@
<?php declare(strict_types=1);
/**
* ownCloud
*
* @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 OcisHelper
*
* Helper functions that are needed to run tests on OCIS
*
* @package TestHelpers
*/
class OcisHelper {
/**
* @return bool
*/
public static function isTestingOnReva():bool {
return (\getenv("TEST_REVA") === "true");
}
/**
* @return bool
*/
public static function isTestingParallelDeployment(): bool {
return (\getenv("TEST_PARALLEL_DEPLOYMENT") === "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 "OWNCLOUD";
}
$storageDriver = \strtoupper($storageDriver);
if ($storageDriver !== "OCIS" && $storageDriver !== "EOS" && $storageDriver !== "OWNCLOUD" && $storageDriver !== "S3NG" && $storageDriver !== "POSIX") {
throw new Exception(
"Invalid storage driver. " .
"STORAGE_DRIVER must be OCIS|EOS|OWNCLOUD|S3NG|POSIX"
);
}
return $storageDriver;
}
/**
* @param string|null $user
*
* @return void
* @throws Exception
*/
public static function deleteRevaUserData(?string $user = ""):void {
$deleteCmd = self::getDeleteUserDataCommand();
if ($deleteCmd === false) {
if (self::getStorageDriver() === "OWNCLOUD") {
self::recurseRmdir(self::getOcisRevaDataRoot() . $user);
}
return;
}
if (self::getStorageDriver() === "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);
}
/**
* Helper for Recursive Upload of file/folder
*
* @param string|null $baseUrl
* @param string|null $source
* @param string|null $userId
* @param string|null $password
* @param string|null $xRequestId
* @param string|null $destination
*
* @return void
* @throws Exception|GuzzleException
*/
public static function recurseUpload(
?string $baseUrl,
?string $source,
?string $userId,
?string $password,
?string $xRequestId = '',
?string $destination = ''
):void {
if ($destination !== '') {
$response = WebDavHelper::makeDavRequest(
$baseUrl,
$userId,
$password,
"MKCOL",
$destination,
[],
$xRequestId
);
if ($response->getStatusCode() !== 201) {
throw new Exception("Could not create folder destination" . $response->getBody()->getContents());
}
}
$dir = \opendir($source);
while (($file = \readdir($dir)) !== false) {
if (($file != '.') && ($file != '..')) {
$sourcePath = $source . '/' . $file;
$destinationPath = $destination . '/' . $file;
if (\is_dir($sourcePath)) {
self::recurseUpload(
$baseUrl,
$sourcePath,
$userId,
$password,
$xRequestId,
$destinationPath
);
} else {
$response = UploadHelper::upload(
$baseUrl,
$userId,
$password,
$sourcePath,
$destinationPath,
$xRequestId
);
$responseStatus = $response->getStatusCode();
if ($responseStatus !== 201) {
throw new Exception(
"Could not upload skeleton file $sourcePath to $destinationPath for user '$userId' status '$responseStatus' response body: '"
. $response->getBody()->getContents() . "'"
);
}
}
}
}
\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=owncloud,dc=com";
}
/**
* @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=owncloud,dc=com";
}
/**
* @return string
*/
public static function getBindPassword():string {
$pw = \getenv("REVA_LDAP_BIND_PASSWORD");
return $pw ?: "";
}
/**
* @return string
*/
private static function getOcisRevaDataRoot():string {
$root = \getenv("OCIS_REVA_DATA_ROOT");
if ($root === false || $root === "") {
$root = "/var/tmp/ocis/owncloud/";
}
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
);
}
}
+159
View File
@@ -0,0 +1,159 @@
<?php declare(strict_types=1);
/**
* ownCloud
*
* @author Viktor Scharf <scharf.vi@gmail.com>
* @copyright Copyright (c) 2024 Viktor Scharf <scharf.vi@gmail.com>
*/
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['params'] = [
"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()
);
}
}
@@ -0,0 +1,103 @@
<?php declare(strict_types=1);
/**
* ownCloud
*
* @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,281 @@
<?php declare(strict_types=1);
/**
* ownCloud
*
* @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 ocis 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,459 @@
<?php declare(strict_types=1);
/**
* ownCloud
*
* @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
* @param string|null $ocPath
*
* @return void
*/
public static function init(
?string $adminUsername,
?string $adminPassword,
?string $baseUrl,
?string $ocPath
): 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);
}
/**
* returns the content of a file in a skeleton folder
*
* @param string|null $fileInSkeletonFolder
* @param string|null $xRequestId
* @param string|null $baseUrl
* @param string|null $adminUsername
* @param string|null $adminPassword
*
* @return string content of the file
* @throws GuzzleException
* @throws Exception
*/
public static function readSkeletonFile(
?string $fileInSkeletonFolder,
?string $xRequestId = '',
?string $baseUrl = null,
?string $adminUsername = null,
?string $adminPassword = null
):string {
$baseUrl = self::checkBaseUrl($baseUrl, "readSkeletonFile");
$adminUsername = self::checkAdminUsername(
$adminUsername,
"readSkeletonFile"
);
$adminPassword = self::checkAdminPassword(
$adminPassword,
"readSkeletonFile"
);
$fileInSkeletonFolder = \rawurlencode("/$fileInSkeletonFolder");
$response = OcsApiHelper::sendRequest(
$baseUrl,
$adminUsername,
$adminPassword,
'GET',
"/apps/testing/api/v1/file?file=$fileInSkeletonFolder&absolute=true",
$xRequestId
);
self::assertSame(
200,
$response->getStatusCode(),
"Failed to read the file $fileInSkeletonFolder"
);
$localContent = HttpRequestHelper::getResponseXml($response, __METHOD__);
$localContent = (string)$localContent->data->element->contentUrlEncoded;
return \urldecode($localContent);
}
}
@@ -0,0 +1,270 @@
<?php declare(strict_types=1);
/**
* ownCloud
*
* @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 ownCloud 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,46 @@
<?php declare(strict_types=1);
/**
* ownCloud
*
* @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,361 @@
<?php declare(strict_types=1);
/**
* ownCloud
*
* @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 owncloud
* e.g. http://localhost:8080
* should include the subfolder
* if owncloud runs in a subfolder
* e.g. http://localhost:8080/owncloud-core
* @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 int|null $chunkingVersion (1|2|null)
* if set to null chunking will not be used
* @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,
?int $chunkingVersion = null,
?int $noOfChunks = 1,
?bool $isGivenStep = false
): ResponseInterface {
//simple upload with no chunking
if ($chunkingVersion === null) {
$data = \file_get_contents($source);
return WebDavHelper::makeDavRequest(
$baseUrl,
$user,
$password,
"PUT",
$destination,
$headers,
$xRequestId,
$data,
$davPathVersionToUse,
"files",
null,
"basic",
false,
0,
null,
[],
null,
$isGivenStep
);
} else {
//prepare chunking
$chunks = self::chunkFile($source, $noOfChunks);
$chunkingId = 'chunking-' . \rand(1000, 9999);
$v2ChunksDestination = '/uploads/' . $user . '/' . $chunkingId;
}
$result = null;
//prepare chunking version specific stuff
if ($chunkingVersion === 1) {
$headers['OC-Chunked'] = '1';
} elseif ($chunkingVersion === 2) {
$result = WebDavHelper::makeDavRequest(
$baseUrl,
$user,
$password,
'MKCOL',
$v2ChunksDestination,
$headers,
$xRequestId,
null,
$davPathVersionToUse,
"uploads",
null,
"basic",
false,
0,
null,
[],
null,
$isGivenStep
);
if ($result->getStatusCode() >= 400) {
return $result;
}
}
//upload chunks
foreach ($chunks as $index => $chunk) {
if ($chunkingVersion === 1) {
$filename = $destination . "-" . $chunkingId . "-" .
\count($chunks) . '-' . $index;
$davRequestType = "files";
} else {
// do chunking version 2
$filename = $v2ChunksDestination . '/' . $index;
$davRequestType = "uploads";
}
$result = WebDavHelper::makeDavRequest(
$baseUrl,
$user,
$password,
"PUT",
$filename,
$headers,
$xRequestId,
$chunk,
$davPathVersionToUse,
$davRequestType,
null,
"basic",
false,
0,
null,
[],
null,
$isGivenStep
);
if ($result->getStatusCode() >= 400) {
return $result;
}
}
//finish upload for new chunking
if ($chunkingVersion === 2) {
$source = $v2ChunksDestination . '/.file';
$headers['Destination'] = $baseUrl . "/" .
WebDavHelper::getDavPath($user, $davPathVersionToUse) .
$destination;
$result = WebDavHelper::makeDavRequest(
$baseUrl,
$user,
$password,
'MOVE',
$source,
$headers,
$xRequestId,
null,
$davPathVersionToUse,
"uploads",
null,
"basic",
false,
0,
null,
[],
null,
$isGivenStep
);
if ($result->getStatusCode() >= 400) {
return $result;
}
}
self::assertNotNull($result, __METHOD__ . " chunking version $chunkingVersion was requested but no upload was done.");
return $result;
}
/**
* Upload the same file multiple times with different mechanisms.
*
* @param string|null $baseUrl URL of owncloud
* @param string|null $user user who uploads
* @param string|null $password
* @param string|null $source source file path
* @param string|null $destination destination path on the server
* @param string|null $xRequestId
* @param bool $overwriteMode when false creates separate files to test uploading brand-new files,
* when true it just overwrites the same file over and over again with the same name
* @param string|null $exceptChunkingType empty string or "old" or "new"
*
* @return array of ResponseInterface
* @throws GuzzleException
*/
public static function uploadWithAllMechanisms(
?string $baseUrl,
?string $user,
?string $password,
?string $source,
?string $destination,
?string $xRequestId = '',
?bool $overwriteMode = false,
?string $exceptChunkingType = ''
):array {
$responses = [];
foreach ([1, 2] as $davPathVersion) {
if ($davPathVersion === 1) {
$davHuman = 'old';
} else {
$davHuman = 'new';
}
switch ($exceptChunkingType) {
case 'old':
$exceptChunkingVersion = 1;
break;
case 'new':
$exceptChunkingVersion = 2;
break;
default:
$exceptChunkingVersion = -1;
break;
}
foreach ([null, 1, 2] as $chunkingVersion) {
if ($chunkingVersion === $exceptChunkingVersion) {
continue;
}
$valid = WebDavHelper::isValidDavChunkingCombination(
$davPathVersion,
$chunkingVersion
);
if ($valid === false) {
continue;
}
$finalDestination = $destination;
if (!$overwriteMode && $chunkingVersion !== null) {
$finalDestination .= "-{$davHuman}dav-{$davHuman}chunking";
} elseif (!$overwriteMode && $chunkingVersion === null) {
$finalDestination .= "-{$davHuman}dav-regular";
}
$responses[] = self::upload(
$baseUrl,
$user,
$password,
$source,
$finalDestination,
$xRequestId,
[],
$davPathVersion,
$chunkingVersion,
2
);
}
}
return $responses;
}
/**
* 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 a file from FilesForUpload directory
*
* @param string|null $name name of the file to upload
*
* @return string
*/
public static function getUploadFilesDir(?string $name):string {
return \getenv("FILES_FOR_UPLOAD") . $name;
}
}
+319
View File
@@ -0,0 +1,319 @@
<?php declare(strict_types=1);
/**
* ownCloud
*
* @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,972 @@
<?php declare(strict_types=1);
/**
* ownCloud
*
* @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;
public static string $SPACE_ID_FROM_OCIS = '';
/**
* @var array of users with their different space ids
*/
public static array $spacesIdRef = [];
/**
* 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 $xRequestId
* @param int|null $davPathVersionToUse
*
* @return string
* @throws Exception|GuzzleException
*/
public static function getFileIdForPath(
?string $baseUrl,
?string $user,
?string $password,
?string $path,
?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,
$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 $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 $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'; // oCIS server's default value
}
$headers['Depth'] = $folderDepth;
return self::makeDavRequest(
$baseUrl,
$user,
$password,
"PROPFIND",
$path,
$headers,
$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
*
* @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"
):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,
[],
$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,
[],
$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 $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 $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,
$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));
}
/**
*
* @param string $baseUrl
* @param string $user
* @param string $password
* @param string $xRequestId
*
* @return string
* @throws GuzzleException
* @throws Exception
*/
public static function getSharesSpaceIdForUser(string $baseUrl, string $user, string $password, string $xRequestId): string {
if (\array_key_exists($user, self::$spacesIdRef) && \array_key_exists("virtual", self::$spacesIdRef[$user])) {
return self::$spacesIdRef[$user]["virtual"];
}
$response = GraphHelper::getMySpaces($baseUrl, $user, $password, '', $xRequestId);
$body = HttpRequestHelper::getJsonDecodedResponseBodyContent($response);
$spaceId = null;
foreach ($body->value as $spaces) {
if ($spaces->driveType === "virtual") {
$spaceId = $spaces->id;
break;
}
}
return $spaceId;
}
/**
* 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"];
}
$trimmedBaseUrl = \trim($baseUrl, "/");
$drivesPath = '/graph/v1.0/me/drives';
$fullUrl = $trimmedBaseUrl . $drivesPath;
$response = HttpRequestHelper::get(
$fullUrl,
$xRequestId,
$user,
$password
);
$bodyContents = $response->getBody()->getContents();
$json = \json_decode($bodyContents);
$personalSpaceId = '';
if ($json === null) {
// the graph endpoint did not give a useful answer
// try getting the information from the webdav endpoint
$fullUrl = $trimmedBaseUrl . '/remote.php/webdav';
$response = HttpRequestHelper::sendRequest(
$fullUrl,
$xRequestId,
'PROPFIND',
$user,
$password
);
// we expect to get a multipart XML response with status 207
$status = $response->getStatusCode();
if ($status === 401) {
throw new SpaceNotFoundException(__METHOD__ . " Personal space not found for user " . $user);
} elseif ($status !== 207) {
throw new Exception(
__METHOD__ . " webdav propfind for user $user failed with status $status - 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:id");
if ($xmlPart === false) {
throw new Exception(
__METHOD__ . " oc:id not found in webdav propfind for user $user - so the personal space id cannot be discovered"
);
}
$ocIdRawString = $xmlPart[0]->__toString();
$separator = "!";
if (\strpos($ocIdRawString, $separator) !== false) {
// The string is not base64-encoded, because the exclamation mark is not in the base64 alphabet.
// We expect to have a string with 2 parts separated by the exclamation mark.
// This is the format introduced in 2022-02
// oc:id should be something like:
// "7464caf6-1799-103c-9046-c7b74deb5f63!7464caf6-1799-103c-9046-c7b74deb5f63"
// There is no encoding to decode.
$decodedId = $ocIdRawString;
} else {
// fall-back to assuming that the oc:id is base64-encoded
// That is the format used before and up to 2022-02
// This can be removed after both the edge and master branches of cs3org/reva are using the new format.
// oc:id should be some base64 encoded string like:
// "NzQ2NGNhZjYtMTc5OS0xMDNjLTkwNDYtYzdiNzRkZWI1ZjYzOjc0NjRjYWY2LTE3OTktMTAzYy05MDQ2LWM3Yjc0ZGViNWY2Mw=="
// That should decode to something like:
// "7464caf6-1799-103c-9046-c7b74deb5f63:7464caf6-1799-103c-9046-c7b74deb5f63"
$decodedId = base64_decode($ocIdRawString);
$separator = ":";
}
$ocIdParts = \explode($separator, $decodedId);
if (\count($ocIdParts) !== 2) {
throw new Exception(
__METHOD__ . " the oc:id $decodedId for user $user does not have 2 parts separated by '$separator', so the personal space id cannot be discovered"
);
}
$personalSpaceId = $ocIdParts[0];
} else {
foreach ($json->value as $spaces) {
if ($spaces->driveType === "personal") {
$personalSpaceId = $spaces->id;
break;
}
}
}
if ($personalSpaceId) {
// If env var LOG_PERSONAL_SPACE_ID is defined, then output the details of the personal space id.
// This is a useful debugging tool to have confidence that the personal space id is found correctly.
if (\getenv('LOG_PERSONAL_SPACE_ID') !== false) {
echo __METHOD__ . " personal space id of user $user is $personalSpaceId\n";
}
self::$spacesIdRef[$user] = [];
self::$spacesIdRef[$user]["personal"] = $personalSpaceId;
return $personalSpaceId;
} else {
throw new SpaceNotFoundException(__METHOD__ . " Personal space not found for user " . $user);
}
}
/**
* 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 {
try {
$spaceId = self::getPersonalSpaceIdForUser(
$baseUrl,
$user,
$password,
$xRequestId,
);
} catch (SpaceNotFoundException $e) {
// if the fetch fails, and the user is not found, then a fake space id is prepared
// this is useful for testing when the personal space is of a non-existing user
$fakeSpaceId = self::generateUUIDv4();
self::$spacesIdRef[$user]["personal"] = $fakeSpaceId;
$spaceId = $fakeSpaceId;
}
return $spaceId;
}
/**
* sends a DAV request
*
* @param string|null $baseUrl
* URL of owncloud e.g. http://localhost:8080
* should include the subfolder if owncloud runs in a subfolder
* e.g. http://localhost:8080/owncloud-core
* @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 $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 $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 (self::$SPACE_ID_FROM_OCIS === '' && $davPathVersionToUse === self::DAV_VERSION_SPACES) {
$path = \ltrim($path, "/");
if (\str_starts_with($path, "Shares/")) {
$spaceId = self::getSharesSpaceIdForUser(
$baseUrl,
$doDavRequestAsUser ?? $user,
$password,
$xRequestId
);
$path = "/" . preg_replace("/^Shares\//", "", $path);
} else {
$spaceId = self::getPersonalSpaceIdForUserOrFakeIfNotFound(
$baseUrl,
$doDavRequestAsUser ?? $user,
$password,
$xRequestId
);
}
} else {
$spaceId = self::$SPACE_ID_FROM_OCIS;
}
$davPath = self::getDavPath($doDavRequestAsUser ?? $user, $davPathVersionToUse, $type, $spaceId);
//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" . $path);
if ($authType === 'bearer') {
$headers['Authorization'] = 'Bearer ' . $password;
$user = null;
$password = null;
}
if ($type === "public-files-new") {
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;
}
}
}
//Clear the space ID from ocis after each request
self::$SPACE_ID_FROM_OCIS = '';
return HttpRequestHelper::sendRequest(
$fullUrl,
$xRequestId,
$method,
$user,
$password,
$headers,
$body,
$config,
null,
$stream,
$timeout,
$client,
$isGivenStep
);
}
/**
* get the dav path
*
* @param string|null $user
* @param int|null $davPathVersionToUse (1|2)
* @param string|null $type
* @param string|null $spaceId
*
* @return string
*/
public static function getDavPath(
?string $user,
?int $davPathVersionToUse = null,
?string $type = "files",
?string $spaceId = null
):string {
$newTrashbinDavPath = "remote.php/dav/trash-bin/$user/";
switch ($type) {
case 'public-files':
case 'public-files-old':
return "public.php/webdav/";
case 'public-files-new':
return "remote.php/dav/public-files/$user/";
case 'archive':
return "remote.php/dav/archive/$user/files";
case 'versions':
case 'customgroups':
return "remote.php/dav/";
default:
break;
}
if ($davPathVersionToUse === self::DAV_VERSION_SPACES) {
// return spaces root path if spaceid is null
// REPORT request uses spaces root path
if ($spaceId === null) {
return "remote.php/dav/spaces/";
}
if ($type === "trash-bin") {
return "remote.php/dav/spaces/trash-bin/" . $spaceId . '/';
}
return "remote.php/dav/spaces/" . $spaceId . '/';
} else {
if ($davPathVersionToUse === self::DAV_VERSION_OLD) {
if ($type === "trash-bin") {
// Since there is no trash bin endpoint for old dav version, new dav version's endpoint is used here.
return $newTrashbinDavPath;
}
return "remote.php/webdav/";
} elseif ($davPathVersionToUse === self::DAV_VERSION_NEW) {
if ($type === "files") {
$path = 'remote.php/dav/files/';
return $path . $user . '/';
} elseif ($type === "trash-bin") {
return $newTrashbinDavPath;
} else {
return "remote.php/dav";
}
} else {
throw new InvalidArgumentException(
"DAV path version $davPathVersionToUse is unknown"
);
}
}
}
/**
* 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);
}
/**
* Decides if the proposed dav version and chunking version are
* a valid combination.
* If no chunkingVersion is specified, then any dav version is valid.
* If a chunkingVersion is specified, then it has to match the dav version.
* Note: in future, the dav and chunking versions might or might not
* move together and/or be supported together. So a more complex
* matrix could be needed here.
*
* @param string|int $davPathVersion
* @param string|int|null $chunkingVersion
*
* @return boolean is this a valid combination
*/
public static function isValidDavChunkingCombination(
$davPathVersion,
$chunkingVersion
): bool {
if ($davPathVersion === self::DAV_VERSION_SPACES) {
// allow only old chunking version when using the spaces dav
return $chunkingVersion === 1;
}
return (
($chunkingVersion === 'no' || $chunkingVersion === null) ||
($davPathVersion === $chunkingVersion)
);
}
/**
* 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,
"/public-files/$token/$fileName",
['d:getlastmodified'],
$xRequestId,
'1',
null,
$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
*
* @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 {
$response = self::propfind(
$baseUrl,
$user,
$password,
$resource,
["d:getlastmodified"],
$xRequestId,
"0",
"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');
}
}