Files
nextcloud-app-api/lib/Service/AppAPIService.php
Alexander Piskun 83333b5e0a cleanup: Qualifier -> Imports (#125)
Fixes IDE warnings:

* Qualifier can be replaced with an import
* Argument matches the parameter's default value

Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
2023-11-25 10:59:07 +03:00

950 lines
29 KiB
PHP

<?php
declare(strict_types=1);
namespace OCA\AppAPI\Service;
use OCA\AppAPI\AppInfo\Application;
use OCA\AppAPI\Db\ExApp;
use OCA\AppAPI\Db\ExAppMapper;
use OCA\AppAPI\Fetcher\ExAppArchiveFetcher;
use OCA\AppAPI\Fetcher\ExAppFetcher;
use OCA\AppAPI\Notifications\ExNotificationsManager;
use OCP\App\IAppManager;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\DB\Exception;
use OCP\Http\Client\IClient;
use OCP\Http\Client\IClientService;
use OCP\Http\Client\IResponse;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Log\ILogFactory;
use OCP\Security\Bruteforce\IThrottler;
use OCP\Security\ISecureRandom;
use Psr\Log\LoggerInterface;
use SimpleXMLElement;
class AppAPIService {
public const BASIC_API_SCOPE = 1;
public const CACHE_TTL = 60 * 60; // 1 hour
private ICache $cache;
private IClient $client;
public function __construct(
private LoggerInterface $logger,
private ILogFactory $logFactory,
ICacheFactory $cacheFactory,
private IThrottler $throttler,
private IConfig $config,
IClientService $clientService,
private ExAppMapper $exAppMapper,
private IAppManager $appManager,
private ExAppUsersService $exAppUsersService,
private ExAppApiScopeService $exAppApiScopeService,
private ExAppScopesService $exAppScopesService,
private ISecureRandom $random,
private IUserSession $userSession,
private ISession $session,
private IUserManager $userManager,
private ExAppConfigService $exAppConfigService,
private ExNotificationsManager $exNotificationsManager,
private TalkBotsService $talkBotsService,
private ExAppFetcher $exAppFetcher,
private ExAppArchiveFetcher $exAppArchiveFetcher,
) {
$this->cache = $cacheFactory->createDistributed(Application::APP_ID . '/service');
$this->client = $clientService->newClient();
}
public function getExApp(string $appId): ?ExApp {
try {
$cacheKey = '/exApp_' . $appId;
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
return $cached instanceof ExApp ? $cached : new ExApp($cached);
}
$exApp = $this->exAppMapper->findByAppId($appId);
$this->cache->set($cacheKey, $exApp, self::CACHE_TTL);
return $exApp;
} catch (DoesNotExistException) {
} catch (MultipleObjectsReturnedException|Exception $e) {
$this->logger->debug(sprintf('Failed to get ExApp %s. Error: %s', $appId, $e->getMessage()), ['exception' => $e]);
}
return null;
}
/**
* Register ExApp or update if already exists
*
* @param string $appId
* @param array $appData [version, name, daemon_config_id, protocol, host, port, secret]
*
* @return ExApp|null
*/
public function registerExApp(string $appId, array $appData): ?ExApp {
$exApp = new ExApp([
'appid' => $appId,
'version' => $appData['version'],
'name' => $appData['name'],
'daemon_config_name' => $appData['daemon_config_name'],
'protocol' => $appData['protocol'],
'host' => $appData['host'],
'port' => $appData['port'],
'secret' => $appData['secret'] !== '' ? $appData['secret'] : $this->random->generate(128),
'status' => json_encode(['active' => false, 'progress' => 0]),
'created_time' => time(),
'last_check_time' => time(),
]);
try {
$cacheKey = '/exApp_' . $appId;
$exApp = $this->exAppMapper->insert($exApp);
$this->cache->set($cacheKey, $exApp, self::CACHE_TTL);
return $exApp;
} catch (Exception $e) {
$this->logger->error(sprintf('Error while registering ExApp %s: %s', $appId, $e->getMessage()));
return null;
}
}
/**
* Unregister ExApp.
* Removes ExApp from database and cache.
*
* @param string $appId
*
* @return ExApp|null
*/
public function unregisterExApp(string $appId): ?ExApp {
$exApp = $this->getExApp($appId);
if ($exApp === null) {
return null;
}
try {
if ($this->exAppMapper->deleteExApp($exApp) !== 1) {
$this->logger->error(sprintf('Error while unregistering ExApp: %s', $appId));
return null;
}
// TODO: Do we need to remove app_config_ex, app_preferences_ex too
$this->exAppScopesService->removeExAppScopes($exApp);
$this->exAppUsersService->removeExAppUsers($exApp);
$this->talkBotsService->unregisterExAppTalkBots($exApp); // TODO: Think about internal Events for clean and flexible unregister ExApp callbacks
$this->cache->remove('/exApp_' . $appId);
return $exApp;
} catch (Exception $e) {
$this->logger->error(sprintf('Error while unregistering ExApp: %s', $e->getMessage()), ['exception' => $e]);
return null;
}
}
public function getExAppsByPort(int $port): array {
try {
return $this->exAppMapper->findByPort($port);
} catch (Exception) {
return [];
}
}
public function getExAppRandomPort(): int {
$port = 10000 + (int) $this->random->generate(4, ISecureRandom::CHAR_DIGITS);
while ($this->getExAppsByPort($port) !== []) {
$port = 10000 + (int) $this->random->generate(4, ISecureRandom::CHAR_DIGITS);
}
return $port;
}
/**
* Enable ExApp. Sends request to ExApp to update enabled state.
* If request fails, ExApp will be disabled.
* Removes ExApp from cache.
*
* @param ExApp $exApp
*
* @return bool
*/
public function enableExApp(ExApp $exApp): bool {
try {
if ($this->exAppMapper->updateExAppEnabled($exApp->getAppid(), true) === 1) {
$cacheKey = '/exApp_' . $exApp->getAppid();
$exApp->setEnabled(1);
$this->cache->set($cacheKey, $exApp, self::CACHE_TTL);
$exAppEnabled = $this->requestToExApp($exApp, '/enabled?enabled=1', null, 'PUT');
if ($exAppEnabled instanceof IResponse) {
$response = json_decode($exAppEnabled->getBody(), true);
if (isset($response['error']) && strlen($response['error']) === 0) {
$this->updateExAppLastCheckTime($exApp);
} else {
$this->logger->error(sprintf('Failed to enable ExApp %s. Error: %s', $exApp->getAppid(), $response['error']));
$this->disableExApp($exApp);
return false;
}
} elseif (isset($exAppEnabled['error'])) {
$this->logger->error(sprintf('Failed to enable ExApp %s. Error: %s', $exApp->getAppid(), $exAppEnabled['error']));
$this->disableExApp($exApp);
return false;
}
return true;
}
} catch (Exception $e) {
$this->logger->error(sprintf('Error while enabling ExApp: %s', $e->getMessage()));
return false;
}
return false;
}
/**
* Disable ExApp. Sends request to ExApp to update enabled state.
* If request fails, ExApp keep disabled in database.
* Removes ExApp from cache.
*
* @param ExApp $exApp
*
* @return bool
*/
public function disableExApp(ExApp $exApp): bool {
try {
$exAppDisabled = $this->requestToExApp($exApp, '/enabled?enabled=0', null, 'PUT');
if ($exAppDisabled instanceof IResponse) {
$response = json_decode($exAppDisabled->getBody(), true);
if (isset($response['error']) && strlen($response['error']) !== 0) {
$this->logger->error(sprintf('Failed to disable ExApp %s. Error: %s', $exApp->getAppid(), $response['error']));
}
} elseif (isset($exAppDisabled['error'])) {
$this->logger->error(sprintf('Failed to enable ExApp %s. Error: %s', $exApp->getAppid(), $exAppDisabled['error']));
}
if ($this->exAppMapper->updateExAppEnabled($exApp->getAppid(), false) !== 1) {
return false;
}
$this->updateExAppLastCheckTime($exApp);
$cacheKey = '/exApp_' . $exApp->getAppid();
$exApp->setEnabled(0);
$this->cache->set($cacheKey, $exApp, self::CACHE_TTL);
return true;
} catch (Exception $e) {
$this->logger->error(sprintf('Error while disabling ExApp: %s', $e->getMessage()));
return false;
}
}
/**
* Update ExApp info (version, name, system app flag changes after update)
*
* @param ExApp $exApp
* @param array $exAppInfo
*
* @return bool
*/
public function updateExAppInfo(ExApp $exApp, array $exAppInfo): bool {
$cacheKey = '/exApp_' . $exApp->getAppid();
$exApp->setVersion($exAppInfo['version']);
if (!$this->updateExAppVersion($exApp)) {
return false;
}
$exApp->setName($exAppInfo['name']);
if (!$this->updateExAppName($exApp)) {
return false;
}
// Update system app flag
$isSystemApp = $this->exAppUsersService->exAppUserExists($exApp->getAppid(), '');
if (filter_var($exAppInfo['system_app'], FILTER_VALIDATE_BOOLEAN) && !$isSystemApp) {
$this->exAppUsersService->setupSystemAppFlag($exApp);
} else {
$this->exAppUsersService->removeExAppUser($exApp, '');
}
$this->cache->set($cacheKey, $exApp, self::CACHE_TTL);
return true;
}
/**
* Update ExApp status during initialization step.
* Active status is set when progress reached 100%.
*
* @param string $appId
* @param int $progress
*
* @return void
*/
public function setAppInitProgress(string $appId, int $progress, string $error = '', bool $update = false): void {
$exApp = $this->getExApp($appId);
$cacheKey = '/exApp_' . $exApp->getAppid();
$status = json_decode($exApp->getStatus(), true);
if ($update) {
// Set active=false during update action, for register it already false
$status['active'] = false;
}
if ($status['active']) {
return;
}
if ($error !== '') {
$this->logger->error(sprintf('ExApp %s initialization failed. Error: %s', $appId, $error));
$status['error'] = $error;
unset($status['progress']);
} else {
if ($progress >= 0 && $progress < 100) {
$status['progress'] = $progress;
} elseif ($progress === 100) {
unset($status['progress']);
} else {
throw new \InvalidArgumentException('Invalid ExApp status progress value');
}
$status['active'] = $progress === 100;
}
$exApp->setStatus(json_encode($status));
try {
$exApp = $this->exAppMapper->update($exApp);
$this->updateExAppLastCheckTime($exApp);
$this->cache->set($cacheKey, $exApp, self::CACHE_TTL);
if ($progress === 100) {
$this->enableExApp($exApp);
}
} catch (Exception) {
}
}
/**
* Regular ExApp heartbeat to verify connection
*
* @param array $params ExApp url params (protocol, host, port)
*
* @return bool
*/
public function heartbeatExApp(array $params): bool {
$heartbeatAttempts = 0;
$delay = 1;
$maxHeartbeatAttempts = 60 * 10 * $delay; // minutes for container initialization
$heartbeatUrl = self::getExAppUrl(
$params['protocol'],
$params['host'],
(int) $params['port'],
) . '/heartbeat';
$options = [
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
],
'nextcloud' => [
'allow_local_address' => true,
],
];
while ($heartbeatAttempts < $maxHeartbeatAttempts) {
$heartbeatAttempts++;
try {
$heartbeatResult = $this->client->get($heartbeatUrl, $options);
} catch (\Exception) {
sleep($delay);
continue;
}
$statusCode = $heartbeatResult->getStatusCode();
if ($statusCode === 200) {
$result = json_decode($heartbeatResult->getBody(), true);
if (isset($result['status']) && $result['status'] === 'ok') {
return true;
}
}
sleep($delay);
}
return false;
}
/**
* Dispatch ExApp initialization step, that may take a long time to display the progress of initialization.
*
* @return void
*/
public function dispatchExAppInit(ExApp $exApp): void {
$initUrl = self::getExAppUrl(
$exApp->getProtocol(),
$exApp->getHost(),
$exApp->getPort(),
) . '/init';
$options = [
'headers' => $this->buildAppAPIAuthHeaders(null, null, $exApp),
'nextcloud' => [
'allow_local_address' => true,
],
];
try {
$this->client->postAsync($initUrl, $options);
} catch (\Exception) {
}
}
public function getExAppRequestedScopes(ExApp $exApp, ?SimpleXMLElement $infoXml = null, array $jsonInfo = []): ?array {
if (isset($jsonInfo['scopes'])) {
return $jsonInfo['scopes'];
}
if ($infoXml === null) {
$exAppInfo = $this->getExAppInfoFromAppstore($exApp);
if (isset($exAppInfo)) {
$infoXml = $exAppInfo;
}
}
if (isset($infoXml)) {
$scopes = $infoXml->xpath('external-app/scopes');
if ($scopes !== false) {
$scopes = (array) $scopes[0];
$required = array_map(function (string $scopeGroup) {
return $scopeGroup;
}, (array) $scopes['required']->value);
$optional = array_map(function (string $scopeGroup) {
return $scopeGroup;
}, (array) $scopes['optional']->value);
return [
'required' => array_values($required),
'optional' => array_values($optional),
];
}
}
return ['error' => 'Failed to get ExApp requested scopes.'];
}
/**
* Get info from App Store releases for specific ExApp and its current version
*
* @param ExApp $exApp
*
* @return SimpleXMLElement|null
*/
public function getExAppInfoFromAppstore(ExApp $exApp): ?SimpleXMLElement {
$exApps = $this->exAppFetcher->get();
$exAppAppstoreData = array_filter($exApps, function (array $exAppItem) use ($exApp) {
return $exAppItem['id'] === $exApp->getAppid() && count(array_filter($exAppItem['releases'], function (array $release) use ($exApp) {
return $release['version'] === $exApp->getVersion();
})) === 1;
});
if (count($exAppAppstoreData) === 1) {
return $this->exAppArchiveFetcher->downloadInfoXml($exAppAppstoreData);
}
return null;
}
/**
* Get latest ExApp release info by ExApp appid (in case of first installation or update)
*
* @param string $appId
*
* @return SimpleXMLElement|null
*/
public function getLatestExAppInfoFromAppstore(string $appId): ?SimpleXMLElement {
$exApps = $this->exAppFetcher->get();
$exAppAppstoreData = array_filter($exApps, function (array $exAppItem) use ($appId) {
return $exAppItem['id'] === $appId && count($exAppItem['releases']) > 0;
});
$exAppAppstoreData = end($exAppAppstoreData);
$exAppReleaseInfo = end($exAppAppstoreData['releases']);
if ($exAppReleaseInfo !== false) {
return $this->exAppArchiveFetcher->downloadInfoXml($exAppAppstoreData);
}
return null;
}
/**
* Request to ExApp with AppAPI auth headers and ExApp user initialization
*
* @param ExApp $exApp
* @param string $route
* @param string $userId
* @param string $method
* @param array $params
* @param array $options
* @param IRequest|null $request
*
* @return array|IResponse
*/
public function aeRequestToExApp(
ExApp $exApp,
string $route,
string $userId,
string $method = 'POST',
array $params = [],
array $options = [],
?IRequest $request = null,
): array|IResponse {
try {
$this->exAppUsersService->setupExAppUser($exApp, $userId);
} catch (\Exception $e) {
$this->logger->error(sprintf('Error while inserting ExApp %s user. Error: %s', $exApp->getAppid(), $e->getMessage()), ['exception' => $e]);
return ['error' => 'Error while inserting ExApp user: ' . $e->getMessage()];
}
return $this->requestToExApp($exApp, $route, $userId, $method, $params, $options, $request);
}
/**
* Request to ExApp by appId with AppAPI auth headers and ExApp user initialization
*
* @param string $appId
* @param string $route
* @param string $userId
* @param string $method
* @param array $params
* @param array $options
* @param IRequest|null $request
*
* @return array|IResponse
*/
public function aeRequestToExAppById(
string $appId,
string $route,
string $userId,
string $method = 'POST',
array $params = [],
array $options = [],
?IRequest $request = null,
): array|IResponse {
$exApp = $this->getExApp($appId);
if ($exApp === null) {
return ['error' => 'ExApp not found'];
}
return $this->aeRequestToExApp($exApp, $route, $userId, $method, $params, $options, $request);
}
/**
* Request to ExApp by appId with AppAPI auth headers
*
* @param string $appId
* @param string $route
* @param string|null $userId
* @param string $method
* @param array $params
* @param array $options
* @param IRequest|null $request
*
* @return array|IResponse
*/
public function requestToExAppById(
string $appId,
string $route,
?string $userId = null,
string $method = 'POST',
array $params = [],
array $options = [],
?IRequest $request = null,
): array|IResponse {
$exApp = $this->getExApp($appId);
if ($exApp === null) {
return ['error' => 'ExApp not found'];
}
return $this->requestToExApp($exApp, $route, $userId, $method, $params, $options, $request);
}
/**
* Request to ExApp with AppAPI auth headers
*
* @param ExApp $exApp
* @param string $route
* @param string|null $userId
* @param string $method
* @param array $params
* @param array $options
* @param IRequest|null $request
*
* @return array|IResponse
*/
public function requestToExApp(
ExApp $exApp,
string $route,
?string $userId = null,
string $method = 'POST',
array $params = [],
array $options = [],
?IRequest $request = null,
): array|IResponse {
$this->handleExAppDebug($exApp, $request, true);
try {
$url = self::getExAppUrl(
$exApp->getProtocol(),
$exApp->getHost(),
$exApp->getPort()) . $route;
if (isset($options['headers']) && is_array($options['headers'])) {
$options['headers'] = [...$options['headers'], ...$this->buildAppAPIAuthHeaders($request, $userId, $exApp)];
} else {
$options['headers'] = $this->buildAppAPIAuthHeaders($request, $userId, $exApp);
}
$options['nextcloud'] = [
'allow_local_address' => true, // it's required as we are using ExApp appid as hostname (usually local)
];
if (count($params) > 0) {
if ($method === 'GET') {
$url .= '?' . $this->getUriEncodedParams($params);
} else {
$options['json'] = $params;
}
}
switch ($method) {
case 'GET':
$response = $this->client->get($url, $options);
break;
case 'POST':
$response = $this->client->post($url, $options);
break;
case 'PUT':
$response = $this->client->put($url, $options);
break;
case 'DELETE':
$response = $this->client->delete($url, $options);
break;
default:
return ['error' => 'Bad HTTP method'];
}
return $response;
} catch (\Exception $e) {
$this->logger->error(sprintf('Error during request to ExApp %s: %s', $exApp->getAppid(), $e->getMessage()), ['exception' => $e]);
return ['error' => $e->getMessage()];
}
}
private function buildAppAPIAuthHeaders(?IRequest $request, ?string $userId, ExApp $exApp): array {
return [
'AA-VERSION' => $this->appManager->getAppVersion(Application::APP_ID, false),
'EX-APP-ID' => $exApp->getAppid(),
'EX-APP-VERSION' => $exApp->getVersion(),
'AUTHORIZATION-APP-API' => base64_encode($userId . ':' . $exApp->getSecret()),
'AA-REQUEST-ID' => $request instanceof IRequest ? $request->getId() : 'CLI',
];
}
/**
* @param string $protocol
* @param string $host
* @param int $port
*
* @return string
*/
public static function getExAppUrl(string $protocol, string $host, int $port): string {
return sprintf('%s://%s:%s', $protocol, $host, $port);
}
public function buildExAppHost(array $deployConfig): string {
if ((isset($deployConfig['net']) && $deployConfig['net'] !== 'host') || isset($deployConfig['host'])) {
return '0.0.0.0';
}
return '127.0.0.1';
}
private function getUriEncodedParams(array $params): string {
$paramsContent = '';
foreach ($params as $key => $value) {
if (is_array($value)) {
foreach ($value as $oneArrayValue) {
$paramsContent .= $key . '[]=' . urlencode($oneArrayValue) . '&';
}
unset($params[$key]);
}
}
return $paramsContent . http_build_query($params);
}
/**
* AppAPI authentication request validation for Nextcloud:
* - checks if ExApp exists and is enabled
* - checks if ExApp version changed and updates it in database
* - checks if ExApp shared secret valid
* - checks ExApp scopes <-> ExApp API copes
*
* More info in docs: https://cloud-py-api.github.io/app_api/authentication.html
*
* @param IRequest $request
* @param bool $isDav
*
* @return bool
*/
public function validateExAppRequestToNC(IRequest $request, bool $isDav = false): bool {
$this->throttler->sleepDelayOrThrowOnMax($request->getRemoteAddress(), Application::APP_ID);
$exApp = $this->getExApp($request->getHeader('EX-APP-ID'));
if ($exApp === null) {
$this->logger->error(sprintf('ExApp with appId %s not found.', $request->getHeader('EX-APP-ID')));
// Protection for guessing installed ExApps list
$this->throttler->registerAttempt(Application::APP_ID, $request->getRemoteAddress(), [
'appid' => $request->getHeader('EX-APP-ID'),
'userid' => explode(':', base64_decode($request->getHeader('AUTHORIZATION-APP-API')), 2)[0],
]);
return false;
}
$this->handleExAppDebug($exApp, $request, false);
$authorization = base64_decode($request->getHeader('AUTHORIZATION-APP-API'));
if ($authorization === false) {
$this->logger->error('Failed to parse AUTHORIZATION-APP-API');
return false;
}
$userId = explode(':', $authorization, 2)[0];
$authorizationSecret = explode(':', $authorization, 2)[1];
$authValid = $authorizationSecret === $exApp->getSecret();
if ($authValid) {
if (!$exApp->getEnabled()) {
// If ExApp is in initializing state, it is disabled yet, so we allow requests in such case
if (!isset(json_decode($exApp->getStatus(), true)['progress'])) {
$this->logger->error(sprintf('ExApp with appId %s is disabled (%s)', $request->getHeader('EX-APP-ID'), $request->getRequestUri()));
return false;
}
}
if (!$this->handleExAppVersionChange($request, $exApp)) {
return false;
}
if (!$isDav) {
try {
$path = $request->getPathInfo();
} catch (\Exception $e) {
$this->logger->error(sprintf('Error getting path info. Error: %s', $e->getMessage()), ['exception' => $e]);
return false;
}
} else {
$path = '/dav/';
}
$apiScope = $this->exAppApiScopeService->getApiScopeByRoute($path);
if ($apiScope === null) {
$this->logger->error(sprintf('Failed to check apiScope %s', $path));
return false;
}
// BASIC ApiScope is granted to all ExApps (all Api routes with BASIC scope group).
if ($apiScope->getScopeGroup() !== self::BASIC_API_SCOPE) {
if (!$this->exAppScopesService->passesScopeCheck($exApp, $apiScope->getScopeGroup())) {
$this->logger->error(sprintf('ExApp %s not passed scope group check %s', $exApp->getAppid(), $path));
return false;
}
}
// For APIs that not assuming work under user context we do not check ExApp users
if ($apiScope->getUserCheck()) {
try {
if (!$this->exAppUsersService->exAppUserExists($exApp->getAppid(), $userId)) {
$this->logger->error(sprintf('ExApp %s user %s does not exist', $exApp->getAppid(), $userId));
return false;
}
} catch (Exception $e) {
$this->logger->error(sprintf('Failed to get ExApp %s user %s. Error: %s', $exApp->getAppid(), $userId, $e->getMessage()), ['exception' => $e]);
return false;
}
}
return $this->finalizeRequestToNC($userId, $request);
} else {
$this->logger->error(sprintf('Invalid signature for ExApp: %s and user: %s.', $exApp->getAppid(), $userId !== '' ? $userId : 'null'));
$this->throttler->registerAttempt(Application::APP_ID, $request->getRemoteAddress(), [
'appid' => $request->getHeader('EX-APP-ID'),
'userid' => $userId,
]);
}
$this->logger->error(sprintf('ExApp %s request to NC validation failed.', $exApp->getAppid()));
return false;
}
/**
* Final step of AppAPI authentication request validation for Nextcloud:
* - sets active user (null if not a user context)
* - updates ExApp last response time
*
* @param string $userId
* @param IRequest $request
*
* @return bool
*/
private function finalizeRequestToNC(string $userId, IRequest $request): bool {
if ($userId !== '') {
$activeUser = $this->userManager->get($userId);
if ($activeUser === null) {
$this->logger->error(sprintf('Requested user does not exists: %s', $userId));
return false;
}
$this->userSession->setUser($activeUser);
} else {
$this->userSession->setUser(null);
}
$this->session->set('app_api', true);
$this->throttler->resetDelay($request->getRemoteAddress(), Application::APP_ID, [
'appid' => $request->getHeader('EX-APP-ID'),
'userid' => $userId,
]);
return true;
}
public function updateExAppLastCheckTime(ExApp $exApp): void {
$exApp->setLastCheckTime(time());
try {
$this->exAppMapper->updateLastCheckTime($exApp);
} catch (Exception $e) {
$this->logger->error(sprintf('Error while updating ExApp last check time for ExApp: %s. Error: %s', $exApp->getAppid(), $e->getMessage()), ['exception' => $e]);
}
}
public function updateExAppVersion(ExApp $exApp): bool {
try {
return $this->exAppMapper->updateExAppVersion($exApp) === 1;
} catch (Exception $e) {
$this->logger->error(sprintf('Failed to update ExApp %s version to %s', $exApp->getAppid(), $exApp->getVersion()), ['exception' => $e]);
return false;
}
}
public function updateExAppName(ExApp $exApp): bool {
try {
return $this->exAppMapper->updateExAppName($exApp) === 1;
} catch (Exception $e) {
$this->logger->error(sprintf('Failed to update ExApp %s name to %s', $exApp->getAppid(), $exApp->getName()), ['exception' => $e]);
return false;
}
}
/**
* Check if ExApp version changed and update it in database.
* Immediately disable ExApp and send notifications to the administrators (users of admins group).
* This handling only intentional case of manual ExApp update
* so the administrator must re-enable ExApp in UI or CLI after that.
*
* Ref: https://github.com/cloud-py-api/app_api/pull/29
* TODO: Add link to docs with warning and mark as not-recommended
*
* @param IRequest $request
* @param ExApp $exApp
*
* @return bool
*/
public function handleExAppVersionChange(IRequest $request, ExApp $exApp): bool {
$requestExAppVersion = $request->getHeader('EX-APP-VERSION');
$versionValid = $exApp->getVersion() === $requestExAppVersion;
if (!$versionValid) {
// Update ExApp version
$oldVersion = $exApp->getVersion();
$exApp->setVersion($requestExAppVersion);
if (!$this->updateExAppVersion($exApp)) {
return false;
}
if ($this->disableExApp($exApp)) {
$this->exNotificationsManager->sendAdminsNotification($exApp->getAppid(), [
'object' => 'ex_app_update',
'object_id' => $exApp->getAppid(),
'subject_type' => 'ex_app_version_update',
'subject_params' => [
'rich_subject' => 'ExApp updated, action required!',
'rich_subject_params' => [],
'rich_message' => sprintf('ExApp %s disabled due to update from %s to %s. Manual re-enable required.', $exApp->getAppid(), $oldVersion, $exApp->getVersion()),
'rich_message_params' => [],
],
]);
}
return false;
}
return true;
}
public function getExAppsList(string $list = 'enabled'): array {
try {
$exApps = $this->exAppMapper->findAll();
if ($list === 'enabled') {
$exApps = array_values(array_filter($exApps, function (ExApp $exApp) {
return $exApp->getEnabled() === 1;
}));
}
$exApps = array_map(function (ExApp $exApp) {
return [
'id' => $exApp->getAppid(),
'name' => $exApp->getName(),
'version' => $exApp->getVersion(),
'enabled' => filter_var($exApp->getEnabled(), FILTER_VALIDATE_BOOLEAN),
'last_check_time' => $exApp->getLastCheckTime(),
'system' => $this->exAppUsersService->exAppUserExists($exApp->getAppid(), ''),
];
}, $exApps);
} catch (Exception $e) {
$this->logger->error(sprintf('Error while getting ExApps list. Error: %s', $e->getMessage()), ['exception' => $e]);
$exApps = [];
}
return $exApps;
}
public function getNCUsersList(): ?array {
return array_map(function (IUser $user) {
return $user->getUID();
}, $this->userManager->searchDisplayName(''));
}
private function getCustomLogger(string $name): LoggerInterface {
$path = $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . '/' . $name;
return $this->logFactory->getCustomPsrLogger($path);
}
private function buildRequestInfo(IRequest $request): array {
$headers = [];
$aeHeadersList = [
'AA-VERSION',
'EX-APP-VERSION',
];
foreach ($aeHeadersList as $header) {
if ($request->getHeader($header) !== '') {
$headers[$header] = $request->getHeader($header);
}
}
return [
'headers' => $headers,
'params' => $request->getParams(),
];
}
private function getExAppDebugSettings(ExApp $exApp): array {
$exAppConfigs = $this->exAppConfigService->getAppConfigValues($exApp->getAppid(), ['debug', 'loglevel']);
$debug = false;
$level = $this->config->getSystemValue('loglevel', 2);
foreach ($exAppConfigs as $exAppConfig) {
if ($exAppConfig['configkey'] === 'debug') {
$debug = $exAppConfig['configvalue'] === 1;
}
if ($exAppConfig['configkey'] === 'loglevel') {
$level = intval($exAppConfig['configvalue']);
}
}
return [
'debug' => $debug,
'level' => $level,
];
}
private function handleExAppDebug(ExApp $exApp, ?IRequest $request, bool $fromNextcloud = true): void {
$exAppDebugSettings = $this->getExAppDebugSettings($exApp);
if ($exAppDebugSettings['debug']) {
$message = $fromNextcloud
? '[' . Application::APP_ID . '] Nextcloud --> ' . $exApp->getAppid()
: '[' . Application::APP_ID . '] ' . $exApp->getAppid() . ' --> Nextcloud';
$aeDebugLogger = $this->getCustomLogger('aa_debug.log');
$aeDebugLogger->log($exAppDebugSettings['level'], $message, [
'app' => $exApp->getAppid(),
'request_info' => $request instanceof IRequest ? $this->buildRequestInfo($request) : 'CLI request',
]);
}
}
}