migrate therinaldos.com data
Build & Deploy to DigitalOcean Space / build (push) Failing after 2m38s

This commit is contained in:
2024-05-05 15:50:45 -04:00
commit ef1ff240d4
23182 changed files with 3801898 additions and 0 deletions
@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OC\Authentication\Events\AppPasswordCreatedEvent;
use OC\Authentication\Token\IProvider;
use OC\Authentication\Token\IToken;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSForbiddenException;
use OCP\Authentication\Exceptions\CredentialsUnavailableException;
use OCP\Authentication\Exceptions\InvalidTokenException;
use OCP\Authentication\Exceptions\PasswordUnavailableException;
use OCP\Authentication\LoginCredentials\IStore;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IRequest;
use OCP\ISession;
use OCP\Security\ISecureRandom;
class AppPasswordController extends \OCP\AppFramework\OCSController {
public function __construct(
string $appName,
IRequest $request,
private ISession $session,
private ISecureRandom $random,
private IProvider $tokenProvider,
private IStore $credentialStore,
private IEventDispatcher $eventDispatcher,
) {
parent::__construct($appName, $request);
}
/**
* @NoAdminRequired
* @PasswordConfirmationRequired
*
* Create app password
*
* @return DataResponse<Http::STATUS_OK, array{apppassword: string}, array{}>
* @throws OCSForbiddenException Creating app password is not allowed
*
* 200: App password returned
*/
public function getAppPassword(): DataResponse {
// We do not allow the creation of new tokens if this is an app password
if ($this->session->exists('app_password')) {
throw new OCSForbiddenException('You cannot request an new apppassword with an apppassword');
}
try {
$credentials = $this->credentialStore->getLoginCredentials();
} catch (CredentialsUnavailableException $e) {
throw new OCSForbiddenException();
}
try {
$password = $credentials->getPassword();
} catch (PasswordUnavailableException $e) {
$password = null;
}
$userAgent = $this->request->getHeader('USER_AGENT');
$token = $this->random->generate(72, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS);
$generatedToken = $this->tokenProvider->generateToken(
$token,
$credentials->getUID(),
$credentials->getLoginName(),
$password,
$userAgent,
IToken::PERMANENT_TOKEN,
IToken::DO_NOT_REMEMBER
);
$this->eventDispatcher->dispatchTyped(
new AppPasswordCreatedEvent($generatedToken)
);
return new DataResponse([
'apppassword' => $token
]);
}
/**
* @NoAdminRequired
*
* Delete app password
*
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>
* @throws OCSForbiddenException Deleting app password is not allowed
*
* 200: App password deleted successfully
*/
public function deleteAppPassword(): DataResponse {
if (!$this->session->exists('app_password')) {
throw new OCSForbiddenException('no app password in use');
}
$appPassword = $this->session->get('app_password');
try {
$token = $this->tokenProvider->getToken($appPassword);
} catch (InvalidTokenException $e) {
throw new OCSForbiddenException('could not remove apptoken');
}
$this->tokenProvider->invalidateTokenById($token->getUID(), $token->getId());
return new DataResponse();
}
/**
* @NoAdminRequired
*
* Rotate app password
*
* @return DataResponse<Http::STATUS_OK, array{apppassword: string}, array{}>
* @throws OCSForbiddenException Rotating app password is not allowed
*
* 200: App password returned
*/
public function rotateAppPassword(): DataResponse {
if (!$this->session->exists('app_password')) {
throw new OCSForbiddenException('no app password in use');
}
$appPassword = $this->session->get('app_password');
try {
$token = $this->tokenProvider->getToken($appPassword);
} catch (InvalidTokenException $e) {
throw new OCSForbiddenException('could not rotate apptoken');
}
$newToken = $this->random->generate(72, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS);
$this->tokenProvider->rotate($token, $appPassword, $newToken);
return new DataResponse([
'apppassword' => $newToken,
]);
}
}
@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2017 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OCA\Core\ResponseDefinitions;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\Collaboration\AutoComplete\AutoCompleteEvent;
use OCP\Collaboration\AutoComplete\AutoCompleteFilterEvent;
use OCP\Collaboration\AutoComplete\IManager;
use OCP\Collaboration\Collaborators\ISearch;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IRequest;
use OCP\Share\IShare;
/**
* @psalm-import-type CoreAutocompleteResult from ResponseDefinitions
*/
class AutoCompleteController extends OCSController {
public function __construct(
string $appName,
IRequest $request,
private ISearch $collaboratorSearch,
private IManager $autoCompleteManager,
private IEventDispatcher $dispatcher,
) {
parent::__construct($appName, $request);
}
/**
* @NoAdminRequired
*
* Autocomplete a query
*
* @param string $search Text to search for
* @param string|null $itemType Type of the items to search for
* @param string|null $itemId ID of the items to search for
* @param string|null $sorter can be piped, top prio first, e.g.: "commenters|share-recipients"
* @param int[] $shareTypes Types of shares to search for
* @param int $limit Maximum number of results to return
*
* @return DataResponse<Http::STATUS_OK, CoreAutocompleteResult[], array{}>
*
* 200: Autocomplete results returned
*/
public function get(string $search, ?string $itemType, ?string $itemId, ?string $sorter = null, array $shareTypes = [IShare::TYPE_USER], int $limit = 10): DataResponse {
// if enumeration/user listings are disabled, we'll receive an empty
// result from search() thus nothing else to do here.
[$results,] = $this->collaboratorSearch->search($search, $shareTypes, false, $limit, 0);
$event = new AutoCompleteEvent([
'search' => $search,
'results' => $results,
'itemType' => $itemType,
'itemId' => $itemId,
'sorter' => $sorter,
'shareTypes' => $shareTypes,
'limit' => $limit,
]);
$this->dispatcher->dispatch(IManager::class . '::filterResults', $event);
$results = $event->getResults();
$event = new AutoCompleteFilterEvent(
$results,
$search,
$itemType,
$itemId,
$sorter,
$shareTypes,
$limit,
);
$this->dispatcher->dispatchTyped($event);
$results = $event->getResults();
$exactMatches = $results['exact'];
unset($results['exact']);
$results = array_merge_recursive($exactMatches, $results);
if ($sorter !== null) {
$sorters = array_reverse(explode('|', $sorter));
$this->autoCompleteManager->runSorters($sorters, $results, [
'itemType' => $itemType,
'itemId' => $itemId,
]);
}
// transform to expected format
$results = $this->prepareResultArray($results);
return new DataResponse($results);
}
/**
* @return CoreAutocompleteResult[]
*/
protected function prepareResultArray(array $results): array {
$output = [];
/** @var string $type */
foreach ($results as $type => $subResult) {
foreach ($subResult as $result) {
/** @var ?string $icon */
$icon = array_key_exists('icon', $result) ? $result['icon'] : null;
/** @var string $label */
$label = $result['label'];
/** @var ?string $subline */
$subline = array_key_exists('subline', $result) ? $result['subline'] : null;
/** @var ?array{status: string, message: ?string, icon: ?string, clearAt: ?int} $status */
$status = array_key_exists('status', $result) && is_array($result['status']) && !empty($result['status']) ? $result['status'] : null;
/** @var ?string $shareWithDisplayNameUnique */
$shareWithDisplayNameUnique = array_key_exists('shareWithDisplayNameUnique', $result) ? $result['shareWithDisplayNameUnique'] : null;
$output[] = [
'id' => (string) $result['value']['shareWith'],
'label' => $label,
'icon' => $icon ?? '',
'source' => $type,
'status' => $status ?? '',
'subline' => $subline ?? '',
'shareWithDisplayNameUnique' => $shareWithDisplayNameUnique ?? '',
];
}
}
return $output;
}
}
@@ -0,0 +1,364 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Joas Schilling <coding@schilljs.com>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Julien Veyssier <eneiluj@posteo.net>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Vincent Petry <vincent@nextcloud.com>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OC\Core\Controller;
use OC\AppFramework\Utility\TimeFactory;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\IAvatarManager;
use OCP\ICache;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
/**
* Class AvatarController
*
* @package OC\Core\Controller
*/
class AvatarController extends Controller {
public function __construct(
string $appName,
IRequest $request,
protected IAvatarManager $avatarManager,
protected ICache $cache,
protected IL10N $l10n,
protected IUserManager $userManager,
protected IRootFolder $rootFolder,
protected LoggerInterface $logger,
protected ?string $userId,
protected TimeFactory $timeFactory,
) {
parent::__construct($appName, $request);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
* @NoSameSiteCookieRequired
* @PublicPage
*
* Get the dark avatar
*
* @param string $userId ID of the user
* @param int $size Size of the avatar
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string, X-NC-IsCustomAvatar: int}>|JSONResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>
*
* 200: Avatar returned
* 404: Avatar not found
*/
public function getAvatarDark(string $userId, int $size) {
if ($size <= 64) {
if ($size !== 64) {
$this->logger->debug('Avatar requested in deprecated size ' . $size);
}
$size = 64;
} else {
if ($size !== 512) {
$this->logger->debug('Avatar requested in deprecated size ' . $size);
}
$size = 512;
}
try {
$avatar = $this->avatarManager->getAvatar($userId);
$avatarFile = $avatar->getFile($size, true);
$response = new FileDisplayResponse(
$avatarFile,
Http::STATUS_OK,
['Content-Type' => $avatarFile->getMimeType(), 'X-NC-IsCustomAvatar' => (int)$avatar->isCustomAvatar()]
);
} catch (\Exception $e) {
return new JSONResponse([], Http::STATUS_NOT_FOUND);
}
// Cache for 1 day
$response->cacheFor(60 * 60 * 24, false, true);
return $response;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
* @NoSameSiteCookieRequired
* @PublicPage
*
* Get the avatar
*
* @param string $userId ID of the user
* @param int $size Size of the avatar
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string, X-NC-IsCustomAvatar: int}>|JSONResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>
*
* 200: Avatar returned
* 404: Avatar not found
*/
public function getAvatar(string $userId, int $size) {
if ($size <= 64) {
if ($size !== 64) {
$this->logger->debug('Avatar requested in deprecated size ' . $size);
}
$size = 64;
} else {
if ($size !== 512) {
$this->logger->debug('Avatar requested in deprecated size ' . $size);
}
$size = 512;
}
try {
$avatar = $this->avatarManager->getAvatar($userId);
$avatarFile = $avatar->getFile($size);
$response = new FileDisplayResponse(
$avatarFile,
Http::STATUS_OK,
['Content-Type' => $avatarFile->getMimeType(), 'X-NC-IsCustomAvatar' => (int)$avatar->isCustomAvatar()]
);
} catch (\Exception $e) {
return new JSONResponse([], Http::STATUS_NOT_FOUND);
}
// Cache for 1 day
$response->cacheFor(60 * 60 * 24, false, true);
return $response;
}
/**
* @NoAdminRequired
*/
public function postAvatar(?string $path = null): JSONResponse {
$files = $this->request->getUploadedFile('files');
if (isset($path)) {
$path = stripslashes($path);
$userFolder = $this->rootFolder->getUserFolder($this->userId);
/** @var File $node */
$node = $userFolder->get($path);
if (!($node instanceof File)) {
return new JSONResponse(['data' => ['message' => $this->l10n->t('Please select a file.')]]);
}
if ($node->getSize() > 20 * 1024 * 1024) {
return new JSONResponse(
['data' => ['message' => $this->l10n->t('File is too big')]],
Http::STATUS_BAD_REQUEST
);
}
if ($node->getMimeType() !== 'image/jpeg' && $node->getMimeType() !== 'image/png') {
return new JSONResponse(
['data' => ['message' => $this->l10n->t('The selected file is not an image.')]],
Http::STATUS_BAD_REQUEST
);
}
try {
$content = $node->getContent();
} catch (\OCP\Files\NotPermittedException $e) {
return new JSONResponse(
['data' => ['message' => $this->l10n->t('The selected file cannot be read.')]],
Http::STATUS_BAD_REQUEST
);
}
} elseif (!is_null($files)) {
if (
$files['error'][0] === 0 &&
is_uploaded_file($files['tmp_name'][0]) &&
!\OC\Files\Filesystem::isFileBlacklisted($files['tmp_name'][0])
) {
if ($files['size'][0] > 20 * 1024 * 1024) {
return new JSONResponse(
['data' => ['message' => $this->l10n->t('File is too big')]],
Http::STATUS_BAD_REQUEST
);
}
$this->cache->set('avatar_upload', file_get_contents($files['tmp_name'][0]), 7200);
$content = $this->cache->get('avatar_upload');
unlink($files['tmp_name'][0]);
} else {
$phpFileUploadErrors = [
UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'),
UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'),
UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'),
UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'),
UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'),
UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'),
];
$message = $phpFileUploadErrors[$files['error'][0]] ?? $this->l10n->t('Invalid file provided');
$this->logger->warning($message, ['app' => 'core']);
return new JSONResponse(
['data' => ['message' => $message]],
Http::STATUS_BAD_REQUEST
);
}
} else {
//Add imgfile
return new JSONResponse(
['data' => ['message' => $this->l10n->t('No image or file provided')]],
Http::STATUS_BAD_REQUEST
);
}
try {
$image = new \OCP\Image();
$image->loadFromData($content);
$image->readExif($content);
$image->fixOrientation();
if ($image->valid()) {
$mimeType = $image->mimeType();
if ($mimeType !== 'image/jpeg' && $mimeType !== 'image/png') {
return new JSONResponse(
['data' => ['message' => $this->l10n->t('Unknown filetype')]],
Http::STATUS_OK
);
}
if ($image->width() === $image->height()) {
try {
$avatar = $this->avatarManager->getAvatar($this->userId);
$avatar->set($image);
// Clean up
$this->cache->remove('tmpAvatar');
return new JSONResponse(['status' => 'success']);
} catch (\Throwable $e) {
$this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'core']);
return new JSONResponse(['data' => ['message' => $this->l10n->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST);
}
}
$this->cache->set('tmpAvatar', $image->data(), 7200);
return new JSONResponse(
['data' => 'notsquare'],
Http::STATUS_OK
);
} else {
return new JSONResponse(
['data' => ['message' => $this->l10n->t('Invalid image')]],
Http::STATUS_OK
);
}
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'core']);
return new JSONResponse(['data' => ['message' => $this->l10n->t('An error occurred. Please contact your admin.')]], Http::STATUS_OK);
}
}
/**
* @NoAdminRequired
*/
public function deleteAvatar(): JSONResponse {
try {
$avatar = $this->avatarManager->getAvatar($this->userId);
$avatar->remove();
return new JSONResponse();
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'core']);
return new JSONResponse(['data' => ['message' => $this->l10n->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST);
}
}
/**
* @NoAdminRequired
*
* @return JSONResponse|DataDisplayResponse
*/
public function getTmpAvatar() {
$tmpAvatar = $this->cache->get('tmpAvatar');
if (is_null($tmpAvatar)) {
return new JSONResponse(['data' => [
'message' => $this->l10n->t("No temporary profile picture available, try again")
]],
Http::STATUS_NOT_FOUND);
}
$image = new \OCP\Image();
$image->loadFromData($tmpAvatar);
$resp = new DataDisplayResponse(
$image->data() ?? '',
Http::STATUS_OK,
['Content-Type' => $image->mimeType()]);
$resp->setETag((string)crc32($image->data() ?? ''));
$resp->cacheFor(0);
$resp->setLastModified(new \DateTime('now', new \DateTimeZone('GMT')));
return $resp;
}
/**
* @NoAdminRequired
*/
public function postCroppedAvatar(?array $crop = null): JSONResponse {
if (is_null($crop)) {
return new JSONResponse(['data' => ['message' => $this->l10n->t("No crop data provided")]],
Http::STATUS_BAD_REQUEST);
}
if (!isset($crop['x'], $crop['y'], $crop['w'], $crop['h'])) {
return new JSONResponse(['data' => ['message' => $this->l10n->t("No valid crop data provided")]],
Http::STATUS_BAD_REQUEST);
}
$tmpAvatar = $this->cache->get('tmpAvatar');
if (is_null($tmpAvatar)) {
return new JSONResponse(['data' => [
'message' => $this->l10n->t("No temporary profile picture available, try again")
]],
Http::STATUS_BAD_REQUEST);
}
$image = new \OCP\Image();
$image->loadFromData($tmpAvatar);
$image->crop($crop['x'], $crop['y'], (int)round($crop['w']), (int)round($crop['h']));
try {
$avatar = $this->avatarManager->getAvatar($this->userId);
$avatar->set($image);
// Clean up
$this->cache->remove('tmpAvatar');
return new JSONResponse(['status' => 'success']);
} catch (\OC\NotSquareException $e) {
return new JSONResponse(['data' => ['message' => $this->l10n->t('Crop is not square')]],
Http::STATUS_BAD_REQUEST);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e, 'app' => 'core']);
return new JSONResponse(['data' => ['message' => $this->l10n->t('An error occurred. Please contact your admin.')]], Http::STATUS_BAD_REQUEST);
}
}
}
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/**
* @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OC\Security\CSRF\CsrfTokenManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class CSRFTokenController extends Controller {
public function __construct(
string $appName,
IRequest $request,
private CsrfTokenManager $tokenManager,
) {
parent::__construct($appName, $request);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
*/
public function index(): JSONResponse {
if (!$this->request->passesStrictCookieCheck()) {
return new JSONResponse([], Http::STATUS_FORBIDDEN);
}
$requestToken = $this->tokenManager->getToken();
return new JSONResponse([
'token' => $requestToken->getEncryptedValue(),
]);
}
}
@@ -0,0 +1,374 @@
<?php
/**
* @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
*
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Mario Danic <mario@lovelyhq.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author RussellAult <RussellAult@users.noreply.github.com>
* @author Sergej Nikolaev <kinolaev@gmail.com>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OC\Authentication\Events\AppPasswordCreatedEvent;
use OC\Authentication\Exceptions\PasswordlessTokenException;
use OC\Authentication\Token\IProvider;
use OC\Authentication\Token\IToken;
use OCA\OAuth2\Db\AccessToken;
use OCA\OAuth2\Db\AccessTokenMapper;
use OCA\OAuth2\Db\ClientMapper;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\Attribute\UseSession;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\StandaloneTemplateResponse;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Authentication\Exceptions\InvalidTokenException;
use OCP\Defaults;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IL10N;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Security\ICrypto;
use OCP\Security\ISecureRandom;
use OCP\Session\Exceptions\SessionNotAvailableException;
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class ClientFlowLoginController extends Controller {
public const STATE_NAME = 'client.flow.state.token';
public function __construct(
string $appName,
IRequest $request,
private IUserSession $userSession,
private IL10N $l10n,
private Defaults $defaults,
private ISession $session,
private IProvider $tokenProvider,
private ISecureRandom $random,
private IURLGenerator $urlGenerator,
private ClientMapper $clientMapper,
private AccessTokenMapper $accessTokenMapper,
private ICrypto $crypto,
private IEventDispatcher $eventDispatcher,
private ITimeFactory $timeFactory,
) {
parent::__construct($appName, $request);
}
private function getClientName(): string {
$userAgent = $this->request->getHeader('USER_AGENT');
return $userAgent !== '' ? $userAgent : 'unknown';
}
private function isValidToken(string $stateToken): bool {
$currentToken = $this->session->get(self::STATE_NAME);
if (!is_string($currentToken)) {
return false;
}
return hash_equals($currentToken, $stateToken);
}
private function stateTokenForbiddenResponse(): StandaloneTemplateResponse {
$response = new StandaloneTemplateResponse(
$this->appName,
'403',
[
'message' => $this->l10n->t('State token does not match'),
],
'guest'
);
$response->setStatus(Http::STATUS_FORBIDDEN);
return $response;
}
/**
* @PublicPage
* @NoCSRFRequired
*/
#[UseSession]
public function showAuthPickerPage(string $clientIdentifier = '', string $user = '', int $direct = 0): StandaloneTemplateResponse {
$clientName = $this->getClientName();
$client = null;
if ($clientIdentifier !== '') {
$client = $this->clientMapper->getByIdentifier($clientIdentifier);
$clientName = $client->getName();
}
// No valid clientIdentifier given and no valid API Request (APIRequest header not set)
$clientRequest = $this->request->getHeader('OCS-APIREQUEST');
if ($clientRequest !== 'true' && $client === null) {
return new StandaloneTemplateResponse(
$this->appName,
'error',
[
'errors' =>
[
[
'error' => 'Access Forbidden',
'hint' => 'Invalid request',
],
],
],
'guest'
);
}
$stateToken = $this->random->generate(
64,
ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_DIGITS
);
$this->session->set(self::STATE_NAME, $stateToken);
$csp = new Http\ContentSecurityPolicy();
if ($client) {
$csp->addAllowedFormActionDomain($client->getRedirectUri());
} else {
$csp->addAllowedFormActionDomain('nc://*');
}
$response = new StandaloneTemplateResponse(
$this->appName,
'loginflow/authpicker',
[
'client' => $clientName,
'clientIdentifier' => $clientIdentifier,
'instanceName' => $this->defaults->getName(),
'urlGenerator' => $this->urlGenerator,
'stateToken' => $stateToken,
'serverHost' => $this->getServerPath(),
'oauthState' => $this->session->get('oauth.state'),
'user' => $user,
'direct' => $direct,
],
'guest'
);
$response->setContentSecurityPolicy($csp);
return $response;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
* @NoSameSiteCookieRequired
*/
#[UseSession]
public function grantPage(string $stateToken = '',
string $clientIdentifier = '',
int $direct = 0): StandaloneTemplateResponse {
if (!$this->isValidToken($stateToken)) {
return $this->stateTokenForbiddenResponse();
}
$clientName = $this->getClientName();
$client = null;
if ($clientIdentifier !== '') {
$client = $this->clientMapper->getByIdentifier($clientIdentifier);
$clientName = $client->getName();
}
$csp = new Http\ContentSecurityPolicy();
if ($client) {
$csp->addAllowedFormActionDomain($client->getRedirectUri());
} else {
$csp->addAllowedFormActionDomain('nc://*');
}
/** @var IUser $user */
$user = $this->userSession->getUser();
$response = new StandaloneTemplateResponse(
$this->appName,
'loginflow/grant',
[
'userId' => $user->getUID(),
'userDisplayName' => $user->getDisplayName(),
'client' => $clientName,
'clientIdentifier' => $clientIdentifier,
'instanceName' => $this->defaults->getName(),
'urlGenerator' => $this->urlGenerator,
'stateToken' => $stateToken,
'serverHost' => $this->getServerPath(),
'oauthState' => $this->session->get('oauth.state'),
'direct' => $direct,
],
'guest'
);
$response->setContentSecurityPolicy($csp);
return $response;
}
/**
* @NoAdminRequired
*
* @return Http\RedirectResponse|Response
*/
#[UseSession]
public function generateAppPassword(string $stateToken,
string $clientIdentifier = '') {
if (!$this->isValidToken($stateToken)) {
$this->session->remove(self::STATE_NAME);
return $this->stateTokenForbiddenResponse();
}
$this->session->remove(self::STATE_NAME);
try {
$sessionId = $this->session->getId();
} catch (SessionNotAvailableException $ex) {
$response = new Response();
$response->setStatus(Http::STATUS_FORBIDDEN);
return $response;
}
try {
$sessionToken = $this->tokenProvider->getToken($sessionId);
$loginName = $sessionToken->getLoginName();
try {
$password = $this->tokenProvider->getPassword($sessionToken, $sessionId);
} catch (PasswordlessTokenException $ex) {
$password = null;
}
} catch (InvalidTokenException $ex) {
$response = new Response();
$response->setStatus(Http::STATUS_FORBIDDEN);
return $response;
}
$clientName = $this->getClientName();
$client = false;
if ($clientIdentifier !== '') {
$client = $this->clientMapper->getByIdentifier($clientIdentifier);
$clientName = $client->getName();
}
$token = $this->random->generate(72, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS);
$uid = $this->userSession->getUser()->getUID();
$generatedToken = $this->tokenProvider->generateToken(
$token,
$uid,
$loginName,
$password,
$clientName,
IToken::PERMANENT_TOKEN,
IToken::DO_NOT_REMEMBER
);
if ($client) {
$code = $this->random->generate(128, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS);
$accessToken = new AccessToken();
$accessToken->setClientId($client->getId());
$accessToken->setEncryptedToken($this->crypto->encrypt($token, $code));
$accessToken->setHashedCode(hash('sha512', $code));
$accessToken->setTokenId($generatedToken->getId());
$accessToken->setCodeCreatedAt($this->timeFactory->now()->getTimestamp());
$this->accessTokenMapper->insert($accessToken);
$redirectUri = $client->getRedirectUri();
if (parse_url($redirectUri, PHP_URL_QUERY)) {
$redirectUri .= '&';
} else {
$redirectUri .= '?';
}
$redirectUri .= sprintf(
'state=%s&code=%s',
urlencode($this->session->get('oauth.state')),
urlencode($code)
);
$this->session->remove('oauth.state');
} else {
$redirectUri = 'nc://login/server:' . $this->getServerPath() . '&user:' . urlencode($loginName) . '&password:' . urlencode($token);
// Clear the token from the login here
$this->tokenProvider->invalidateToken($sessionId);
}
$this->eventDispatcher->dispatchTyped(
new AppPasswordCreatedEvent($generatedToken)
);
return new Http\RedirectResponse($redirectUri);
}
/**
* @PublicPage
*/
public function apptokenRedirect(string $stateToken, string $user, string $password): Response {
if (!$this->isValidToken($stateToken)) {
return $this->stateTokenForbiddenResponse();
}
try {
$token = $this->tokenProvider->getToken($password);
if ($token->getLoginName() !== $user) {
throw new InvalidTokenException('login name does not match');
}
} catch (InvalidTokenException $e) {
$response = new StandaloneTemplateResponse(
$this->appName,
'403',
[
'message' => $this->l10n->t('Invalid app password'),
],
'guest'
);
$response->setStatus(Http::STATUS_FORBIDDEN);
return $response;
}
$redirectUri = 'nc://login/server:' . $this->getServerPath() . '&user:' . urlencode($user) . '&password:' . urlencode($password);
return new Http\RedirectResponse($redirectUri);
}
private function getServerPath(): string {
$serverPostfix = '';
if (str_contains($this->request->getRequestUri(), '/index.php')) {
$serverPostfix = substr($this->request->getRequestUri(), 0, strpos($this->request->getRequestUri(), '/index.php'));
} elseif (str_contains($this->request->getRequestUri(), '/login/flow')) {
$serverPostfix = substr($this->request->getRequestUri(), 0, strpos($this->request->getRequestUri(), '/login/flow'));
}
$protocol = $this->request->getServerProtocol();
if ($protocol !== "https") {
$xForwardedProto = $this->request->getHeader('X-Forwarded-Proto');
$xForwardedSSL = $this->request->getHeader('X-Forwarded-Ssl');
if ($xForwardedProto === 'https' || $xForwardedSSL === 'on') {
$protocol = 'https';
}
}
return $protocol . "://" . $this->request->getServerHost() . $serverPostfix;
}
}
@@ -0,0 +1,383 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OC\Core\Db\LoginFlowV2;
use OC\Core\Exception\LoginFlowV2NotFoundException;
use OC\Core\Service\LoginFlowV2Service;
use OCA\Core\ResponseDefinitions;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\Attribute\UseSession;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\StandaloneTemplateResponse;
use OCP\Authentication\Exceptions\InvalidTokenException;
use OCP\Defaults;
use OCP\IL10N;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Security\ISecureRandom;
/**
* @psalm-import-type CoreLoginFlowV2Credentials from ResponseDefinitions
* @psalm-import-type CoreLoginFlowV2 from ResponseDefinitions
*/
class ClientFlowLoginV2Controller extends Controller {
public const TOKEN_NAME = 'client.flow.v2.login.token';
public const STATE_NAME = 'client.flow.v2.state.token';
public function __construct(
string $appName,
IRequest $request,
private LoginFlowV2Service $loginFlowV2Service,
private IURLGenerator $urlGenerator,
private ISession $session,
private IUserSession $userSession,
private ISecureRandom $random,
private Defaults $defaults,
private ?string $userId,
private IL10N $l10n,
) {
parent::__construct($appName, $request);
}
/**
* @NoCSRFRequired
* @PublicPage
*
* Poll the login flow credentials
*
* @param string $token Token of the flow
* @return JSONResponse<Http::STATUS_OK, CoreLoginFlowV2Credentials, array{}>|JSONResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>
*
* 200: Login flow credentials returned
* 404: Login flow not found or completed
*/
public function poll(string $token): JSONResponse {
try {
$creds = $this->loginFlowV2Service->poll($token);
} catch (LoginFlowV2NotFoundException $e) {
return new JSONResponse([], Http::STATUS_NOT_FOUND);
}
return new JSONResponse($creds->jsonSerialize());
}
/**
* @NoCSRFRequired
* @PublicPage
*/
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
#[UseSession]
public function landing(string $token, $user = ''): Response {
if (!$this->loginFlowV2Service->startLoginFlow($token)) {
return $this->loginTokenForbiddenResponse();
}
$this->session->set(self::TOKEN_NAME, $token);
return new RedirectResponse(
$this->urlGenerator->linkToRouteAbsolute('core.ClientFlowLoginV2.showAuthPickerPage', ['user' => $user])
);
}
/**
* @NoCSRFRequired
* @PublicPage
*/
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
#[UseSession]
public function showAuthPickerPage($user = ''): StandaloneTemplateResponse {
try {
$flow = $this->getFlowByLoginToken();
} catch (LoginFlowV2NotFoundException $e) {
return $this->loginTokenForbiddenResponse();
}
$stateToken = $this->random->generate(
64,
ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_DIGITS
);
$this->session->set(self::STATE_NAME, $stateToken);
return new StandaloneTemplateResponse(
$this->appName,
'loginflowv2/authpicker',
[
'client' => $flow->getClientName(),
'instanceName' => $this->defaults->getName(),
'urlGenerator' => $this->urlGenerator,
'stateToken' => $stateToken,
'user' => $user,
],
'guest'
);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
* @NoSameSiteCookieRequired
*/
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
#[UseSession]
public function grantPage(?string $stateToken): StandaloneTemplateResponse {
if ($stateToken === null) {
return $this->stateTokenMissingResponse();
}
if (!$this->isValidStateToken($stateToken)) {
return $this->stateTokenForbiddenResponse();
}
try {
$flow = $this->getFlowByLoginToken();
} catch (LoginFlowV2NotFoundException $e) {
return $this->loginTokenForbiddenResponse();
}
/** @var IUser $user */
$user = $this->userSession->getUser();
return new StandaloneTemplateResponse(
$this->appName,
'loginflowv2/grant',
[
'userId' => $user->getUID(),
'userDisplayName' => $user->getDisplayName(),
'client' => $flow->getClientName(),
'instanceName' => $this->defaults->getName(),
'urlGenerator' => $this->urlGenerator,
'stateToken' => $stateToken,
],
'guest'
);
}
/**
* @PublicPage
*/
public function apptokenRedirect(?string $stateToken, string $user, string $password) {
if ($stateToken === null) {
return $this->stateTokenMissingResponse();
}
if (!$this->isValidStateToken($stateToken)) {
return $this->stateTokenForbiddenResponse();
}
try {
$this->getFlowByLoginToken();
} catch (LoginFlowV2NotFoundException $e) {
return $this->loginTokenForbiddenResponse();
}
$loginToken = $this->session->get(self::TOKEN_NAME);
// Clear session variables
$this->session->remove(self::TOKEN_NAME);
$this->session->remove(self::STATE_NAME);
try {
$token = \OC::$server->get(\OC\Authentication\Token\IProvider::class)->getToken($password);
if ($token->getLoginName() !== $user) {
throw new InvalidTokenException('login name does not match');
}
} catch (InvalidTokenException $e) {
$response = new StandaloneTemplateResponse(
$this->appName,
'403',
[
'message' => $this->l10n->t('Invalid app password'),
],
'guest'
);
$response->setStatus(Http::STATUS_FORBIDDEN);
return $response;
}
$result = $this->loginFlowV2Service->flowDoneWithAppPassword($loginToken, $this->getServerPath(), $token->getLoginName(), $password);
return $this->handleFlowDone($result);
}
/**
* @NoAdminRequired
*/
#[UseSession]
public function generateAppPassword(?string $stateToken): Response {
if ($stateToken === null) {
return $this->stateTokenMissingResponse();
}
if (!$this->isValidStateToken($stateToken)) {
return $this->stateTokenForbiddenResponse();
}
try {
$this->getFlowByLoginToken();
} catch (LoginFlowV2NotFoundException $e) {
return $this->loginTokenForbiddenResponse();
}
$loginToken = $this->session->get(self::TOKEN_NAME);
// Clear session variables
$this->session->remove(self::TOKEN_NAME);
$this->session->remove(self::STATE_NAME);
$sessionId = $this->session->getId();
$result = $this->loginFlowV2Service->flowDone($loginToken, $sessionId, $this->getServerPath(), $this->userId);
return $this->handleFlowDone($result);
}
private function handleFlowDone(bool $result): StandaloneTemplateResponse {
if ($result) {
return new StandaloneTemplateResponse(
$this->appName,
'loginflowv2/done',
[],
'guest'
);
}
$response = new StandaloneTemplateResponse(
$this->appName,
'403',
[
'message' => $this->l10n->t('Could not complete login'),
],
'guest'
);
$response->setStatus(Http::STATUS_FORBIDDEN);
return $response;
}
/**
* @NoCSRFRequired
* @PublicPage
*
* Init a login flow
*
* @return JSONResponse<Http::STATUS_OK, CoreLoginFlowV2, array{}>
*
* 200: Login flow init returned
*/
public function init(): JSONResponse {
// Get client user agent
$userAgent = $this->request->getHeader('USER_AGENT');
$tokens = $this->loginFlowV2Service->createTokens($userAgent);
$data = [
'poll' => [
'token' => $tokens->getPollToken(),
'endpoint' => $this->urlGenerator->linkToRouteAbsolute('core.ClientFlowLoginV2.poll')
],
'login' => $this->urlGenerator->linkToRouteAbsolute('core.ClientFlowLoginV2.landing', ['token' => $tokens->getLoginToken()]),
];
return new JSONResponse($data);
}
private function isValidStateToken(string $stateToken): bool {
$currentToken = $this->session->get(self::STATE_NAME);
if (!is_string($stateToken) || !is_string($currentToken)) {
return false;
}
return hash_equals($currentToken, $stateToken);
}
private function stateTokenMissingResponse(): StandaloneTemplateResponse {
$response = new StandaloneTemplateResponse(
$this->appName,
'403',
[
'message' => $this->l10n->t('State token missing'),
],
'guest'
);
$response->setStatus(Http::STATUS_FORBIDDEN);
return $response;
}
private function stateTokenForbiddenResponse(): StandaloneTemplateResponse {
$response = new StandaloneTemplateResponse(
$this->appName,
'403',
[
'message' => $this->l10n->t('State token does not match'),
],
'guest'
);
$response->setStatus(Http::STATUS_FORBIDDEN);
return $response;
}
/**
* @return LoginFlowV2
* @throws LoginFlowV2NotFoundException
*/
private function getFlowByLoginToken(): LoginFlowV2 {
$currentToken = $this->session->get(self::TOKEN_NAME);
if (!is_string($currentToken)) {
throw new LoginFlowV2NotFoundException('Login token not set in session');
}
return $this->loginFlowV2Service->getByLoginToken($currentToken);
}
private function loginTokenForbiddenResponse(): StandaloneTemplateResponse {
$response = new StandaloneTemplateResponse(
$this->appName,
'403',
[
'message' => $this->l10n->t('Your login token is invalid or has expired'),
],
'guest'
);
$response->setStatus(Http::STATUS_FORBIDDEN);
return $response;
}
private function getServerPath(): string {
$serverPostfix = '';
if (str_contains($this->request->getRequestUri(), '/index.php')) {
$serverPostfix = substr($this->request->getRequestUri(), 0, strpos($this->request->getRequestUri(), '/index.php'));
} elseif (str_contains($this->request->getRequestUri(), '/login/v2')) {
$serverPostfix = substr($this->request->getRequestUri(), 0, strpos($this->request->getRequestUri(), '/login/v2'));
}
$protocol = $this->request->getServerProtocol();
return $protocol . '://' . $this->request->getServerHost() . $serverPostfix;
}
}
@@ -0,0 +1,343 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use Exception;
use OCA\Core\ResponseDefinitions;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\Collaboration\Resources\CollectionException;
use OCP\Collaboration\Resources\ICollection;
use OCP\Collaboration\Resources\IManager;
use OCP\Collaboration\Resources\IResource;
use OCP\Collaboration\Resources\ResourceException;
use OCP\IRequest;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;
/**
* @psalm-import-type CoreResource from ResponseDefinitions
* @psalm-import-type CoreCollection from ResponseDefinitions
*/
class CollaborationResourcesController extends OCSController {
public function __construct(
string $appName,
IRequest $request,
private IManager $manager,
private IUserSession $userSession,
private LoggerInterface $logger,
) {
parent::__construct($appName, $request);
}
/**
* @param int $collectionId
* @return ICollection
* @throws CollectionException when the collection was not found for the user
*/
protected function getCollection(int $collectionId): ICollection {
$collection = $this->manager->getCollectionForUser($collectionId, $this->userSession->getUser());
if (!$collection->canAccess($this->userSession->getUser())) {
throw new CollectionException('Not found');
}
return $collection;
}
/**
* @NoAdminRequired
*
* Get a collection
*
* @param int $collectionId ID of the collection
* @return DataResponse<Http::STATUS_OK, CoreCollection, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array<empty>, array{}>
*
* 200: Collection returned
* 404: Collection not found
*/
public function listCollection(int $collectionId): DataResponse {
try {
$collection = $this->getCollection($collectionId);
} catch (CollectionException $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
return $this->respondCollection($collection);
}
/**
* @NoAdminRequired
*
* Search for collections
*
* @param string $filter Filter collections
* @return DataResponse<Http::STATUS_OK, CoreCollection[], array{}>|DataResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>
*
* 200: Collections returned
* 404: Collection not found
*/
public function searchCollections(string $filter): DataResponse {
try {
$collections = $this->manager->searchCollections($this->userSession->getUser(), $filter);
} catch (CollectionException $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
return new DataResponse($this->prepareCollections($collections));
}
/**
* @NoAdminRequired
*
* Add a resource to a collection
*
* @param int $collectionId ID of the collection
* @param string $resourceType Name of the resource
* @param string $resourceId ID of the resource
* @return DataResponse<Http::STATUS_OK, CoreCollection, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array<empty>, array{}>
*
* 200: Collection returned
* 404: Collection not found or resource inaccessible
*/
public function addResource(int $collectionId, string $resourceType, string $resourceId): DataResponse {
try {
$collection = $this->getCollection($collectionId);
} catch (CollectionException $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
$resource = $this->manager->createResource($resourceType, $resourceId);
if (!$resource->canAccess($this->userSession->getUser())) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
try {
$collection->addResource($resource);
} catch (ResourceException $e) {
}
return $this->respondCollection($collection);
}
/**
* @NoAdminRequired
*
* Remove a resource from a collection
*
* @param int $collectionId ID of the collection
* @param string $resourceType Name of the resource
* @param string $resourceId ID of the resource
* @return DataResponse<Http::STATUS_OK, CoreCollection, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array<empty>, array{}>
*
* 200: Collection returned
* 404: Collection or resource not found
*/
public function removeResource(int $collectionId, string $resourceType, string $resourceId): DataResponse {
try {
$collection = $this->getCollection($collectionId);
} catch (CollectionException $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
try {
$resource = $this->manager->getResourceForUser($resourceType, $resourceId, $this->userSession->getUser());
} catch (CollectionException $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
$collection->removeResource($resource);
return $this->respondCollection($collection);
}
/**
* @NoAdminRequired
*
* Get collections by resource
*
* @param string $resourceType Type of the resource
* @param string $resourceId ID of the resource
* @return DataResponse<Http::STATUS_OK, CoreCollection[], array{}>|DataResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>
*
* 200: Collections returned
* 404: Resource not accessible
*/
public function getCollectionsByResource(string $resourceType, string $resourceId): DataResponse {
try {
$resource = $this->manager->getResourceForUser($resourceType, $resourceId, $this->userSession->getUser());
} catch (ResourceException $e) {
$resource = $this->manager->createResource($resourceType, $resourceId);
}
if (!$resource->canAccess($this->userSession->getUser())) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
return new DataResponse($this->prepareCollections($resource->getCollections()));
}
/**
* @NoAdminRequired
*
* Create a collection for a resource
*
* @param string $baseResourceType Type of the base resource
* @param string $baseResourceId ID of the base resource
* @param string $name Name of the collection
* @return DataResponse<Http::STATUS_OK, CoreCollection, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array<empty>, array{}>
*
* 200: Collection returned
* 400: Creating collection is not possible
* 404: Resource inaccessible
*/
public function createCollectionOnResource(string $baseResourceType, string $baseResourceId, string $name): DataResponse {
if (!isset($name[0]) || isset($name[64])) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
try {
$resource = $this->manager->createResource($baseResourceType, $baseResourceId);
} catch (CollectionException $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
if (!$resource->canAccess($this->userSession->getUser())) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
$collection = $this->manager->newCollection($name);
$collection->addResource($resource);
return $this->respondCollection($collection);
}
/**
* @NoAdminRequired
*
* Rename a collection
*
* @param int $collectionId ID of the collection
* @param string $collectionName New name
* @return DataResponse<Http::STATUS_OK, CoreCollection, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array<empty>, array{}>
*
* 200: Collection returned
* 404: Collection not found
*/
public function renameCollection(int $collectionId, string $collectionName): DataResponse {
try {
$collection = $this->getCollection($collectionId);
} catch (CollectionException $exception) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
$collection->setName($collectionName);
return $this->respondCollection($collection);
}
/**
* @return DataResponse<Http::STATUS_OK, CoreCollection, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array<empty>, array{}>
*/
protected function respondCollection(ICollection $collection): DataResponse {
try {
return new DataResponse($this->prepareCollection($collection));
} catch (CollectionException $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
} catch (Exception $e) {
$this->logger->critical($e->getMessage(), ['exception' => $e, 'app' => 'core']);
return new DataResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
/**
* @return CoreCollection[]
*/
protected function prepareCollections(array $collections): array {
$result = [];
foreach ($collections as $collection) {
try {
$result[] = $this->prepareCollection($collection);
} catch (CollectionException $e) {
} catch (Exception $e) {
$this->logger->critical($e->getMessage(), ['exception' => $e, 'app' => 'core']);
}
}
return $result;
}
/**
* @return CoreCollection
*/
protected function prepareCollection(ICollection $collection): array {
if (!$collection->canAccess($this->userSession->getUser())) {
throw new CollectionException('Can not access collection');
}
return [
'id' => $collection->getId(),
'name' => $collection->getName(),
'resources' => $this->prepareResources($collection->getResources()),
];
}
/**
* @return CoreResource[]
*/
protected function prepareResources(array $resources): array {
$result = [];
foreach ($resources as $resource) {
try {
$result[] = $this->prepareResource($resource);
} catch (ResourceException $e) {
} catch (Exception $e) {
$this->logger->critical($e->getMessage(), ['exception' => $e, 'app' => 'core']);
}
}
return $result;
}
/**
* @return CoreResource
*/
protected function prepareResource(IResource $resource): array {
if (!$resource->canAccess($this->userSession->getUser())) {
throw new ResourceException('Can not access resource');
}
return $resource->getRichObject();
}
}
@@ -0,0 +1,68 @@
<?php
/**
* @copyright 2017 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Georg Ehrke <oc.list@georgehrke.com>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use Exception;
use OC\Contacts\ContactsMenu\Manager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
use OCP\IUserSession;
class ContactsMenuController extends Controller {
public function __construct(
IRequest $request,
private IUserSession $userSession,
private Manager $manager,
) {
parent::__construct('core', $request);
}
/**
* @NoAdminRequired
*
* @return \JsonSerializable[]
* @throws Exception
*/
public function index(?string $filter = null): array {
return $this->manager->getEntries($this->userSession->getUser(), $filter);
}
/**
* @NoAdminRequired
*
* @return JSONResponse|\JsonSerializable
* @throws Exception
*/
public function findOne(int $shareType, string $shareWith) {
$contact = $this->manager->findOne($this->userSession->getUser(), $shareType, $shareWith);
if ($contact) {
return $contact;
}
return new JSONResponse([], Http::STATUS_NOT_FOUND);
}
}
@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, John Molakvoæ (skjnldsv@protonmail.com)
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Citharel <nextcloud@tcit.fr>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OC\Files\AppData\Factory;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\Files\SimpleFS\ISimpleFolder;
use OCP\IRequest;
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class CssController extends Controller {
protected IAppData $appData;
public function __construct(
string $appName,
IRequest $request,
Factory $appDataFactory,
protected ITimeFactory $timeFactory,
) {
parent::__construct($appName, $request);
$this->appData = $appDataFactory->get('css');
}
/**
* @PublicPage
* @NoCSRFRequired
* @NoSameSiteCookieRequired
*
* @param string $fileName css filename with extension
* @param string $appName css folder name
* @return FileDisplayResponse|NotFoundResponse
*/
public function getCss(string $fileName, string $appName): Response {
try {
$folder = $this->appData->getFolder($appName);
$gzip = false;
$file = $this->getFile($folder, $fileName, $gzip);
} catch (NotFoundException $e) {
return new NotFoundResponse();
}
$response = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'text/css']);
if ($gzip) {
$response->addHeader('Content-Encoding', 'gzip');
}
$ttl = 31536000;
$response->addHeader('Cache-Control', 'max-age='.$ttl.', immutable');
$expires = new \DateTime();
$expires->setTimestamp($this->timeFactory->getTime());
$expires->add(new \DateInterval('PT'.$ttl.'S'));
$response->addHeader('Expires', $expires->format(\DateTime::RFC1123));
return $response;
}
/**
* @param ISimpleFolder $folder
* @param string $fileName
* @param bool $gzip is set to true if we use the gzip file
* @return ISimpleFile
* @throws NotFoundException
*/
private function getFile(ISimpleFolder $folder, string $fileName, bool &$gzip): ISimpleFile {
$encoding = $this->request->getHeader('Accept-Encoding');
if (str_contains($encoding, 'gzip')) {
try {
$gzip = true;
return $folder->getFile($fileName . '.gzip'); # Safari doesn't like .gz
} catch (NotFoundException $e) {
// continue
}
}
$gzip = false;
return $folder->getFile($fileName);
}
}
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\TemplateResponse;
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class ErrorController extends \OCP\AppFramework\Controller {
/**
* @PublicPage
* @NoCSRFRequired
*/
public function error403(): TemplateResponse {
$response = new TemplateResponse(
'core',
'403',
[],
'error'
);
$response->setStatus(Http::STATUS_FORBIDDEN);
return $response;
}
/**
* @PublicPage
* @NoCSRFRequired
*/
public function error404(): TemplateResponse {
$response = new TemplateResponse(
'core',
'404',
[],
'error'
);
$response->setStatus(Http::STATUS_NOT_FOUND);
return $response;
}
}
@@ -0,0 +1,119 @@
<?php
/**
* @copyright Copyright (c) 2019, Michael Weimann <mail@michael-weimann.eu>
*
* @author Michael Weimann <mail@michael-weimann.eu>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\Response;
use OCP\IAvatarManager;
use OCP\IRequest;
use Psr\Log\LoggerInterface;
/**
* This controller handles guest avatar requests.
*/
class GuestAvatarController extends Controller {
/**
* GuestAvatarController constructor.
*/
public function __construct(
string $appName,
IRequest $request,
private IAvatarManager $avatarManager,
private LoggerInterface $logger,
) {
parent::__construct($appName, $request);
}
/**
* Returns a guest avatar image response
*
* @PublicPage
* @NoCSRFRequired
*
* @param string $guestName The guest name, e.g. "Albert"
* @param string $size The desired avatar size, e.g. 64 for 64x64px
* @param bool|null $darkTheme Return dark avatar
* @return FileDisplayResponse<Http::STATUS_OK|Http::STATUS_CREATED, array{Content-Type: string}>|Response<Http::STATUS_INTERNAL_SERVER_ERROR, array{}>
*
* 200: Custom avatar returned
* 201: Avatar returned
*/
public function getAvatar(string $guestName, string $size, ?bool $darkTheme = false) {
$size = (int) $size;
$darkTheme = $darkTheme ?? false;
if ($size <= 64) {
if ($size !== 64) {
$this->logger->debug('Avatar requested in deprecated size ' . $size);
}
$size = 64;
} else {
if ($size !== 512) {
$this->logger->debug('Avatar requested in deprecated size ' . $size);
}
$size = 512;
}
try {
$avatar = $this->avatarManager->getGuestAvatar($guestName);
$avatarFile = $avatar->getFile($size, $darkTheme);
$resp = new FileDisplayResponse(
$avatarFile,
$avatar->isCustomAvatar() ? Http::STATUS_OK : Http::STATUS_CREATED,
['Content-Type' => $avatarFile->getMimeType()]
);
} catch (\Exception $e) {
$this->logger->error('error while creating guest avatar', [
'err' => $e,
]);
$resp = new Http\Response();
$resp->setStatus(Http::STATUS_INTERNAL_SERVER_ERROR);
return $resp;
}
// Cache for 30 minutes
$resp->cacheFor(1800, false, true);
return $resp;
}
/**
* Returns a dark guest avatar image response
*
* @PublicPage
* @NoCSRFRequired
*
* @param string $guestName The guest name, e.g. "Albert"
* @param string $size The desired avatar size, e.g. 64 for 64x64px
* @return FileDisplayResponse<Http::STATUS_OK|Http::STATUS_CREATED, array{Content-Type: string}>|Response<Http::STATUS_INTERNAL_SERVER_ERROR, array{}>
*
* 200: Custom avatar returned
* 201: Avatar returned
*/
public function getAvatarDark(string $guestName, string $size) {
return $this->getAvatar($guestName, $size, true);
}
}
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OC\Contacts\ContactsMenu\Manager;
use OCA\Core\ResponseDefinitions;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
use OCP\IUserSession;
use OCP\Share\IShare;
/**
* @psalm-import-type CoreContactsAction from ResponseDefinitions
*/
class HoverCardController extends \OCP\AppFramework\OCSController {
public function __construct(
IRequest $request,
private IUserSession $userSession,
private Manager $manager,
) {
parent::__construct('core', $request);
}
/**
* @NoAdminRequired
*
* Get the user details for a hovercard
*
* @param string $userId ID of the user
* @return DataResponse<Http::STATUS_OK, array{userId: string, displayName: string, actions: CoreContactsAction[]}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>
*
* 200: User details returned
* 404: User not found
*/
public function getUser(string $userId): DataResponse {
$contact = $this->manager->findOne($this->userSession->getUser(), IShare::TYPE_USER, $userId);
if (!$contact) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
$data = $contact->jsonSerialize();
$actions = $data['actions'];
if ($data['topAction']) {
array_unshift($actions, $data['topAction']);
}
/** @var CoreContactsAction[] $actions */
return new DataResponse([
'userId' => $userId,
'displayName' => $contact->getFullName(),
'actions' => $actions,
]);
}
}
@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
/**
* @copyright 2017, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Citharel <nextcloud@tcit.fr>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OC\Files\AppData\Factory;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\NotFoundResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFile;
use OCP\Files\SimpleFS\ISimpleFolder;
use OCP\IRequest;
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class JsController extends Controller {
protected IAppData $appData;
public function __construct(
string $appName,
IRequest $request,
Factory $appDataFactory,
protected ITimeFactory $timeFactory,
) {
parent::__construct($appName, $request);
$this->appData = $appDataFactory->get('js');
}
/**
* @PublicPage
* @NoCSRFRequired
* @NoSameSiteCookieRequired
*
* @param string $fileName js filename with extension
* @param string $appName js folder name
* @return FileDisplayResponse|NotFoundResponse
*/
public function getJs(string $fileName, string $appName): Response {
try {
$folder = $this->appData->getFolder($appName);
$gzip = false;
$file = $this->getFile($folder, $fileName, $gzip);
} catch (NotFoundException $e) {
return new NotFoundResponse();
}
$response = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'application/javascript']);
if ($gzip) {
$response->addHeader('Content-Encoding', 'gzip');
}
$ttl = 31536000;
$response->addHeader('Cache-Control', 'max-age='.$ttl.', immutable');
$expires = new \DateTime();
$expires->setTimestamp($this->timeFactory->getTime());
$expires->add(new \DateInterval('PT'.$ttl.'S'));
$response->addHeader('Expires', $expires->format(\DateTime::RFC1123));
return $response;
}
/**
* @param ISimpleFolder $folder
* @param string $fileName
* @param bool $gzip is set to true if we use the gzip file
* @return ISimpleFile
*
* @throws NotFoundException
*/
private function getFile(ISimpleFolder $folder, string $fileName, bool &$gzip): ISimpleFile {
$encoding = $this->request->getHeader('Accept-Encoding');
if (str_contains($encoding, 'gzip')) {
try {
$gzip = true;
return $folder->getFile($fileName . '.gzip'); # Safari doesn't like .gz
} catch (NotFoundException $e) {
// continue
}
}
$gzip = false;
return $folder->getFile($fileName);
}
}
@@ -0,0 +1,376 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2017, Sandro Lutz <sandro.lutz@temparus.ch>
* @copyright Copyright (c) 2016 Joas Schilling <coding@schilljs.com>
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Joas Schilling <coding@schilljs.com>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Michael Weimann <mail@michael-weimann.eu>
* @author Rayn0r <andrew@ilpss8.myfirewall.org>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OC\Core\Controller;
use OC\AppFramework\Http\Request;
use OC\Authentication\Login\Chain;
use OC\Authentication\Login\LoginData;
use OC\Authentication\WebAuthn\Manager as WebAuthnManager;
use OC\User\Session;
use OC_App;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\Attribute\UseSession;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\Defaults;
use OCP\IConfig;
use OCP\IInitialStateService;
use OCP\IL10N;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Notification\IManager;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Util;
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class LoginController extends Controller {
public const LOGIN_MSG_INVALIDPASSWORD = 'invalidpassword';
public const LOGIN_MSG_USERDISABLED = 'userdisabled';
public const LOGIN_MSG_CSRFCHECKFAILED = 'csrfCheckFailed';
public function __construct(
?string $appName,
IRequest $request,
private IUserManager $userManager,
private IConfig $config,
private ISession $session,
private Session $userSession,
private IURLGenerator $urlGenerator,
private Defaults $defaults,
private IThrottler $throttler,
private IInitialStateService $initialStateService,
private WebAuthnManager $webAuthnManager,
private IManager $manager,
private IL10N $l10n,
) {
parent::__construct($appName, $request);
}
/**
* @NoAdminRequired
*
* @return RedirectResponse
*/
#[UseSession]
public function logout() {
$loginToken = $this->request->getCookie('nc_token');
if (!is_null($loginToken)) {
$this->config->deleteUserValue($this->userSession->getUser()->getUID(), 'login_token', $loginToken);
}
$this->userSession->logout();
$response = new RedirectResponse($this->urlGenerator->linkToRouteAbsolute(
'core.login.showLoginForm',
['clear' => true] // this param the code in login.js may be removed when the "Clear-Site-Data" is working in the browsers
));
$this->session->set('clearingExecutionContexts', '1');
$this->session->close();
if (
$this->request->getServerProtocol() === 'https' &&
!$this->request->isUserAgent([Request::USER_AGENT_CHROME, Request::USER_AGENT_ANDROID_MOBILE_CHROME])
) {
$response->addHeader('Clear-Site-Data', '"cache", "storage"');
}
return $response;
}
/**
* @PublicPage
* @NoCSRFRequired
*
* @param string $user
* @param string $redirect_url
*
* @return TemplateResponse|RedirectResponse
*/
#[UseSession]
public function showLoginForm(string $user = null, string $redirect_url = null): Http\Response {
if ($this->userSession->isLoggedIn()) {
return new RedirectResponse($this->urlGenerator->linkToDefaultPageUrl());
}
$loginMessages = $this->session->get('loginMessages');
if (!$this->manager->isFairUseOfFreePushService()) {
if (!is_array($loginMessages)) {
$loginMessages = [[], []];
}
$loginMessages[1][] = $this->l10n->t('This community release of Nextcloud is unsupported and push notifications are limited.');
}
if (is_array($loginMessages)) {
[$errors, $messages] = $loginMessages;
$this->initialStateService->provideInitialState('core', 'loginMessages', $messages);
$this->initialStateService->provideInitialState('core', 'loginErrors', $errors);
}
$this->session->remove('loginMessages');
if ($user !== null && $user !== '') {
$this->initialStateService->provideInitialState('core', 'loginUsername', $user);
} else {
$this->initialStateService->provideInitialState('core', 'loginUsername', '');
}
$this->initialStateService->provideInitialState(
'core',
'loginAutocomplete',
$this->config->getSystemValue('login_form_autocomplete', true) === true
);
if (!empty($redirect_url)) {
[$url, ] = explode('?', $redirect_url);
if ($url !== $this->urlGenerator->linkToRoute('core.login.logout')) {
$this->initialStateService->provideInitialState('core', 'loginRedirectUrl', $redirect_url);
}
}
$this->initialStateService->provideInitialState(
'core',
'loginThrottleDelay',
$this->throttler->getDelay($this->request->getRemoteAddress())
);
$this->setPasswordResetInitialState($user);
$this->initialStateService->provideInitialState('core', 'webauthn-available', $this->webAuthnManager->isWebAuthnAvailable());
$this->initialStateService->provideInitialState('core', 'hideLoginForm', $this->config->getSystemValueBool('hide_login_form', false));
// OpenGraph Support: http://ogp.me/
Util::addHeader('meta', ['property' => 'og:title', 'content' => Util::sanitizeHTML($this->defaults->getName())]);
Util::addHeader('meta', ['property' => 'og:description', 'content' => Util::sanitizeHTML($this->defaults->getSlogan())]);
Util::addHeader('meta', ['property' => 'og:site_name', 'content' => Util::sanitizeHTML($this->defaults->getName())]);
Util::addHeader('meta', ['property' => 'og:url', 'content' => $this->urlGenerator->getAbsoluteURL('/')]);
Util::addHeader('meta', ['property' => 'og:type', 'content' => 'website']);
Util::addHeader('meta', ['property' => 'og:image', 'content' => $this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('core', 'favicon-touch.png'))]);
$parameters = [
'alt_login' => OC_App::getAlternativeLogIns(),
'pageTitle' => $this->l10n->t('Login'),
];
$this->initialStateService->provideInitialState('core', 'countAlternativeLogins', count($parameters['alt_login']));
$this->initialStateService->provideInitialState('core', 'alternativeLogins', $parameters['alt_login']);
return new TemplateResponse(
$this->appName,
'login',
$parameters,
TemplateResponse::RENDER_AS_GUEST,
);
}
/**
* Sets the password reset state
*
* @param string $username
*/
private function setPasswordResetInitialState(?string $username): void {
if ($username !== null && $username !== '') {
$user = $this->userManager->get($username);
} else {
$user = null;
}
$passwordLink = $this->config->getSystemValueString('lost_password_link', '');
$this->initialStateService->provideInitialState(
'core',
'loginResetPasswordLink',
$passwordLink
);
$this->initialStateService->provideInitialState(
'core',
'loginCanResetPassword',
$this->canResetPassword($passwordLink, $user)
);
}
/**
* @param string|null $passwordLink
* @param IUser|null $user
*
* Users may not change their passwords if:
* - The account is disabled
* - The backend doesn't support password resets
* - The password reset function is disabled
*
* @return bool
*/
private function canResetPassword(?string $passwordLink, ?IUser $user): bool {
if ($passwordLink === 'disabled') {
return false;
}
if (!$passwordLink && $user !== null) {
return $user->canChangePassword();
}
if ($user !== null && $user->isEnabled() === false) {
return false;
}
return true;
}
private function generateRedirect(?string $redirectUrl): RedirectResponse {
if ($redirectUrl !== null && $this->userSession->isLoggedIn()) {
$location = $this->urlGenerator->getAbsoluteURL($redirectUrl);
// Deny the redirect if the URL contains a @
// This prevents unvalidated redirects like ?redirect_url=:user@domain.com
if (!str_contains($location, '@')) {
return new RedirectResponse($location);
}
}
return new RedirectResponse($this->urlGenerator->linkToDefaultPageUrl());
}
/**
* @PublicPage
* @NoCSRFRequired
* @BruteForceProtection(action=login)
*
* @return RedirectResponse
*/
#[UseSession]
public function tryLogin(Chain $loginChain,
string $user = '',
string $password = '',
string $redirect_url = null,
string $timezone = '',
string $timezone_offset = ''): RedirectResponse {
if (!$this->request->passesCSRFCheck()) {
if ($this->userSession->isLoggedIn()) {
// If the user is already logged in and the CSRF check does not pass then
// simply redirect the user to the correct page as required. This is the
// case when a user has already logged-in, in another tab.
return $this->generateRedirect($redirect_url);
}
// Clear any auth remnants like cookies to ensure a clean login
// For the next attempt
$this->userSession->logout();
return $this->createLoginFailedResponse(
$user,
$user,
$redirect_url,
self::LOGIN_MSG_CSRFCHECKFAILED
);
}
$data = new LoginData(
$this->request,
trim($user),
$password,
$redirect_url,
$timezone,
$timezone_offset
);
$result = $loginChain->process($data);
if (!$result->isSuccess()) {
return $this->createLoginFailedResponse(
$data->getUsername(),
$user,
$redirect_url,
$result->getErrorMessage()
);
}
if ($result->getRedirectUrl() !== null) {
return new RedirectResponse($result->getRedirectUrl());
}
return $this->generateRedirect($redirect_url);
}
/**
* Creates a login failed response.
*
* @param string $user
* @param string $originalUser
* @param string $redirect_url
* @param string $loginMessage
*
* @return RedirectResponse
*/
private function createLoginFailedResponse(
$user, $originalUser, $redirect_url, string $loginMessage) {
// Read current user and append if possible we need to
// return the unmodified user otherwise we will leak the login name
$args = $user !== null ? ['user' => $originalUser, 'direct' => 1] : [];
if ($redirect_url !== null) {
$args['redirect_url'] = $redirect_url;
}
$response = new RedirectResponse(
$this->urlGenerator->linkToRoute('core.login.showLoginForm', $args)
);
$response->throttle(['user' => substr($user, 0, 64)]);
$this->session->set('loginMessages', [
[$loginMessage], []
]);
return $response;
}
/**
* @NoAdminRequired
* @BruteForceProtection(action=sudo)
*
* @license GNU AGPL version 3 or any later version
*
*/
#[UseSession]
public function confirmPassword(string $password): DataResponse {
$loginName = $this->userSession->getLoginName();
$loginResult = $this->userManager->checkPassword($loginName, $password);
if ($loginResult === false) {
$response = new DataResponse([], Http::STATUS_FORBIDDEN);
$response->throttle(['loginName' => $loginName]);
return $response;
}
$confirmTimestamp = time();
$this->session->set('last-password-confirm', $confirmTimestamp);
$this->throttler->resetDelay($this->request->getRemoteAddress(), 'sudo', ['loginName' => $loginName]);
return new DataResponse(['lastLogin' => $confirmTimestamp], Http::STATUS_OK);
}
}
@@ -0,0 +1,337 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bernhard Posselt <dev@bernhard-posselt.com>
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Haertl <jus@bitgrid.net>
* @author Julius Härtl <jus@bitgrid.net>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Rémy Jacquin <remy@remyj.fr>
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
* @author Victor Dubiniuk <dubiniuk@owncloud.com>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OC\Core\Controller;
use Exception;
use OC\Authentication\TwoFactorAuth\Manager;
use OC\Core\Events\BeforePasswordResetEvent;
use OC\Core\Events\PasswordResetEvent;
use OC\Core\Exception\ResetPasswordException;
use OC\Security\RateLimiting\Exception\RateLimitExceededException;
use OC\Security\RateLimiting\Limiter;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\Defaults;
use OCP\Encryption\IEncryptionModule;
use OCP\Encryption\IManager;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\HintException;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Mail\IMailer;
use OCP\Security\VerificationToken\InvalidTokenException;
use OCP\Security\VerificationToken\IVerificationToken;
use Psr\Log\LoggerInterface;
use function array_filter;
use function count;
use function reset;
/**
* Class LostController
*
* Successfully changing a password will emit the post_passwordReset hook.
*
* @package OC\Core\Controller
*/
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class LostController extends Controller {
protected string $from;
public function __construct(
string $appName,
IRequest $request,
private IURLGenerator $urlGenerator,
private IUserManager $userManager,
private Defaults $defaults,
private IL10N $l10n,
private IConfig $config,
string $defaultMailAddress,
private IManager $encryptionManager,
private IMailer $mailer,
private LoggerInterface $logger,
private Manager $twoFactorManager,
private IInitialState $initialState,
private IVerificationToken $verificationToken,
private IEventDispatcher $eventDispatcher,
private Limiter $limiter,
) {
parent::__construct($appName, $request);
$this->from = $defaultMailAddress;
}
/**
* Someone wants to reset their password:
*
* @PublicPage
* @NoCSRFRequired
* @BruteForceProtection(action=passwordResetEmail)
* @AnonRateThrottle(limit=10, period=300)
*/
public function resetform(string $token, string $userId): TemplateResponse {
try {
$this->checkPasswordResetToken($token, $userId);
} catch (Exception $e) {
if ($this->config->getSystemValue('lost_password_link', '') !== 'disabled'
|| ($e instanceof InvalidTokenException
&& !in_array($e->getCode(), [InvalidTokenException::TOKEN_NOT_FOUND, InvalidTokenException::USER_UNKNOWN]))
) {
$response = new TemplateResponse(
'core', 'error', [
"errors" => [["error" => $e->getMessage()]]
],
TemplateResponse::RENDER_AS_GUEST
);
$response->throttle();
return $response;
}
return new TemplateResponse('core', 'error', [
'errors' => [['error' => $this->l10n->t('Password reset is disabled')]]
],
TemplateResponse::RENDER_AS_GUEST
);
}
$this->initialState->provideInitialState('resetPasswordUser', $userId);
$this->initialState->provideInitialState('resetPasswordTarget',
$this->urlGenerator->linkToRouteAbsolute('core.lost.setPassword', ['userId' => $userId, 'token' => $token])
);
return new TemplateResponse(
'core',
'login',
[],
'guest'
);
}
/**
* @throws Exception
*/
protected function checkPasswordResetToken(string $token, string $userId): void {
try {
$user = $this->userManager->get($userId);
$this->verificationToken->check($token, $user, 'lostpassword', $user ? $user->getEMailAddress() : '', true);
} catch (InvalidTokenException $e) {
$error = $e->getCode() === InvalidTokenException::TOKEN_EXPIRED
? $this->l10n->t('Could not reset password because the token is expired')
: $this->l10n->t('Could not reset password because the token is invalid');
throw new Exception($error, (int)$e->getCode(), $e);
}
}
private function error(string $message, array $additional = []): array {
return array_merge(['status' => 'error', 'msg' => $message], $additional);
}
private function success(array $data = []): array {
return array_merge($data, ['status' => 'success']);
}
/**
* @PublicPage
* @BruteForceProtection(action=passwordResetEmail)
* @AnonRateThrottle(limit=10, period=300)
*/
public function email(string $user): JSONResponse {
if ($this->config->getSystemValue('lost_password_link', '') !== '') {
return new JSONResponse($this->error($this->l10n->t('Password reset is disabled')));
}
$user = trim($user);
\OCP\Util::emitHook(
'\OCA\Files_Sharing\API\Server2Server',
'preLoginNameUsedAsUserName',
['uid' => &$user]
);
// FIXME: use HTTP error codes
try {
$this->sendEmail($user);
} catch (ResetPasswordException $e) {
// Ignore the error since we do not want to leak this info
$this->logger->warning('Could not send password reset email: ' . $e->getMessage());
} catch (Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
}
$response = new JSONResponse($this->success());
$response->throttle();
return $response;
}
/**
* @PublicPage
* @BruteForceProtection(action=passwordResetEmail)
* @AnonRateThrottle(limit=10, period=300)
*/
public function setPassword(string $token, string $userId, string $password, bool $proceed): JSONResponse {
if ($this->encryptionManager->isEnabled() && !$proceed) {
$encryptionModules = $this->encryptionManager->getEncryptionModules();
foreach ($encryptionModules as $module) {
/** @var IEncryptionModule $instance */
$instance = call_user_func($module['callback']);
// this way we can find out whether per-user keys are used or a system wide encryption key
if ($instance->needDetailedAccessList()) {
return new JSONResponse($this->error('', ['encryption' => true]));
}
}
}
try {
$this->checkPasswordResetToken($token, $userId);
$user = $this->userManager->get($userId);
$this->eventDispatcher->dispatchTyped(new BeforePasswordResetEvent($user, $password));
\OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'pre_passwordReset', ['uid' => $userId, 'password' => $password]);
if (strlen($password) > IUserManager::MAX_PASSWORD_LENGTH) {
throw new HintException('Password too long', $this->l10n->t('Password is too long. Maximum allowed length is 469 characters.'));
}
if (!$user->setPassword($password)) {
throw new Exception();
}
$this->eventDispatcher->dispatchTyped(new PasswordResetEvent($user, $password));
\OC_Hook::emit('\OC\Core\LostPassword\Controller\LostController', 'post_passwordReset', ['uid' => $userId, 'password' => $password]);
$this->twoFactorManager->clearTwoFactorPending($userId);
$this->config->deleteUserValue($userId, 'core', 'lostpassword');
@\OC::$server->getUserSession()->unsetMagicInCookie();
} catch (HintException $e) {
$response = new JSONResponse($this->error($e->getHint()));
$response->throttle();
return $response;
} catch (Exception $e) {
$response = new JSONResponse($this->error($e->getMessage()));
$response->throttle();
return $response;
}
return new JSONResponse($this->success(['user' => $userId]));
}
/**
* @throws ResetPasswordException
* @throws \OCP\PreConditionNotMetException
*/
protected function sendEmail(string $input): void {
$user = $this->findUserByIdOrMail($input);
$email = $user->getEMailAddress();
if (empty($email)) {
throw new ResetPasswordException('Could not send reset e-mail since there is no email for username ' . $input);
}
try {
$this->limiter->registerUserRequest('lostpasswordemail', 5, 1800, $user);
} catch (RateLimitExceededException $e) {
throw new ResetPasswordException('Could not send reset e-mail, 5 of them were already sent in the last 30 minutes', 0, $e);
}
// Generate the token. It is stored encrypted in the database with the
// secret being the users' email address appended with the system secret.
// This makes the token automatically invalidate once the user changes
// their email address.
$token = $this->verificationToken->create($user, 'lostpassword', $email);
$link = $this->urlGenerator->linkToRouteAbsolute('core.lost.resetform', ['userId' => $user->getUID(), 'token' => $token]);
$emailTemplate = $this->mailer->createEMailTemplate('core.ResetPassword', [
'link' => $link,
]);
$emailTemplate->setSubject($this->l10n->t('%s password reset', [$this->defaults->getName()]));
$emailTemplate->addHeader();
$emailTemplate->addHeading($this->l10n->t('Password reset'));
$emailTemplate->addBodyText(
htmlspecialchars($this->l10n->t('Click the following button to reset your password. If you have not requested the password reset, then ignore this email.')),
$this->l10n->t('Click the following link to reset your password. If you have not requested the password reset, then ignore this email.')
);
$emailTemplate->addBodyButton(
htmlspecialchars($this->l10n->t('Reset your password')),
$link,
false
);
$emailTemplate->addFooter();
try {
$message = $this->mailer->createMessage();
$message->setTo([$email => $user->getDisplayName()]);
$message->setFrom([$this->from => $this->defaults->getName()]);
$message->useTemplate($emailTemplate);
$this->mailer->send($message);
} catch (Exception $e) {
// Log the exception and continue
$this->logger->error($e->getMessage(), ['app' => 'core', 'exception' => $e]);
}
}
/**
* @throws ResetPasswordException
*/
protected function findUserByIdOrMail(string $input): IUser {
$user = $this->userManager->get($input);
if ($user instanceof IUser) {
if (!$user->isEnabled()) {
throw new ResetPasswordException('User ' . $user->getUID() . ' is disabled');
}
return $user;
}
$users = array_filter($this->userManager->getByEmail($input), function (IUser $user) {
return $user->isEnabled();
});
if (count($users) === 1) {
return reset($users);
}
throw new ResetPasswordException('Could not find user ' . $input);
}
}
@@ -0,0 +1,128 @@
<?php
/**
* @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OCA\Core\ResponseDefinitions;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\INavigationManager;
use OCP\IRequest;
use OCP\IURLGenerator;
/**
* @psalm-import-type CoreNavigationEntry from ResponseDefinitions
*/
class NavigationController extends OCSController {
public function __construct(
string $appName,
IRequest $request,
private INavigationManager $navigationManager,
private IURLGenerator $urlGenerator,
) {
parent::__construct($appName, $request);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* Get the apps navigation
*
* @param bool $absolute Rewrite URLs to absolute ones
* @return DataResponse<Http::STATUS_OK, CoreNavigationEntry[], array{}>|DataResponse<Http::STATUS_NOT_MODIFIED, array<empty>, array{}>
*
* 200: Apps navigation returned
* 304: No apps navigation changed
*/
public function getAppsNavigation(bool $absolute = false): DataResponse {
$navigation = $this->navigationManager->getAll();
if ($absolute) {
$navigation = $this->rewriteToAbsoluteUrls($navigation);
}
$navigation = array_values($navigation);
$etag = $this->generateETag($navigation);
if ($this->request->getHeader('If-None-Match') === $etag) {
return new DataResponse([], Http::STATUS_NOT_MODIFIED);
}
$response = new DataResponse($navigation);
$response->setETag($etag);
return $response;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* Get the settings navigation
*
* @param bool $absolute Rewrite URLs to absolute ones
* @return DataResponse<Http::STATUS_OK, CoreNavigationEntry[], array{}>|DataResponse<Http::STATUS_NOT_MODIFIED, array<empty>, array{}>
*
* 200: Apps navigation returned
* 304: No apps navigation changed
*/
public function getSettingsNavigation(bool $absolute = false): DataResponse {
$navigation = $this->navigationManager->getAll('settings');
if ($absolute) {
$navigation = $this->rewriteToAbsoluteUrls($navigation);
}
$navigation = array_values($navigation);
$etag = $this->generateETag($navigation);
if ($this->request->getHeader('If-None-Match') === $etag) {
return new DataResponse([], Http::STATUS_NOT_MODIFIED);
}
$response = new DataResponse($navigation);
$response->setETag($etag);
return $response;
}
/**
* Generate an ETag for a list of navigation entries
*/
private function generateETag(array $navigation): string {
foreach ($navigation as &$nav) {
if ($nav['id'] === 'logout') {
$nav['href'] = 'logout';
}
}
return md5(json_encode($navigation));
}
/**
* Rewrite href attribute of navigation entries to an absolute URL
*/
private function rewriteToAbsoluteUrls(array $navigation): array {
foreach ($navigation as &$entry) {
/* If parse_url finds no host it means the URL is not absolute */
if (!isset(\parse_url($entry['href'])['host'])) {
$entry['href'] = $this->urlGenerator->getAbsoluteURL($entry['href']);
}
if (!str_starts_with($entry['icon'], $this->urlGenerator->getBaseUrl())) {
$entry['icon'] = $this->urlGenerator->getAbsoluteURL($entry['icon']);
}
}
return $navigation;
}
}
@@ -0,0 +1,95 @@
<?php
/**
* @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Bjoern Schiessle <bjoern@schiessle.org>
* @author Joas Schilling <coding@schilljs.com>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use bantu\IniGetWrapper\IniGetWrapper;
use OC\CapabilitiesManager;
use OC\Template\JSConfigHelper;
use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\DataDisplayResponse;
use OCP\Defaults;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IInitialStateService;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\L10N\IFactory;
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class OCJSController extends Controller {
private JSConfigHelper $helper;
public function __construct(
string $appName,
IRequest $request,
IFactory $l10nFactory,
Defaults $defaults,
IAppManager $appManager,
ISession $session,
IUserSession $userSession,
IConfig $config,
IGroupManager $groupManager,
IniGetWrapper $iniWrapper,
IURLGenerator $urlGenerator,
CapabilitiesManager $capabilitiesManager,
IInitialStateService $initialStateService,
) {
parent::__construct($appName, $request);
$this->helper = new JSConfigHelper(
$l10nFactory->get('lib'),
$defaults,
$appManager,
$session,
$userSession->getUser(),
$config,
$groupManager,
$iniWrapper,
$urlGenerator,
$capabilitiesManager,
$initialStateService
);
}
/**
* @NoCSRFRequired
* @NoTwoFactorRequired
* @PublicPage
*/
public function getConfig(): DataDisplayResponse {
$data = $this->helper->getConfig();
return new DataDisplayResponse($data, Http::STATUS_OK, ['Content-type' => 'text/javascript']);
}
}
@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Maxence Lange <maxence@artificial-owl.com>
*
* @author Maxence Lange <maxence@artificial-owl.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use Exception;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\Capabilities\ICapability;
use OCP\IConfig;
use OCP\IRequest;
use OCP\Server;
use Psr\Container\ContainerExceptionInterface;
use Psr\Log\LoggerInterface;
/**
* Controller about the endpoint /ocm-provider/
*
* @since 28.0.0
*/
class OCMController extends Controller {
public function __construct(
IRequest $request,
private IConfig $config,
private LoggerInterface $logger
) {
parent::__construct('core', $request);
}
/**
* generate a OCMProvider with local data and send it as DataResponse.
* This replaces the old PHP file ocm-provider/index.php
*
* @PublicPage
* @NoCSRFRequired
* @psalm-suppress MoreSpecificReturnType
* @psalm-suppress LessSpecificReturnStatement
* @return DataResponse<Http::STATUS_OK, array{enabled: bool, apiVersion: string, endPoint: string, resourceTypes: array{name: string, shareTypes: string[], protocols: array{webdav: string}}[]}, array{X-NEXTCLOUD-OCM-PROVIDERS: true, Content-Type: 'application/json'}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: OCM Provider details returned
* 500: OCM not supported
*/
public function discovery(): DataResponse {
try {
$cap = Server::get(
$this->config->getAppValue(
'core',
'ocm_providers',
'\OCA\CloudFederationAPI\Capabilities'
)
);
if (!($cap instanceof ICapability)) {
throw new Exception('loaded class does not implements OCP\Capabilities\ICapability');
}
return new DataResponse(
$cap->getCapabilities()['ocm'] ?? ['enabled' => false],
Http::STATUS_OK,
[
'X-NEXTCLOUD-OCM-PROVIDERS' => true,
'Content-Type' => 'application/json'
]
);
} catch (ContainerExceptionInterface|Exception $e) {
$this->logger->error('issue during OCM discovery request', ['exception' => $e]);
return new DataResponse(
['message' => '/ocm-provider/ not supported'],
Http::STATUS_INTERNAL_SERVER_ERROR
);
}
}
}
@@ -0,0 +1,139 @@
<?php
/**
* @copyright Copyright (c) 2016 Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
* @author Joas Schilling <coding@schilljs.com>
* @author Julius Härtl <jus@bitgrid.net>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OC\CapabilitiesManager;
use OC\Security\IdentityProof\Manager;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\DataResponse;
use OCP\IRequest;
use OCP\IUserManager;
use OCP\IUserSession;
class OCSController extends \OCP\AppFramework\OCSController {
public function __construct(
string $appName,
IRequest $request,
private CapabilitiesManager $capabilitiesManager,
private IUserSession $userSession,
private IUserManager $userManager,
private Manager $keyManager,
) {
parent::__construct($appName, $request);
}
/**
* @PublicPage
*/
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
public function getConfig(): DataResponse {
$data = [
'version' => '1.7',
'website' => 'Nextcloud',
'host' => $this->request->getServerHost(),
'contact' => '',
'ssl' => 'false',
];
return new DataResponse($data);
}
/**
* @PublicPage
*
* Get the capabilities
*
* @return DataResponse<Http::STATUS_OK, array{version: array{major: int, minor: int, micro: int, string: string, edition: '', extendedSupport: bool}, capabilities: array<string, mixed>}, array{}>
*
* 200: Capabilities returned
*/
public function getCapabilities(): DataResponse {
$result = [];
[$major, $minor, $micro] = \OCP\Util::getVersion();
$result['version'] = [
'major' => (int)$major,
'minor' => (int)$minor,
'micro' => (int)$micro,
'string' => \OC_Util::getVersionString(),
'edition' => '',
'extendedSupport' => \OCP\Util::hasExtendedSupport()
];
if ($this->userSession->isLoggedIn()) {
$result['capabilities'] = $this->capabilitiesManager->getCapabilities();
} else {
$result['capabilities'] = $this->capabilitiesManager->getCapabilities(true);
}
$response = new DataResponse($result);
$response->setETag(md5(json_encode($result)));
return $response;
}
/**
* @PublicPage
* @BruteForceProtection(action=login)
*/
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
public function personCheck(string $login = '', string $password = ''): DataResponse {
if ($login !== '' && $password !== '') {
if ($this->userManager->checkPassword($login, $password)) {
return new DataResponse([
'person' => [
'personid' => $login
]
]);
}
$response = new DataResponse([], 102);
$response->throttle();
return $response;
}
return new DataResponse([], 101);
}
/**
* @PublicPage
*/
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
public function getIdentityProof(string $cloudId): DataResponse {
$userObject = $this->userManager->get($cloudId);
if ($userObject !== null) {
$key = $this->keyManager->getKey($userObject);
$data = [
'public' => $key->getPublic(),
];
return new DataResponse($data);
}
return new DataResponse(['User not found'], 404);
}
}
@@ -0,0 +1,192 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OCA\Files_Sharing\SharedStorage;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\IPreview;
use OCP\IRequest;
use OCP\Preview\IMimeIconProvider;
class PreviewController extends Controller {
public function __construct(
string $appName,
IRequest $request,
private IPreview $preview,
private IRootFolder $root,
private ?string $userId,
private IMimeIconProvider $mimeIconProvider,
) {
parent::__construct($appName, $request);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* Get a preview by file path
*
* @param string $file Path of the file
* @param int $x Width of the preview
* @param int $y Height of the preview
* @param bool $a Whether to not crop the preview
* @param bool $forceIcon Force returning an icon
* @param string $mode How to crop the image
* @param bool $mimeFallback Whether to fallback to the mime icon if no preview is available
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}>
*
* 200: Preview returned
* 303: Redirect to the mime icon url if mimeFallback is true
* 400: Getting preview is not possible
* 403: Getting preview is not allowed
* 404: Preview not found
*/
public function getPreview(
string $file = '',
int $x = 32,
int $y = 32,
bool $a = false,
bool $forceIcon = true,
string $mode = 'fill',
bool $mimeFallback = false): Http\Response {
if ($file === '' || $x === 0 || $y === 0) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
try {
$userFolder = $this->root->getUserFolder($this->userId);
$node = $userFolder->get($file);
} catch (NotFoundException $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
return $this->fetchPreview($node, $x, $y, $a, $forceIcon, $mode, $mimeFallback);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* Get a preview by file ID
*
* @param int $fileId ID of the file
* @param int $x Width of the preview
* @param int $y Height of the preview
* @param bool $a Whether to not crop the preview
* @param bool $forceIcon Force returning an icon
* @param string $mode How to crop the image
* @param bool $mimeFallback Whether to fallback to the mime icon if no preview is available
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}>
*
* 200: Preview returned
* 303: Redirect to the mime icon url if mimeFallback is true
* 400: Getting preview is not possible
* 403: Getting preview is not allowed
* 404: Preview not found
*/
public function getPreviewByFileId(
int $fileId = -1,
int $x = 32,
int $y = 32,
bool $a = false,
bool $forceIcon = true,
string $mode = 'fill',
bool $mimeFallback = false) {
if ($fileId === -1 || $x === 0 || $y === 0) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
$userFolder = $this->root->getUserFolder($this->userId);
$nodes = $userFolder->getById($fileId);
if (\count($nodes) === 0) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
$node = array_pop($nodes);
return $this->fetchPreview($node, $x, $y, $a, $forceIcon, $mode, $mimeFallback);
}
/**
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, array<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}>
*/
private function fetchPreview(
Node $node,
int $x,
int $y,
bool $a,
bool $forceIcon,
string $mode,
bool $mimeFallback = false) : Http\Response {
if (!($node instanceof File) || (!$forceIcon && !$this->preview->isAvailable($node))) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
if (!$node->isReadable()) {
return new DataResponse([], Http::STATUS_FORBIDDEN);
}
$storage = $node->getStorage();
if ($storage->instanceOfStorage(SharedStorage::class)) {
/** @var SharedStorage $storage */
$share = $storage->getShare();
$attributes = $share->getAttributes();
if ($attributes !== null && $attributes->getAttribute('permissions', 'download') === false) {
return new DataResponse([], Http::STATUS_FORBIDDEN);
}
}
try {
$f = $this->preview->getPreview($node, $x, $y, !$a, $mode);
$response = new FileDisplayResponse($f, Http::STATUS_OK, [
'Content-Type' => $f->getMimeType(),
]);
$response->cacheFor(3600 * 24, false, true);
return $response;
} catch (NotFoundException $e) {
// If we have no preview enabled, we can redirect to the mime icon if any
if ($mimeFallback) {
if ($url = $this->mimeIconProvider->getMimeIconUrl($node->getMimeType())) {
return new RedirectResponse($url);
}
}
return new DataResponse([], Http::STATUS_NOT_FOUND);
} catch (\InvalidArgumentException $e) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
}
}
@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OC\Core\Db\ProfileConfigMapper;
use OC\Profile\ProfileManager;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\AppFramework\OCS\OCSForbiddenException;
use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
use OCP\IUserManager;
use OCP\IUserSession;
class ProfileApiController extends OCSController {
public function __construct(
IRequest $request,
private ProfileConfigMapper $configMapper,
private ProfileManager $profileManager,
private IUserManager $userManager,
private IUserSession $userSession,
) {
parent::__construct('core', $request);
}
/**
* @NoAdminRequired
* @NoSubAdminRequired
* @PasswordConfirmationRequired
* @UserRateThrottle(limit=40, period=600)
*
* Update the visibility of a parameter
*
* @param string $targetUserId ID of the user
* @param string $paramId ID of the parameter
* @param string $visibility New visibility
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>
* @throws OCSBadRequestException Updating visibility is not possible
* @throws OCSForbiddenException Not allowed to edit other users visibility
* @throws OCSNotFoundException User not found
*
* 200: Visibility updated successfully
*/
public function setVisibility(string $targetUserId, string $paramId, string $visibility): DataResponse {
$requestingUser = $this->userSession->getUser();
$targetUser = $this->userManager->get($targetUserId);
if (!$this->userManager->userExists($targetUserId)) {
throw new OCSNotFoundException('User does not exist');
}
if ($requestingUser !== $targetUser) {
throw new OCSForbiddenException('Users can only edit their own visibility settings');
}
// Ensure that a profile config is created in the database
$this->profileManager->getProfileConfig($targetUser, $targetUser);
$config = $this->configMapper->get($targetUserId);
if (!in_array($paramId, array_keys($config->getVisibilityMap()), true)) {
throw new OCSBadRequestException('User does not have a profile parameter with ID: ' . $paramId);
}
$config->setVisibility($paramId, $visibility);
$this->configMapper->update($config);
return new DataResponse();
}
}
@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OC\Profile\ProfileManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\INavigationManager;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Profile\BeforeTemplateRenderedEvent;
use OCP\Share\IManager as IShareManager;
use OCP\UserStatus\IManager as IUserStatusManager;
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class ProfilePageController extends Controller {
public function __construct(
string $appName,
IRequest $request,
private IInitialState $initialStateService,
private ProfileManager $profileManager,
private IShareManager $shareManager,
private IUserManager $userManager,
private IUserSession $userSession,
private IUserStatusManager $userStatusManager,
private INavigationManager $navigationManager,
private IEventDispatcher $eventDispatcher,
) {
parent::__construct($appName, $request);
}
/**
* @PublicPage
* @NoCSRFRequired
* @NoAdminRequired
* @NoSubAdminRequired
*/
public function index(string $targetUserId): TemplateResponse {
$profileNotFoundTemplate = new TemplateResponse(
'core',
'404-profile',
[],
TemplateResponse::RENDER_AS_GUEST,
);
$targetUser = $this->userManager->get($targetUserId);
if (!($targetUser instanceof IUser) || !$targetUser->isEnabled()) {
return $profileNotFoundTemplate;
}
$visitingUser = $this->userSession->getUser();
if (!$this->profileManager->isProfileEnabled($targetUser)) {
return $profileNotFoundTemplate;
}
// Run user enumeration checks only if viewing another user's profile
if ($targetUser !== $visitingUser) {
if (!$this->shareManager->currentUserCanEnumerateTargetUser($visitingUser, $targetUser)) {
return $profileNotFoundTemplate;
}
}
if ($visitingUser !== null) {
$userStatuses = $this->userStatusManager->getUserStatuses([$targetUserId]);
$status = $userStatuses[$targetUserId] ?? null;
if ($status !== null) {
$this->initialStateService->provideInitialState('status', [
'icon' => $status->getIcon(),
'message' => $status->getMessage(),
]);
}
}
$this->initialStateService->provideInitialState(
'profileParameters',
$this->profileManager->getProfileFields($targetUser, $visitingUser),
);
if ($targetUser === $visitingUser) {
$this->navigationManager->setActiveEntry('profile');
}
$this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($targetUserId));
\OCP\Util::addScript('core', 'profile');
return new TemplateResponse(
'core',
'profile',
[],
$this->userSession->isLoggedIn() ? TemplateResponse::RENDER_AS_USER : TemplateResponse::RENDER_AS_PUBLIC,
);
}
}
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/**
* @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\StandaloneTemplateResponse;
use OCP\IInitialStateService;
use OCP\IRequest;
use OCP\IURLGenerator;
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class RecommendedAppsController extends Controller {
public function __construct(
IRequest $request,
public IURLGenerator $urlGenerator,
private IInitialStateService $initialStateService,
) {
parent::__construct('core', $request);
}
/**
* @NoCSRFRequired
* @return Response
*/
public function index(): Response {
$defaultPageUrl = $this->urlGenerator->linkToDefaultPageUrl();
$this->initialStateService->provideInitialState('core', 'defaultPageUrl', $defaultPageUrl);
return new StandaloneTemplateResponse($this->appName, 'recommendedapps', [], 'guest');
}
}
@@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OCA\Core\ResponseDefinitions;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\Collaboration\Reference\IDiscoverableReferenceProvider;
use OCP\Collaboration\Reference\IReferenceManager;
use OCP\Collaboration\Reference\Reference;
use OCP\IRequest;
/**
* @psalm-import-type CoreReference from ResponseDefinitions
* @psalm-import-type CoreReferenceProvider from ResponseDefinitions
*/
class ReferenceApiController extends \OCP\AppFramework\OCSController {
public function __construct(
string $appName,
IRequest $request,
private IReferenceManager $referenceManager,
private ?string $userId,
) {
parent::__construct($appName, $request);
}
/**
* @NoAdminRequired
*
* Extract references from a text
*
* @param string $text Text to extract from
* @param bool $resolve Resolve the references
* @param int $limit Maximum amount of references to extract
* @return DataResponse<Http::STATUS_OK, array{references: array<string, CoreReference|null>}, array{}>
*
* 200: References returned
*/
public function extract(string $text, bool $resolve = false, int $limit = 1): DataResponse {
$references = $this->referenceManager->extractReferences($text);
$result = [];
$index = 0;
foreach ($references as $reference) {
if ($index++ >= $limit) {
break;
}
$result[$reference] = $resolve ? $this->referenceManager->resolveReference($reference)->jsonSerialize() : null;
}
return new DataResponse([
'references' => $result
]);
}
/**
* @NoAdminRequired
*
* Resolve a reference
*
* @param string $reference Reference to resolve
* @return DataResponse<Http::STATUS_OK, array{references: array<string, ?CoreReference>}, array{}>
*
* 200: Reference returned
*/
public function resolveOne(string $reference): DataResponse {
/** @var ?CoreReference $resolvedReference */
$resolvedReference = $this->referenceManager->resolveReference(trim($reference))?->jsonSerialize();
$response = new DataResponse(['references' => [$reference => $resolvedReference]]);
$response->cacheFor(3600, false, true);
return $response;
}
/**
* @NoAdminRequired
*
* Resolve multiple references
*
* @param string[] $references References to resolve
* @param int $limit Maximum amount of references to resolve
* @return DataResponse<Http::STATUS_OK, array{references: array<string, CoreReference|null>}, array{}>
*
* 200: References returned
*/
public function resolve(array $references, int $limit = 1): DataResponse {
$result = [];
$index = 0;
foreach ($references as $reference) {
if ($index++ >= $limit) {
break;
}
$result[$reference] = $this->referenceManager->resolveReference($reference)?->jsonSerialize();
}
return new DataResponse([
'references' => $result
]);
}
/**
* @NoAdminRequired
*
* Get the providers
*
* @return DataResponse<Http::STATUS_OK, CoreReferenceProvider[], array{}>
*
* 200: Providers returned
*/
public function getProvidersInfo(): DataResponse {
$providers = $this->referenceManager->getDiscoverableProviders();
$jsonProviders = array_map(static function (IDiscoverableReferenceProvider $provider) {
return $provider->jsonSerialize();
}, $providers);
return new DataResponse($jsonProviders);
}
/**
* @NoAdminRequired
*
* Touch a provider
*
* @param string $providerId ID of the provider
* @param int|null $timestamp Timestamp of the last usage
* @return DataResponse<Http::STATUS_OK, array{success: bool}, array{}>
*
* 200: Provider touched
*/
public function touchProvider(string $providerId, ?int $timestamp = null): DataResponse {
if ($this->userId !== null) {
$success = $this->referenceManager->touchProvider($this->userId, $providerId, $timestamp);
return new DataResponse(['success' => $success]);
}
return new DataResponse(['success' => false]);
}
}
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataDownloadResponse;
use OCP\AppFramework\Http\DataResponse;
use OCP\Collaboration\Reference\IReferenceManager;
use OCP\Files\AppData\IAppDataFactory;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IRequest;
class ReferenceController extends Controller {
public function __construct(
string $appName,
IRequest $request,
private IReferenceManager $referenceManager,
private IAppDataFactory $appDataFactory,
) {
parent::__construct($appName, $request);
}
/**
* @PublicPage
* @NoCSRFRequired
*
* Get a preview for a reference
*
* @param string $referenceId the reference cache key
* @return DataDownloadResponse<Http::STATUS_OK, string, array{}>|DataResponse<Http::STATUS_NOT_FOUND, '', array{}>
*
* 200: Preview returned
* 404: Reference not found
*/
public function preview(string $referenceId): DataDownloadResponse|DataResponse {
$reference = $this->referenceManager->getReferenceByCacheKey($referenceId);
try {
$appData = $this->appDataFactory->get('core');
$folder = $appData->getFolder('opengraph');
$file = $folder->getFile($referenceId);
$contentType = $reference === null || $reference->getImageContentType() === null
? $file->getMimeType()
: $reference->getImageContentType();
$response = new DataDownloadResponse(
$file->getContent(),
$referenceId,
$contentType
);
} catch (NotFoundException|NotPermittedException $e) {
$response = new DataResponse('', Http::STATUS_NOT_FOUND);
}
$response->cacheFor(3600, false, true);
return $response;
}
}
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/**
* @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Robin Appelman <robin@icewind.nl>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
use OCP\ISearch;
use OCP\Search\Result;
use Psr\Log\LoggerInterface;
class SearchController extends Controller {
public function __construct(
string $appName,
IRequest $request,
private ISearch $searcher,
private LoggerInterface $logger,
) {
parent::__construct($appName, $request);
}
/**
* @NoAdminRequired
*/
public function search(string $query, array $inApps = [], int $page = 1, int $size = 30): JSONResponse {
$results = $this->searcher->searchPaged($query, $inApps, $page, $size);
$results = array_filter($results, function (Result $result) {
if (json_encode($result, JSON_HEX_TAG) === false) {
$this->logger->warning("Skipping search result due to invalid encoding: {type: " . $result->type . ", id: " . $result->id . "}");
return false;
} else {
return true;
}
});
return new JSONResponse($results);
}
}
@@ -0,0 +1,144 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Bart Visscher <bartv@thisnet.nl>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Damjan Georgievski <gdamjan@gmail.com>
* @author ideaship <ideaship@users.noreply.github.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Robin Appelman <robin@icewind.nl>
* @author Robin McCorkell <robin@mccorkell.me.uk>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Thomas Müller <thomas.mueller@tmit.eu>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OC\Core\Controller;
use OC\Setup;
use OCP\Util;
use Psr\Log\LoggerInterface;
class SetupController {
private string $autoConfigFile;
public function __construct(
protected Setup $setupHelper,
protected LoggerInterface $logger,
) {
$this->autoConfigFile = \OC::$configDir.'autoconfig.php';
}
public function run(array $post): void {
// Check for autosetup:
$post = $this->loadAutoConfig($post);
$opts = $this->setupHelper->getSystemInfo();
// convert 'abcpassword' to 'abcpass'
if (isset($post['adminpassword'])) {
$post['adminpass'] = $post['adminpassword'];
}
if (isset($post['dbpassword'])) {
$post['dbpass'] = $post['dbpassword'];
}
if (!$this->setupHelper->canInstallFileExists()) {
$this->displaySetupForbidden();
return;
}
if (isset($post['install']) and $post['install'] == 'true') {
// We have to launch the installation process :
$e = $this->setupHelper->install($post);
$errors = ['errors' => $e];
if (count($e) > 0) {
$options = array_merge($opts, $post, $errors);
$this->display($options);
} else {
$this->finishSetup();
}
} else {
$options = array_merge($opts, $post);
$this->display($options);
}
}
private function displaySetupForbidden(): void {
\OC_Template::printGuestPage('', 'installation_forbidden');
}
public function display($post): void {
$defaults = [
'adminlogin' => '',
'adminpass' => '',
'dbuser' => '',
'dbpass' => '',
'dbname' => '',
'dbtablespace' => '',
'dbhost' => 'localhost',
'dbtype' => '',
];
$parameters = array_merge($defaults, $post);
Util::addStyle('server', null);
// include common nextcloud webpack bundle
Util::addScript('core', 'common');
Util::addScript('core', 'main');
Util::addTranslations('core');
\OC_Template::printGuestPage('', 'installation', $parameters);
}
private function finishSetup(): void {
if (file_exists($this->autoConfigFile)) {
unlink($this->autoConfigFile);
}
\OC::$server->getIntegrityCodeChecker()->runInstanceVerification();
if ($this->setupHelper->shouldRemoveCanInstallFile()) {
\OC_Template::printGuestPage('', 'installation_incomplete');
}
header('Location: ' . \OC::$server->getURLGenerator()->getAbsoluteURL('index.php/core/apps/recommended'));
exit();
}
public function loadAutoConfig(array $post): array {
if (file_exists($this->autoConfigFile)) {
$this->logger->info('Autoconfig file found, setting up Nextcloud…');
$AUTOCONFIG = [];
include $this->autoConfigFile;
$post = array_merge($post, $AUTOCONFIG);
}
$dbIsSet = isset($post['dbtype']);
$directoryIsSet = isset($post['directory']);
$adminAccountIsSet = isset($post['adminlogin']);
if ($dbIsSet and $directoryIsSet and $adminAccountIsSet) {
$post['install'] = 'true';
}
$post['dbIsSet'] = $dbIsSet;
$post['directoryIsSet'] = $directoryIsSet;
return $post;
}
}
@@ -0,0 +1,225 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use InvalidArgumentException;
use OCA\Core\ResponseDefinitions;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\AnonRateLimit;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\Attribute\UserRateLimit;
use OCP\AppFramework\Http\DataResponse;
use OCP\Common\Exception\NotFoundException;
use OCP\DB\Exception;
use OCP\IL10N;
use OCP\IRequest;
use OCP\PreConditionNotMetException;
use OCP\TextProcessing\Exception\TaskFailureException;
use OCP\TextProcessing\IManager;
use OCP\TextProcessing\ITaskType;
use OCP\TextProcessing\Task;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
/**
* @psalm-import-type CoreTextProcessingTask from ResponseDefinitions
*/
class TextProcessingApiController extends \OCP\AppFramework\OCSController {
public function __construct(
string $appName,
IRequest $request,
private IManager $textProcessingManager,
private IL10N $l,
private ?string $userId,
private ContainerInterface $container,
private LoggerInterface $logger,
) {
parent::__construct($appName, $request);
}
/**
* This endpoint returns all available LanguageModel task types
*
* @return DataResponse<Http::STATUS_OK, array{types: array{id: string, name: string, description: string}[]}, array{}>
*
* 200: Task types returned
*/
#[PublicPage]
public function taskTypes(): DataResponse {
$typeClasses = $this->textProcessingManager->getAvailableTaskTypes();
$types = [];
/** @var string $typeClass */
foreach ($typeClasses as $typeClass) {
try {
/** @var ITaskType $object */
$object = $this->container->get($typeClass);
} catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) {
$this->logger->warning('Could not find ' . $typeClass, ['exception' => $e]);
continue;
}
$types[] = [
'id' => $typeClass,
'name' => $object->getName(),
'description' => $object->getDescription(),
];
}
return new DataResponse([
'types' => $types,
]);
}
/**
* This endpoint allows scheduling a language model task
*
* @param string $input Input text
* @param string $type Type of the task
* @param string $appId ID of the app that will execute the task
* @param string $identifier An arbitrary identifier for the task
*
* @return DataResponse<Http::STATUS_OK, array{task: CoreTextProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_BAD_REQUEST|Http::STATUS_PRECONDITION_FAILED, array{message: string}, array{}>
*
* 200: Task scheduled successfully
* 400: Scheduling task is not possible
* 412: Scheduling task is not possible
*/
#[PublicPage]
#[UserRateLimit(limit: 20, period: 120)]
#[AnonRateLimit(limit: 5, period: 120)]
public function schedule(string $input, string $type, string $appId, string $identifier = ''): DataResponse {
try {
$task = new Task($type, $input, $appId, $this->userId, $identifier);
} catch (InvalidArgumentException) {
return new DataResponse(['message' => $this->l->t('Requested task type does not exist')], Http::STATUS_BAD_REQUEST);
}
try {
try {
$this->textProcessingManager->runOrScheduleTask($task);
} catch(TaskFailureException) {
// noop, because the task object has the failure status set already, we just return the task json
}
$json = $task->jsonSerialize();
return new DataResponse([
'task' => $json,
]);
} catch (PreConditionNotMetException) {
return new DataResponse(['message' => $this->l->t('Necessary language model provider is not available')], Http::STATUS_PRECONDITION_FAILED);
} catch (Exception) {
return new DataResponse(['message' => 'Internal server error'], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
/**
* This endpoint allows checking the status and results of a task.
* Tasks are removed 1 week after receiving their last update.
*
* @param int $id The id of the task
*
* @return DataResponse<Http::STATUS_OK, array{task: CoreTextProcessingTask}, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Task returned
* 404: Task not found
*/
#[PublicPage]
public function getTask(int $id): DataResponse {
try {
$task = $this->textProcessingManager->getUserTask($id, $this->userId);
$json = $task->jsonSerialize();
return new DataResponse([
'task' => $json,
]);
} catch (NotFoundException $e) {
return new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND);
} catch (\RuntimeException $e) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
/**
* This endpoint allows to delete a scheduled task for a user
*
* @param int $id The id of the task
*
* @return DataResponse<Http::STATUS_OK, array{task: CoreTextProcessingTask}, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Task returned
* 404: Task not found
*/
#[NoAdminRequired]
public function deleteTask(int $id): DataResponse {
try {
$task = $this->textProcessingManager->getUserTask($id, $this->userId);
$this->textProcessingManager->deleteTask($task);
$json = $task->jsonSerialize();
return new DataResponse([
'task' => $json,
]);
} catch (NotFoundException $e) {
return new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND);
} catch (\RuntimeException $e) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
/**
* This endpoint returns a list of tasks of a user that are related
* with a specific appId and optionally with an identifier
*
* @param string $appId ID of the app
* @param string|null $identifier An arbitrary identifier for the task
* @return DataResponse<Http::STATUS_OK, array{tasks: CoreTextProcessingTask[]}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Task list returned
*/
#[NoAdminRequired]
public function listTasksByApp(string $appId, ?string $identifier = null): DataResponse {
try {
$tasks = $this->textProcessingManager->getUserTasksByApp($this->userId, $appId, $identifier);
/** @var CoreTextProcessingTask[] $json */
$json = array_map(static function (Task $task) {
return $task->jsonSerialize();
}, $tasks);
return new DataResponse([
'tasks' => $json,
]);
} catch (\RuntimeException $e) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
}
@@ -0,0 +1,246 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net>
*
* @author Marcel Klehr <mklehr@gmx.net>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OC\Files\AppData\AppData;
use OCA\Core\ResponseDefinitions;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\AnonRateLimit;
use OCP\AppFramework\Http\Attribute\BruteForceProtection;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\Attribute\UserRateLimit;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\DB\Exception;
use OCP\Files\NotFoundException;
use OCP\IL10N;
use OCP\IRequest;
use OCP\PreConditionNotMetException;
use OCP\TextToImage\Exception\TaskFailureException;
use OCP\TextToImage\Exception\TaskNotFoundException;
use OCP\TextToImage\IManager;
use OCP\TextToImage\Task;
/**
* @psalm-import-type CoreTextToImageTask from ResponseDefinitions
*/
class TextToImageApiController extends \OCP\AppFramework\OCSController {
public function __construct(
string $appName,
IRequest $request,
private IManager $textToImageManager,
private IL10N $l,
private ?string $userId,
private AppData $appData,
) {
parent::__construct($appName, $request);
}
/**
* Check whether this feature is available
*
* @return DataResponse<Http::STATUS_OK, array{isAvailable: bool}, array{}>
*
* 200: Returns availability status
*/
#[PublicPage]
public function isAvailable(): DataResponse {
return new DataResponse([
'isAvailable' => $this->textToImageManager->hasProviders(),
]);
}
/**
* This endpoint allows scheduling a text to image task
*
* @param string $input Input text
* @param string $appId ID of the app that will execute the task
* @param string $identifier An arbitrary identifier for the task
* @param int $numberOfImages The number of images to generate
*
* @return DataResponse<Http::STATUS_OK, array{task: CoreTextToImageTask}, array{}>|DataResponse<Http::STATUS_PRECONDITION_FAILED|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Task scheduled successfully
* 412: Scheduling task is not possible
*/
#[PublicPage]
#[UserRateLimit(limit: 20, period: 120)]
#[AnonRateLimit(limit: 5, period: 120)]
public function schedule(string $input, string $appId, string $identifier = '', int $numberOfImages = 8): DataResponse {
$task = new Task($input, $appId, $numberOfImages, $this->userId, $identifier);
try {
try {
$this->textToImageManager->runOrScheduleTask($task);
} catch (TaskFailureException) {
// Task status was already updated by the manager, nothing to do here
}
$json = $task->jsonSerialize();
return new DataResponse([
'task' => $json,
]);
} catch (PreConditionNotMetException) {
return new DataResponse(['message' => $this->l->t('No text to image provider is available')], Http::STATUS_PRECONDITION_FAILED);
} catch (Exception) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
/**
* This endpoint allows checking the status and results of a task.
* Tasks are removed 1 week after receiving their last update.
*
* @param int $id The id of the task
*
* @return DataResponse<Http::STATUS_OK, array{task: CoreTextToImageTask}, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Task returned
* 404: Task not found
*/
#[PublicPage]
#[BruteForceProtection(action: 'text2image')]
public function getTask(int $id): DataResponse {
try {
$task = $this->textToImageManager->getUserTask($id, $this->userId);
$json = $task->jsonSerialize();
return new DataResponse([
'task' => $json,
]);
} catch (TaskNotFoundException) {
$res = new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND);
$res->throttle(['action' => 'text2image']);
return $res;
} catch (\RuntimeException) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
/**
* This endpoint allows downloading the resulting image of a task
*
* @param int $id The id of the task
* @param int $index The index of the image to retrieve
*
* @return FileDisplayResponse<Http::STATUS_OK, array{'Content-Type': string}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Image returned
* 404: Task or image not found
*/
#[PublicPage]
#[BruteForceProtection(action: 'text2image')]
public function getImage(int $id, int $index): DataResponse|FileDisplayResponse {
try {
$task = $this->textToImageManager->getUserTask($id, $this->userId);
try {
$folder = $this->appData->getFolder('text2image');
} catch(NotFoundException) {
$res = new DataResponse(['message' => $this->l->t('Image not found')], Http::STATUS_NOT_FOUND);
$res->throttle(['action' => 'text2image']);
return $res;
}
$file = $folder->getFolder((string) $task->getId())->getFile((string) $index);
$info = getimagesizefromstring($file->getContent());
return new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => image_type_to_mime_type($info[2])]);
} catch (TaskNotFoundException) {
$res = new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND);
$res->throttle(['action' => 'text2image']);
return $res;
} catch (\RuntimeException) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
} catch (NotFoundException) {
$res = new DataResponse(['message' => $this->l->t('Image not found')], Http::STATUS_NOT_FOUND);
$res->throttle(['action' => 'text2image']);
return $res;
}
}
/**
* This endpoint allows to delete a scheduled task for a user
*
* @param int $id The id of the task
*
* @return DataResponse<Http::STATUS_OK, array{task: CoreTextToImageTask}, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Task returned
* 404: Task not found
*/
#[NoAdminRequired]
#[BruteForceProtection(action: 'text2image')]
public function deleteTask(int $id): DataResponse {
try {
$task = $this->textToImageManager->getUserTask($id, $this->userId);
$this->textToImageManager->deleteTask($task);
$json = $task->jsonSerialize();
return new DataResponse([
'task' => $json,
]);
} catch (TaskNotFoundException) {
$res = new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND);
$res->throttle(['action' => 'text2image']);
return $res;
} catch (\RuntimeException) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
/**
* This endpoint returns a list of tasks of a user that are related
* with a specific appId and optionally with an identifier
*
* @param string $appId ID of the app
* @param string|null $identifier An arbitrary identifier for the task
* @return DataResponse<Http::STATUS_OK, array{tasks: CoreTextToImageTask[]}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
*
* 200: Task list returned
*/
#[NoAdminRequired]
#[AnonRateLimit(limit: 5, period: 120)]
public function listTasksByApp(string $appId, ?string $identifier = null): DataResponse {
try {
$tasks = $this->textToImageManager->getUserTasksByApp($this->userId, $appId, $identifier);
/** @var CoreTextToImageTask[] $json */
$json = array_map(static function (Task $task) {
return $task->jsonSerialize();
}, $tasks);
return new DataResponse([
'tasks' => $json,
]);
} catch (\RuntimeException) {
return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
}
@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022 Julius Härtl <jus@bitgrid.net>
*
* @author Julius Härtl <jus@bitgrid.net>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use InvalidArgumentException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\IL10N;
use OCP\IRequest;
use OCP\PreConditionNotMetException;
use OCP\Translation\CouldNotTranslateException;
use OCP\Translation\ITranslationManager;
class TranslationApiController extends \OCP\AppFramework\OCSController {
public function __construct(
string $appName,
IRequest $request,
private ITranslationManager $translationManager,
private IL10N $l10n,
) {
parent::__construct($appName, $request);
}
/**
* @PublicPage
*
* Get the list of supported languages
*
* @return DataResponse<Http::STATUS_OK, array{languages: array{from: string, fromLabel: string, to: string, toLabel: string}[], languageDetection: bool}, array{}>
*
* 200: Supported languages returned
*/
public function languages(): DataResponse {
return new DataResponse([
'languages' => array_map(fn ($lang) => $lang->jsonSerialize(), $this->translationManager->getLanguages()),
'languageDetection' => $this->translationManager->canDetectLanguage(),
]);
}
/**
* @PublicPage
* @UserRateThrottle(limit=25, period=120)
* @AnonRateThrottle(limit=10, period=120)
*
* Translate a text
*
* @param string $text Text to be translated
* @param string|null $fromLanguage Language to translate from
* @param string $toLanguage Language to translate to
* @return DataResponse<Http::STATUS_OK, array{text: string, from: ?string}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_PRECONDITION_FAILED|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string, from?: ?string}, array{}>
*
* 200: Translated text returned
* 400: Language not detected or unable to translate
* 412: Translating is not possible
*/
public function translate(string $text, ?string $fromLanguage, string $toLanguage): DataResponse {
try {
$translation = $this->translationManager->translate($text, $fromLanguage, $toLanguage);
return new DataResponse([
'text' => $translation,
'from' => $fromLanguage,
]);
} catch (PreConditionNotMetException) {
return new DataResponse(['message' => $this->l10n->t('No translation provider available')], Http::STATUS_PRECONDITION_FAILED);
} catch (InvalidArgumentException) {
return new DataResponse(['message' => $this->l10n->t('Could not detect language')], Http::STATUS_BAD_REQUEST);
} catch (CouldNotTranslateException $e) {
return new DataResponse(['message' => $this->l10n->t('Unable to translate'), 'from' => $e->getFrom()], Http::STATUS_BAD_REQUEST);
}
}
}
@@ -0,0 +1,268 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Cornelius Kölbel <cornelius.koelbel@netknights.it>
* @author Joas Schilling <coding@schilljs.com>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OC\Core\Controller;
use OC\Authentication\TwoFactorAuth\Manager;
use OC_User;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\Attribute\UseSession;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\StandaloneTemplateResponse;
use OCP\Authentication\TwoFactorAuth\IActivatableAtLogin;
use OCP\Authentication\TwoFactorAuth\IProvider;
use OCP\Authentication\TwoFactorAuth\IProvidesCustomCSP;
use OCP\Authentication\TwoFactorAuth\TwoFactorException;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class TwoFactorChallengeController extends Controller {
public function __construct(
string $appName,
IRequest $request,
private Manager $twoFactorManager,
private IUserSession $userSession,
private ISession $session,
private IURLGenerator $urlGenerator,
private LoggerInterface $logger,
) {
parent::__construct($appName, $request);
}
/**
* @return string
*/
protected function getLogoutUrl() {
return OC_User::getLogoutUrl($this->urlGenerator);
}
/**
* @param IProvider[] $providers
*/
private function splitProvidersAndBackupCodes(array $providers): array {
$regular = [];
$backup = null;
foreach ($providers as $provider) {
if ($provider->getId() === 'backup_codes') {
$backup = $provider;
} else {
$regular[] = $provider;
}
}
return [$regular, $backup];
}
/**
* @NoAdminRequired
* @NoCSRFRequired
* @TwoFactorSetUpDoneRequired
*
* @param string $redirect_url
* @return StandaloneTemplateResponse
*/
public function selectChallenge($redirect_url) {
$user = $this->userSession->getUser();
$providerSet = $this->twoFactorManager->getProviderSet($user);
$allProviders = $providerSet->getProviders();
[$providers, $backupProvider] = $this->splitProvidersAndBackupCodes($allProviders);
$setupProviders = $this->twoFactorManager->getLoginSetupProviders($user);
$data = [
'providers' => $providers,
'backupProvider' => $backupProvider,
'providerMissing' => $providerSet->isProviderMissing(),
'redirect_url' => $redirect_url,
'logout_url' => $this->getLogoutUrl(),
'hasSetupProviders' => !empty($setupProviders),
];
return new StandaloneTemplateResponse($this->appName, 'twofactorselectchallenge', $data, 'guest');
}
/**
* @NoAdminRequired
* @NoCSRFRequired
* @TwoFactorSetUpDoneRequired
*
* @param string $challengeProviderId
* @param string $redirect_url
* @return StandaloneTemplateResponse|RedirectResponse
*/
#[UseSession]
public function showChallenge($challengeProviderId, $redirect_url) {
$user = $this->userSession->getUser();
$providerSet = $this->twoFactorManager->getProviderSet($user);
$provider = $providerSet->getProvider($challengeProviderId);
if (is_null($provider)) {
return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge'));
}
$backupProvider = $providerSet->getProvider('backup_codes');
if (!is_null($backupProvider) && $backupProvider->getId() === $provider->getId()) {
// Don't show the backup provider link if we're already showing that provider's challenge
$backupProvider = null;
}
$errorMessage = '';
$error = false;
if ($this->session->exists('two_factor_auth_error')) {
$this->session->remove('two_factor_auth_error');
$error = true;
$errorMessage = $this->session->get("two_factor_auth_error_message");
$this->session->remove('two_factor_auth_error_message');
}
$tmpl = $provider->getTemplate($user);
$tmpl->assign('redirect_url', $redirect_url);
$data = [
'error' => $error,
'error_message' => $errorMessage,
'provider' => $provider,
'backupProvider' => $backupProvider,
'logout_url' => $this->getLogoutUrl(),
'redirect_url' => $redirect_url,
'template' => $tmpl->fetchPage(),
];
$response = new StandaloneTemplateResponse($this->appName, 'twofactorshowchallenge', $data, 'guest');
if ($provider instanceof IProvidesCustomCSP) {
$response->setContentSecurityPolicy($provider->getCSP());
}
return $response;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
* @TwoFactorSetUpDoneRequired
*
* @UserRateThrottle(limit=5, period=100)
*
* @param string $challengeProviderId
* @param string $challenge
* @param string $redirect_url
* @return RedirectResponse
*/
#[UseSession]
public function solveChallenge($challengeProviderId, $challenge, $redirect_url = null) {
$user = $this->userSession->getUser();
$provider = $this->twoFactorManager->getProvider($user, $challengeProviderId);
if (is_null($provider)) {
return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge'));
}
try {
if ($this->twoFactorManager->verifyChallenge($challengeProviderId, $user, $challenge)) {
if (!is_null($redirect_url)) {
return new RedirectResponse($this->urlGenerator->getAbsoluteURL(urldecode($redirect_url)));
}
return new RedirectResponse($this->urlGenerator->linkToDefaultPageUrl());
}
} catch (TwoFactorException $e) {
/*
* The 2FA App threw an TwoFactorException. Now we display more
* information to the user. The exception text is stored in the
* session to be used in showChallenge()
*/
$this->session->set('two_factor_auth_error_message', $e->getMessage());
}
$ip = $this->request->getRemoteAddress();
$uid = $user->getUID();
$this->logger->warning("Two-factor challenge failed: $uid (Remote IP: $ip)");
$this->session->set('two_factor_auth_error', true);
return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.showChallenge', [
'challengeProviderId' => $provider->getId(),
'redirect_url' => $redirect_url,
]));
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function setupProviders(): StandaloneTemplateResponse {
$user = $this->userSession->getUser();
$setupProviders = $this->twoFactorManager->getLoginSetupProviders($user);
$data = [
'providers' => $setupProviders,
'logout_url' => $this->getLogoutUrl(),
];
return new StandaloneTemplateResponse($this->appName, 'twofactorsetupselection', $data, 'guest');
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function setupProvider(string $providerId) {
$user = $this->userSession->getUser();
$providers = $this->twoFactorManager->getLoginSetupProviders($user);
$provider = null;
foreach ($providers as $p) {
if ($p->getId() === $providerId) {
$provider = $p;
break;
}
}
if ($provider === null) {
return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge'));
}
/** @var IActivatableAtLogin $provider */
$tmpl = $provider->getLoginSetup($user)->getBody();
$data = [
'provider' => $provider,
'logout_url' => $this->getLogoutUrl(),
'template' => $tmpl->fetchPage(),
];
$response = new StandaloneTemplateResponse($this->appName, 'twofactorsetupchallenge', $data, 'guest');
return $response;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @todo handle the extreme edge case of an invalid provider ID and redirect to the provider selection page
*/
public function confirmProviderSetup(string $providerId) {
return new RedirectResponse($this->urlGenerator->linkToRoute(
'core.TwoFactorChallenge.showChallenge',
[
'challengeProviderId' => $providerId,
]
));
}
}
@@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Joas Schilling <coding@schilljs.com>
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use InvalidArgumentException;
use OC\Search\SearchComposer;
use OC\Search\SearchQuery;
use OC\Search\UnsupportedFilter;
use OCA\Core\ResponseDefinitions;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserSession;
use OCP\Route\IRouter;
use OCP\Search\ISearchQuery;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
/**
* @psalm-import-type CoreUnifiedSearchProvider from ResponseDefinitions
* @psalm-import-type CoreUnifiedSearchResult from ResponseDefinitions
*/
class UnifiedSearchController extends OCSController {
public function __construct(
IRequest $request,
private IUserSession $userSession,
private SearchComposer $composer,
private IRouter $router,
private IURLGenerator $urlGenerator,
) {
parent::__construct('core', $request);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* Get the providers for unified search
*
* @param string $from the url the user is currently at
* @return DataResponse<Http::STATUS_OK, CoreUnifiedSearchProvider[], array{}>
*
* 200: Providers returned
*/
public function getProviders(string $from = ''): DataResponse {
[$route, $parameters] = $this->getRouteInformation($from);
$result = $this->composer->getProviders($route, $parameters);
$response = new DataResponse($result);
$response->setETag(md5(json_encode($result)));
return $response;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* Launch a search for a specific search provider.
*
* Additional filters are available for each provider.
* Send a request to /providers endpoint to list providers with their available filters.
*
* @param string $providerId ID of the provider
* @param string $term Term to search
* @param int|null $sortOrder Order of entries
* @param int|null $limit Maximum amount of entries
* @param int|string|null $cursor Offset for searching
* @param string $from The current user URL
*
* @return DataResponse<Http::STATUS_OK, CoreUnifiedSearchResult, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, string, array{}>
*
* 200: Search entries returned
* 400: Searching is not possible
*/
public function search(
string $providerId,
// Unused parameter for OpenAPI spec generator
string $term = '',
?int $sortOrder = null,
?int $limit = null,
$cursor = null,
string $from = '',
): DataResponse {
[$route, $routeParameters] = $this->getRouteInformation($from);
try {
$filters = $this->composer->buildFilterList($providerId, $this->request->getParams());
} catch (UnsupportedFilter|InvalidArgumentException $e) {
return new DataResponse($e->getMessage(), Http::STATUS_BAD_REQUEST);
}
return new DataResponse(
$this->composer->search(
$this->userSession->getUser(),
$providerId,
new SearchQuery(
$filters,
$sortOrder ?? ISearchQuery::SORT_DATE_DESC,
$limit ?? SearchQuery::LIMIT_DEFAULT,
$cursor,
$route,
$routeParameters
)
)->jsonSerialize()
);
}
protected function getRouteInformation(string $url): array {
$routeStr = '';
$parameters = [];
if ($url !== '') {
$urlParts = parse_url($url);
$urlPath = $urlParts['path'];
// Optionally strip webroot from URL. Required for route matching on setups
// with Nextcloud in a webserver subfolder (webroot).
$webroot = $this->urlGenerator->getWebroot();
if ($webroot !== '' && substr($urlPath, 0, strlen($webroot)) === $webroot) {
$urlPath = substr($urlPath, strlen($webroot));
}
try {
$parameters = $this->router->findMatchingRoute($urlPath);
// contacts.PageController.index => contacts.Page.index
$route = $parameters['caller'];
if (substr($route[1], -10) === 'Controller') {
$route[1] = substr($route[1], 0, -10);
}
$routeStr = implode('.', $route);
// cleanup
unset($parameters['_route'], $parameters['action'], $parameters['caller']);
} catch (ResourceNotFoundException $exception) {
}
if (isset($urlParts['query'])) {
parse_str($urlParts['query'], $queryParameters);
$parameters = array_merge($parameters, $queryParameters);
}
}
return [
$routeStr,
$parameters,
];
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IRequest;
use OCP\Util;
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class UnsupportedBrowserController extends Controller {
public function __construct(IRequest $request) {
parent::__construct('core', $request);
}
/**
* @PublicPage
* @NoCSRFRequired
*
* @return Response
*/
public function index(): Response {
Util::addScript('core', 'unsupported-browser');
Util::addStyle('core', 'icons');
return new TemplateResponse('core', 'unsupportedbrowser', [], TemplateResponse::RENDER_AS_ERROR);
}
}
@@ -0,0 +1,69 @@
<?php
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Lukas Reschke <lukas@statuscode.ch>
* @author Morris Jobke <hey@morrisjobke.de>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OC\Core\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
use OCP\IUserManager;
class UserController extends Controller {
public function __construct(
string $appName,
IRequest $request,
protected IUserManager $userManager,
) {
parent::__construct($appName, $request);
}
/**
* Lookup user display names
*
* @NoAdminRequired
*
* @param array $users
*
* @return JSONResponse
*/
public function getDisplayNames($users) {
$result = [];
foreach ($users as $user) {
$userObject = $this->userManager->get($user);
if (is_object($userObject)) {
$result[$user] = $userObject->getDisplayName();
} else {
$result[$user] = $user;
}
}
$json = [
'users' => $result,
'status' => 'success'
];
return new JSONResponse($json);
}
}
@@ -0,0 +1,43 @@
<?php
/**
* @copyright 2017, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\Response;
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class WalledGardenController extends Controller {
/**
* @PublicPage
* @NoCSRFRequired
*/
public function get(): Response {
$resp = new Response();
$resp->setStatus(Http::STATUS_NO_CONTENT);
return $resp;
}
}
@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Richard Steinmetz <richard@steinmetz.cloud>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OC\Authentication\Login\LoginData;
use OC\Authentication\Login\WebAuthnChain;
use OC\Authentication\WebAuthn\Manager;
use OC\URLGenerator;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\UseSession;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
use OCP\ISession;
use OCP\Util;
use Psr\Log\LoggerInterface;
use Webauthn\PublicKeyCredentialRequestOptions;
class WebAuthnController extends Controller {
private const WEBAUTHN_LOGIN = 'webauthn_login';
private const WEBAUTHN_LOGIN_UID = 'webauthn_login_uid';
public function __construct(
string $appName,
IRequest $request,
private Manager $webAuthnManger,
private ISession $session,
private LoggerInterface $logger,
private WebAuthnChain $webAuthnChain,
private URLGenerator $urlGenerator,
) {
parent::__construct($appName, $request);
}
/**
* @NoAdminRequired
* @PublicPage
*/
#[UseSession]
public function startAuthentication(string $loginName): JSONResponse {
$this->logger->debug('Starting WebAuthn login');
$this->logger->debug('Converting login name to UID');
$uid = $loginName;
Util::emitHook(
'\OCA\Files_Sharing\API\Server2Server',
'preLoginNameUsedAsUserName',
['uid' => &$uid]
);
$this->logger->debug('Got UID: ' . $uid);
$publicKeyCredentialRequestOptions = $this->webAuthnManger->startAuthentication($uid, $this->request->getServerHost());
$this->session->set(self::WEBAUTHN_LOGIN, json_encode($publicKeyCredentialRequestOptions));
$this->session->set(self::WEBAUTHN_LOGIN_UID, $uid);
return new JSONResponse($publicKeyCredentialRequestOptions);
}
/**
* @NoAdminRequired
* @PublicPage
*/
#[UseSession]
public function finishAuthentication(string $data): JSONResponse {
$this->logger->debug('Validating WebAuthn login');
if (!$this->session->exists(self::WEBAUTHN_LOGIN) || !$this->session->exists(self::WEBAUTHN_LOGIN_UID)) {
$this->logger->debug('Trying to finish WebAuthn login without session data');
return new JSONResponse([], Http::STATUS_BAD_REQUEST);
}
// Obtain the publicKeyCredentialOptions from when we started the registration
$publicKeyCredentialRequestOptions = PublicKeyCredentialRequestOptions::createFromString($this->session->get(self::WEBAUTHN_LOGIN));
$uid = $this->session->get(self::WEBAUTHN_LOGIN_UID);
$this->webAuthnManger->finishAuthentication($publicKeyCredentialRequestOptions, $data, $uid);
//TODO: add other parameters
$loginData = new LoginData(
$this->request,
$uid,
''
);
$this->webAuthnChain->process($loginData);
return new JSONResponse([
'defaultRedirectUrl' => $this->urlGenerator->linkToDefaultPageUrl(),
]);
}
}
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OC\Http\WellKnown\RequestManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\Response;
use OCP\IRequest;
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class WellKnownController extends Controller {
public function __construct(
IRequest $request,
private RequestManager $requestManager,
) {
parent::__construct('core', $request);
}
/**
* @PublicPage
* @NoCSRFRequired
*
* @return Response
*/
public function handle(string $service): Response {
$response = $this->requestManager->process(
$service,
$this->request
);
if ($response === null) {
$httpResponse = new JSONResponse(["message" => "$service not supported"], Http::STATUS_NOT_FOUND);
} else {
$httpResponse = $response->toHttpResponse();
}
// We add a custom header so that setup checks can detect if their requests are answered by this controller
return $httpResponse->addHeader('X-NEXTCLOUD-WELL-KNOWN', '1');
}
}
@@ -0,0 +1,124 @@
<?php
/**
* @copyright Copyright (c) 2018 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OC\CapabilitiesManager;
use OC\Security\IdentityProof\Manager;
use OC\Updater\ChangesCheck;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\Defaults;
use OCP\IConfig;
use OCP\IRequest;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\L10N\IFactory;
class WhatsNewController extends OCSController {
public function __construct(
string $appName,
IRequest $request,
CapabilitiesManager $capabilitiesManager,
private IUserSession $userSession,
IUserManager $userManager,
Manager $keyManager,
private IConfig $config,
private ChangesCheck $whatsNewService,
private IFactory $langFactory,
private Defaults $defaults,
) {
parent::__construct($appName, $request, $capabilitiesManager, $userSession, $userManager, $keyManager);
}
/**
* @NoAdminRequired
*
* Get the changes
*
* @return DataResponse<Http::STATUS_OK, array{changelogURL: string, product: string, version: string, whatsNew?: array{regular: string[], admin: string[]}}, array{}>|DataResponse<Http::STATUS_NO_CONTENT, array<empty>, array{}>
*
* 200: Changes returned
* 204: No changes
*/
public function get():DataResponse {
$user = $this->userSession->getUser();
if ($user === null) {
throw new \RuntimeException("Acting user cannot be resolved");
}
$lastRead = $this->config->getUserValue($user->getUID(), 'core', 'whatsNewLastRead', 0);
$currentVersion = $this->whatsNewService->normalizeVersion($this->config->getSystemValue('version'));
if (version_compare($lastRead, $currentVersion, '>=')) {
return new DataResponse([], Http::STATUS_NO_CONTENT);
}
try {
$iterator = $this->langFactory->getLanguageIterator();
$whatsNew = $this->whatsNewService->getChangesForVersion($currentVersion);
$resultData = [
'changelogURL' => $whatsNew['changelogURL'],
'product' => $this->defaults->getProductName(),
'version' => $currentVersion,
];
do {
$lang = $iterator->current();
if (isset($whatsNew['whatsNew'][$lang])) {
$resultData['whatsNew'] = $whatsNew['whatsNew'][$lang];
break;
}
$iterator->next();
} while ($lang !== 'en' && $iterator->valid());
return new DataResponse($resultData);
} catch (DoesNotExistException $e) {
return new DataResponse([], Http::STATUS_NO_CONTENT);
}
}
/**
* @NoAdminRequired
*
* Dismiss the changes
*
* @param string $version Version to dismiss the changes for
*
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>
* @throws \OCP\PreConditionNotMetException
* @throws DoesNotExistException
*
* 200: Changes dismissed
*/
public function dismiss(string $version):DataResponse {
$user = $this->userSession->getUser();
if ($user === null) {
throw new \RuntimeException("Acting user cannot be resolved");
}
$version = $this->whatsNewService->normalizeVersion($version);
// checks whether it's a valid version, throws an Exception otherwise
$this->whatsNewService->getChangesForVersion($version);
$this->config->setUserValue($user->getUID(), 'core', 'whatsNewLastRead', $version);
return new DataResponse();
}
}
@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program 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 (at your option) 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 OC\Core\Controller;
use OC\Authentication\Token\RemoteWipe;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\Authentication\Exceptions\InvalidTokenException;
use OCP\IRequest;
class WipeController extends Controller {
public function __construct(
string $appName,
IRequest $request,
private RemoteWipe $remoteWipe,
) {
parent::__construct($appName, $request);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
*
* @AnonRateThrottle(limit=10, period=300)
*
* Check if the device should be wiped
*
* @param string $token App password
*
* @return JSONResponse<Http::STATUS_OK, array{wipe: bool}, array{}>|JSONResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>
*
* 200: Device should be wiped
* 404: Device should not be wiped
*/
public function checkWipe(string $token): JSONResponse {
try {
if ($this->remoteWipe->start($token)) {
return new JSONResponse([
'wipe' => true
]);
}
return new JSONResponse([], Http::STATUS_NOT_FOUND);
} catch (InvalidTokenException $e) {
return new JSONResponse([], Http::STATUS_NOT_FOUND);
}
}
/**
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
*
* @AnonRateThrottle(limit=10, period=300)
*
* Finish the wipe
*
* @param string $token App password
*
* @return JSONResponse<Http::STATUS_OK|Http::STATUS_NOT_FOUND, array<empty>, array{}>
*
* 200: Wipe finished successfully
* 404: Device should not be wiped
*/
public function wipeDone(string $token): JSONResponse {
try {
if ($this->remoteWipe->finish($token)) {
return new JSONResponse([]);
}
return new JSONResponse([], Http::STATUS_NOT_FOUND);
} catch (InvalidTokenException $e) {
return new JSONResponse([], Http::STATUS_NOT_FOUND);
}
}
}