mirror of
https://github.com/nextcloud/server.git
synced 2025-08-16 15:41:57 +00:00

The Theming app injects the stylesheets for the different themes in the "<header>" element of the page, and those stylesheets are then loaded by the browser from a "Controller" (a plain "Controller", not an "OCSController"). The stylesheets, in turn, may also get some images (like the background) also from the "Controller". When handling a request to "index.php" it is checked whether the user is logged in and, if not, a login is tried. A disabled user is explicitly seen as not logged in, so a login is always tried in that case, but disabled users are also explicitly prevented to log in, so the login also fails. Due to that trying to get any of the themed stylesheets or images with a disabled account (to be able to show the "Account disabled" error page) fails with an HTTP status 401. To solve that, and to avoid touching this basic logic as much as possible, the login exception is now ignored (if the user is disabled) for some specific requests to the Theming app. Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
563 lines
15 KiB
PHP
563 lines
15 KiB
PHP
<?php
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
|
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
use Behat\Gherkin\Node\TableNode;
|
|
use GuzzleHttp\Client;
|
|
use GuzzleHttp\Cookie\CookieJar;
|
|
use GuzzleHttp\Exception\ClientException;
|
|
use GuzzleHttp\Exception\ServerException;
|
|
use PHPUnit\Framework\Assert;
|
|
use Psr\Http\Message\ResponseInterface;
|
|
|
|
require __DIR__ . '/../../vendor/autoload.php';
|
|
|
|
trait BasicStructure {
|
|
use Auth;
|
|
use Avatar;
|
|
use Download;
|
|
use Mail;
|
|
use Theming;
|
|
|
|
/** @var string */
|
|
private $currentUser = '';
|
|
|
|
/** @var string */
|
|
private $currentServer = '';
|
|
|
|
/** @var string */
|
|
private $baseUrl = '';
|
|
|
|
/** @var int */
|
|
private $apiVersion = 1;
|
|
|
|
/** @var ResponseInterface */
|
|
private $response = null;
|
|
|
|
/** @var CookieJar */
|
|
private $cookieJar;
|
|
|
|
/** @var string */
|
|
private $requestToken;
|
|
|
|
protected $adminUser;
|
|
protected $regularUser;
|
|
protected $localBaseUrl;
|
|
protected $remoteBaseUrl;
|
|
|
|
public function __construct($baseUrl, $admin, $regular_user_password) {
|
|
// Initialize your context here
|
|
$this->baseUrl = $baseUrl;
|
|
$this->adminUser = $admin;
|
|
$this->regularUser = $regular_user_password;
|
|
$this->localBaseUrl = $this->baseUrl;
|
|
$this->remoteBaseUrl = $this->baseUrl;
|
|
$this->currentServer = 'LOCAL';
|
|
$this->cookieJar = new CookieJar();
|
|
|
|
// in case of ci deployment we take the server url from the environment
|
|
$testServerUrl = getenv('TEST_SERVER_URL');
|
|
if ($testServerUrl !== false) {
|
|
$this->baseUrl = $testServerUrl;
|
|
$this->localBaseUrl = $testServerUrl;
|
|
}
|
|
|
|
// federated server url from the environment
|
|
$testRemoteServerUrl = getenv('TEST_SERVER_FED_URL');
|
|
if ($testRemoteServerUrl !== false) {
|
|
$this->remoteBaseUrl = $testRemoteServerUrl;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @Given /^using api version "(\d+)"$/
|
|
* @param string $version
|
|
*/
|
|
public function usingApiVersion($version) {
|
|
$this->apiVersion = (int)$version;
|
|
}
|
|
|
|
/**
|
|
* @Given /^As an "([^"]*)"$/
|
|
* @param string $user
|
|
*/
|
|
public function asAn($user) {
|
|
$this->currentUser = $user;
|
|
}
|
|
|
|
/**
|
|
* @Given /^Using server "(LOCAL|REMOTE)"$/
|
|
* @param string $server
|
|
* @return string Previous used server
|
|
*/
|
|
public function usingServer($server) {
|
|
$previousServer = $this->currentServer;
|
|
if ($server === 'LOCAL') {
|
|
$this->baseUrl = $this->localBaseUrl;
|
|
$this->currentServer = 'LOCAL';
|
|
return $previousServer;
|
|
} else {
|
|
$this->baseUrl = $this->remoteBaseUrl;
|
|
$this->currentServer = 'REMOTE';
|
|
return $previousServer;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @When /^sending "([^"]*)" to "([^"]*)"$/
|
|
* @param string $verb
|
|
* @param string $url
|
|
*/
|
|
public function sendingTo($verb, $url) {
|
|
$this->sendingToWith($verb, $url, null);
|
|
}
|
|
|
|
/**
|
|
* Parses the xml answer to get ocs response which doesn't match with
|
|
* http one in v1 of the api.
|
|
*
|
|
* @param ResponseInterface $response
|
|
* @return string
|
|
*/
|
|
public function getOCSResponse($response) {
|
|
$body = simplexml_load_string((string)$response->getBody());
|
|
if ($body === false) {
|
|
throw new \RuntimeException('Could not parse OCS response, body is not valid XML');
|
|
}
|
|
return $body->meta[0]->statuscode;
|
|
}
|
|
|
|
/**
|
|
* This function is needed to use a vertical fashion in the gherkin tables.
|
|
*
|
|
* @param array $arrayOfArrays
|
|
* @return array
|
|
*/
|
|
public function simplifyArray($arrayOfArrays) {
|
|
$a = array_map(function ($subArray) {
|
|
return $subArray[0];
|
|
}, $arrayOfArrays);
|
|
return $a;
|
|
}
|
|
|
|
/**
|
|
* @When /^sending "([^"]*)" to "([^"]*)" with$/
|
|
* @param string $verb
|
|
* @param string $url
|
|
* @param TableNode $body
|
|
*/
|
|
public function sendingToWith($verb, $url, $body) {
|
|
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php" . $url;
|
|
$client = new Client();
|
|
$options = [];
|
|
if ($this->currentUser === 'admin') {
|
|
$options['auth'] = $this->adminUser;
|
|
} elseif (strpos($this->currentUser, 'anonymous') !== 0) {
|
|
$options['auth'] = [$this->currentUser, $this->regularUser];
|
|
}
|
|
$options['headers'] = [
|
|
'OCS-APIRequest' => 'true'
|
|
];
|
|
if ($body instanceof TableNode) {
|
|
$fd = $body->getRowsHash();
|
|
$options['form_params'] = $fd;
|
|
}
|
|
|
|
// TODO: Fix this hack!
|
|
if ($verb === 'PUT' && $body === null) {
|
|
$options['form_params'] = [
|
|
'foo' => 'bar',
|
|
];
|
|
}
|
|
|
|
try {
|
|
$this->response = $client->request($verb, $fullUrl, $options);
|
|
} catch (ClientException $ex) {
|
|
$this->response = $ex->getResponse();
|
|
} catch (ServerException $ex) {
|
|
$this->response = $ex->getResponse();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $verb
|
|
* @param string $url
|
|
* @param TableNode|array|null $body
|
|
* @param array $headers
|
|
*/
|
|
protected function sendRequestForJSON(string $verb, string $url, $body = null, array $headers = []): void {
|
|
$fullUrl = $this->baseUrl . "v{$this->apiVersion}.php" . $url;
|
|
$client = new Client();
|
|
$options = [];
|
|
if ($this->currentUser === 'admin') {
|
|
$options['auth'] = ['admin', 'admin'];
|
|
} elseif (strpos($this->currentUser, 'anonymous') !== 0) {
|
|
$options['auth'] = [$this->currentUser, $this->regularUser];
|
|
}
|
|
if ($body instanceof TableNode) {
|
|
$fd = $body->getRowsHash();
|
|
$options['form_params'] = $fd;
|
|
} elseif (is_array($body)) {
|
|
$options['form_params'] = $body;
|
|
}
|
|
|
|
$options['headers'] = array_merge($headers, [
|
|
'OCS-ApiRequest' => 'true',
|
|
'Accept' => 'application/json',
|
|
]);
|
|
|
|
try {
|
|
$this->response = $client->{$verb}($fullUrl, $options);
|
|
} catch (ClientException $ex) {
|
|
$this->response = $ex->getResponse();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @When /^sending "([^"]*)" with exact url to "([^"]*)"$/
|
|
* @param string $verb
|
|
* @param string $url
|
|
*/
|
|
public function sendingToDirectUrl($verb, $url) {
|
|
$this->sendingToWithDirectUrl($verb, $url, null);
|
|
}
|
|
|
|
public function sendingToWithDirectUrl($verb, $url, $body) {
|
|
$fullUrl = substr($this->baseUrl, 0, -5) . $url;
|
|
$client = new Client();
|
|
$options = [];
|
|
if ($this->currentUser === 'admin') {
|
|
$options['auth'] = $this->adminUser;
|
|
} elseif (strpos($this->currentUser, 'anonymous') !== 0) {
|
|
$options['auth'] = [$this->currentUser, $this->regularUser];
|
|
}
|
|
if ($body instanceof TableNode) {
|
|
$fd = $body->getRowsHash();
|
|
$options['form_params'] = $fd;
|
|
}
|
|
|
|
try {
|
|
$this->response = $client->request($verb, $fullUrl, $options);
|
|
} catch (ClientException $ex) {
|
|
$this->response = $ex->getResponse();
|
|
}
|
|
}
|
|
|
|
public function isExpectedUrl($possibleUrl, $finalPart) {
|
|
$baseUrlChopped = substr($this->baseUrl, 0, -4);
|
|
$endCharacter = strlen($baseUrlChopped) + strlen($finalPart);
|
|
return (substr($possibleUrl, 0, $endCharacter) == "$baseUrlChopped" . "$finalPart");
|
|
}
|
|
|
|
/**
|
|
* @Then /^the OCS status code should be "([^"]*)"$/
|
|
* @param int $statusCode
|
|
*/
|
|
public function theOCSStatusCodeShouldBe($statusCode) {
|
|
Assert::assertEquals($statusCode, $this->getOCSResponse($this->response));
|
|
}
|
|
|
|
/**
|
|
* @Then /^the HTTP status code should be "([^"]*)"$/
|
|
* @param int $statusCode
|
|
*/
|
|
public function theHTTPStatusCodeShouldBe($statusCode) {
|
|
Assert::assertEquals($statusCode, $this->response->getStatusCode());
|
|
}
|
|
|
|
/**
|
|
* @Then /^the Content-Type should be "([^"]*)"$/
|
|
* @param string $contentType
|
|
*/
|
|
public function theContentTypeShouldbe($contentType) {
|
|
Assert::assertEquals($contentType, $this->response->getHeader('Content-Type')[0]);
|
|
}
|
|
|
|
/**
|
|
* @param ResponseInterface $response
|
|
*/
|
|
private function extracRequestTokenFromResponse(ResponseInterface $response) {
|
|
$this->requestToken = substr(preg_replace('/(.*)data-requesttoken="(.*)">(.*)/sm', '\2', $response->getBody()->getContents()), 0, 89);
|
|
}
|
|
|
|
/**
|
|
* @Given Logging in using web as :user
|
|
* @param string $user
|
|
*/
|
|
public function loggingInUsingWebAs($user) {
|
|
$baseUrl = substr($this->baseUrl, 0, -5);
|
|
$loginUrl = $baseUrl . '/index.php/login';
|
|
// Request a new session and extract CSRF token
|
|
$client = new Client();
|
|
$response = $client->get(
|
|
$loginUrl,
|
|
[
|
|
'cookies' => $this->cookieJar,
|
|
]
|
|
);
|
|
$this->extracRequestTokenFromResponse($response);
|
|
|
|
// Login and extract new token
|
|
$password = ($user === 'admin') ? 'admin' : '123456';
|
|
$client = new Client();
|
|
$response = $client->post(
|
|
$loginUrl,
|
|
[
|
|
'form_params' => [
|
|
'user' => $user,
|
|
'password' => $password,
|
|
'requesttoken' => $this->requestToken,
|
|
],
|
|
'cookies' => $this->cookieJar,
|
|
'headers' => [
|
|
'Origin' => $baseUrl,
|
|
],
|
|
]
|
|
);
|
|
$this->extracRequestTokenFromResponse($response);
|
|
}
|
|
|
|
/**
|
|
* @When Sending a :method to :url with requesttoken
|
|
* @param string $method
|
|
* @param string $url
|
|
* @param TableNode|array|null $body
|
|
*/
|
|
public function sendingAToWithRequesttoken($method, $url, $body = null) {
|
|
$baseUrl = substr($this->baseUrl, 0, -5);
|
|
|
|
$options = [
|
|
'cookies' => $this->cookieJar,
|
|
'headers' => [
|
|
'requesttoken' => $this->requestToken
|
|
],
|
|
];
|
|
|
|
if ($body instanceof TableNode) {
|
|
$fd = $body->getRowsHash();
|
|
$options['form_params'] = $fd;
|
|
} elseif ($body) {
|
|
$options = array_merge_recursive($options, $body);
|
|
}
|
|
|
|
$client = new Client();
|
|
try {
|
|
$this->response = $client->request(
|
|
$method,
|
|
$baseUrl . $url,
|
|
$options
|
|
);
|
|
} catch (ClientException $e) {
|
|
$this->response = $e->getResponse();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @When Sending a :method to :url without requesttoken
|
|
* @param string $method
|
|
* @param string $url
|
|
*/
|
|
public function sendingAToWithoutRequesttoken($method, $url) {
|
|
$baseUrl = substr($this->baseUrl, 0, -5);
|
|
|
|
$client = new Client();
|
|
try {
|
|
$this->response = $client->request(
|
|
$method,
|
|
$baseUrl . $url,
|
|
[
|
|
'cookies' => $this->cookieJar
|
|
]
|
|
);
|
|
} catch (ClientException $e) {
|
|
$this->response = $e->getResponse();
|
|
}
|
|
}
|
|
|
|
public static function removeFile($path, $filename) {
|
|
if (file_exists("$path" . "$filename")) {
|
|
unlink("$path" . "$filename");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @Given User :user modifies text of :filename with text :text
|
|
* @param string $user
|
|
* @param string $filename
|
|
* @param string $text
|
|
*/
|
|
public function modifyTextOfFile($user, $filename, $text) {
|
|
self::removeFile($this->getDataDirectory() . "/$user/files", "$filename");
|
|
file_put_contents($this->getDataDirectory() . "/$user/files" . "$filename", "$text");
|
|
}
|
|
|
|
private function getDataDirectory() {
|
|
// Based on "runOcc" from CommandLine trait
|
|
$args = ['config:system:get', 'datadirectory'];
|
|
$args = array_map(function ($arg) {
|
|
return escapeshellarg($arg);
|
|
}, $args);
|
|
$args[] = '--no-ansi --no-warnings';
|
|
$args = implode(' ', $args);
|
|
|
|
$descriptor = [
|
|
0 => ['pipe', 'r'],
|
|
1 => ['pipe', 'w'],
|
|
2 => ['pipe', 'w'],
|
|
];
|
|
$process = proc_open('php console.php ' . $args, $descriptor, $pipes, $ocPath = '../..');
|
|
$lastStdOut = stream_get_contents($pipes[1]);
|
|
proc_close($process);
|
|
|
|
return trim($lastStdOut);
|
|
}
|
|
|
|
/**
|
|
* @Given file :filename is created :times times in :user user data
|
|
* @param string $filename
|
|
* @param string $times
|
|
* @param string $user
|
|
*/
|
|
public function fileIsCreatedTimesInUserData($filename, $times, $user) {
|
|
for ($i = 0; $i < $times; $i++) {
|
|
file_put_contents($this->getDataDirectory() . "/$user/files" . "$filename-$i", "content-$i");
|
|
}
|
|
}
|
|
|
|
public function createFileSpecificSize($name, $size) {
|
|
$file = fopen('work/' . "$name", 'w');
|
|
fseek($file, $size - 1, SEEK_CUR);
|
|
fwrite($file, 'a'); // write a dummy char at SIZE position
|
|
fclose($file);
|
|
}
|
|
|
|
public function createFileWithText($name, $text) {
|
|
$file = fopen('work/' . "$name", 'w');
|
|
fwrite($file, $text);
|
|
fclose($file);
|
|
}
|
|
|
|
/**
|
|
* @Given file :filename of size :size is created in local storage
|
|
* @param string $filename
|
|
* @param string $size
|
|
*/
|
|
public function fileIsCreatedInLocalStorageWithSize($filename, $size) {
|
|
$this->createFileSpecificSize("local_storage/$filename", $size);
|
|
}
|
|
|
|
/**
|
|
* @Given file :filename with text :text is created in local storage
|
|
* @param string $filename
|
|
* @param string $text
|
|
*/
|
|
public function fileIsCreatedInLocalStorageWithText($filename, $text) {
|
|
$this->createFileWithText("local_storage/$filename", $text);
|
|
}
|
|
|
|
/**
|
|
* @When Sleep for :seconds seconds
|
|
* @param int $seconds
|
|
*/
|
|
public function sleepForSeconds($seconds) {
|
|
sleep((int)$seconds);
|
|
}
|
|
|
|
/**
|
|
* @BeforeSuite
|
|
*/
|
|
public static function addFilesToSkeleton() {
|
|
for ($i = 0; $i < 5; $i++) {
|
|
file_put_contents('../../core/skeleton/' . 'textfile' . "$i" . '.txt', "Nextcloud test text file\n");
|
|
}
|
|
if (!file_exists('../../core/skeleton/FOLDER')) {
|
|
mkdir('../../core/skeleton/FOLDER', 0777, true);
|
|
}
|
|
if (!file_exists('../../core/skeleton/PARENT')) {
|
|
mkdir('../../core/skeleton/PARENT', 0777, true);
|
|
}
|
|
file_put_contents('../../core/skeleton/PARENT/' . 'parent.txt', "Nextcloud test text file\n");
|
|
if (!file_exists('../../core/skeleton/PARENT/CHILD')) {
|
|
mkdir('../../core/skeleton/PARENT/CHILD', 0777, true);
|
|
}
|
|
file_put_contents('../../core/skeleton/PARENT/CHILD/' . 'child.txt', "Nextcloud test text file\n");
|
|
}
|
|
|
|
/**
|
|
* @AfterSuite
|
|
*/
|
|
public static function removeFilesFromSkeleton() {
|
|
for ($i = 0; $i < 5; $i++) {
|
|
self::removeFile('../../core/skeleton/', 'textfile' . "$i" . '.txt');
|
|
}
|
|
if (is_dir('../../core/skeleton/FOLDER')) {
|
|
rmdir('../../core/skeleton/FOLDER');
|
|
}
|
|
self::removeFile('../../core/skeleton/PARENT/CHILD/', 'child.txt');
|
|
if (is_dir('../../core/skeleton/PARENT/CHILD')) {
|
|
rmdir('../../core/skeleton/PARENT/CHILD');
|
|
}
|
|
self::removeFile('../../core/skeleton/PARENT/', 'parent.txt');
|
|
if (is_dir('../../core/skeleton/PARENT')) {
|
|
rmdir('../../core/skeleton/PARENT');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @BeforeScenario @local_storage
|
|
*/
|
|
public static function removeFilesFromLocalStorageBefore() {
|
|
$dir = './work/local_storage/';
|
|
$di = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);
|
|
$ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST);
|
|
foreach ($ri as $file) {
|
|
$file->isDir() ? rmdir($file) : unlink($file);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @AfterScenario @local_storage
|
|
*/
|
|
public static function removeFilesFromLocalStorageAfter() {
|
|
$dir = './work/local_storage/';
|
|
$di = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);
|
|
$ri = new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST);
|
|
foreach ($ri as $file) {
|
|
$file->isDir() ? rmdir($file) : unlink($file);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @Given /^cookies are reset$/
|
|
*/
|
|
public function cookiesAreReset() {
|
|
$this->cookieJar = new CookieJar();
|
|
}
|
|
|
|
/**
|
|
* @Then The following headers should be set
|
|
* @param TableNode $table
|
|
* @throws \Exception
|
|
*/
|
|
public function theFollowingHeadersShouldBeSet(TableNode $table) {
|
|
foreach ($table->getTable() as $header) {
|
|
$headerName = $header[0];
|
|
$expectedHeaderValue = $header[1];
|
|
$returnedHeader = $this->response->getHeader($headerName)[0];
|
|
if ($returnedHeader !== $expectedHeaderValue) {
|
|
throw new \Exception(
|
|
sprintf(
|
|
"Expected value '%s' for header '%s', got '%s'",
|
|
$expectedHeaderValue,
|
|
$headerName,
|
|
$returnedHeader
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|