Files
Cloud-CMS/lib/Controller/Applications.php

653 lines
22 KiB
PHP
Raw Permalink Normal View History

2025-12-02 10:32:59 -05:00
<?php
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo 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 Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Controller;
use Carbon\Carbon;
use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\AuthCodeGrant;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Stash\Interfaces\PoolInterface;
use Xibo\Entity\ApplicationScope;
use Xibo\Factory\ApplicationFactory;
use Xibo\Factory\ApplicationRedirectUriFactory;
use Xibo\Factory\ApplicationScopeFactory;
use Xibo\Factory\ConnectorFactory;
use Xibo\Factory\UserFactory;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\Session;
use Xibo\OAuth\AuthCodeRepository;
use Xibo\OAuth\RefreshTokenRepository;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ControllerNotImplemented;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
/**
* Class Applications
* @package Xibo\Controller
*/
class Applications extends Base
{
/**
* @var Session
*/
private $session;
/**
* @var ApplicationFactory
*/
private $applicationFactory;
/**
* @var ApplicationRedirectUriFactory
*/
private $applicationRedirectUriFactory;
/** @var ApplicationScopeFactory */
private $applicationScopeFactory;
/** @var UserFactory */
private $userFactory;
/** @var PoolInterface */
private $pool;
/** @var \Xibo\Factory\ConnectorFactory */
private $connectorFactory;
/**
* Set common dependencies.
* @param Session $session
* @param ApplicationFactory $applicationFactory
* @param ApplicationRedirectUriFactory $applicationRedirectUriFactory
* @param $applicationScopeFactory
* @param UserFactory $userFactory
* @param $pool
* @param \Xibo\Factory\ConnectorFactory $connectorFactory
*/
public function __construct(
$session,
$applicationFactory,
$applicationRedirectUriFactory,
$applicationScopeFactory,
$userFactory,
$pool,
ConnectorFactory $connectorFactory
) {
$this->session = $session;
$this->applicationFactory = $applicationFactory;
$this->applicationRedirectUriFactory = $applicationRedirectUriFactory;
$this->applicationScopeFactory = $applicationScopeFactory;
$this->userFactory = $userFactory;
$this->pool = $pool;
$this->connectorFactory = $connectorFactory;
}
/**
* Display Page
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function displayPage(Request $request, Response $response)
{
// Load all connectors and output any javascript.
$connectorJavaScript = [];
foreach ($this->connectorFactory->query(['isVisible' => 1]) as $connector) {
try {
// Create a connector, add in platform settings and register it with the dispatcher.
$connectorObject = $this->connectorFactory->create($connector);
$settingsFormJavaScript = $connectorObject->getSettingsFormJavaScript();
if (!empty($settingsFormJavaScript)) {
$connectorJavaScript[] = $settingsFormJavaScript;
}
} catch (\Exception $exception) {
// Log and ignore.
$this->getLog()->error('Incorrectly configured connector. e=' . $exception->getMessage());
}
}
$this->getState()->template = 'applications-page';
$this->getState()->setData([
'connectorJavaScript' => $connectorJavaScript,
]);
return $this->render($request, $response);
}
/**
* Display page grid
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function grid(Request $request, Response $response)
{
$this->getState()->template = 'grid';
$sanitizedParams = $this->getSanitizer($request->getParams());
$applications = $this->applicationFactory->query(
$this->gridRenderSort($sanitizedParams),
$this->gridRenderFilter(
['name' => $sanitizedParams->getString('name')],
$sanitizedParams
)
);
foreach ($applications as $application) {
if ($this->isApi($request)) {
throw new AccessDeniedException();
}
// Include the buttons property
$application->includeProperty('buttons');
// Add an Edit button (edit form also exposes the secret - not possible to get through the API)
$application->buttons = [];
if ($application->userId == $this->getUser()->userId || $this->getUser()->getUserTypeId() == 1) {
// Edit
$application->buttons[] = [
'id' => 'application_edit_button',
'url' => $this->urlFor($request, 'application.edit.form', ['id' => $application->key]),
'text' => __('Edit')
];
// Delete
$application->buttons[] = [
'id' => 'application_delete_button',
'url' => $this->urlFor($request, 'application.delete.form', ['id' => $application->key]),
'text' => __('Delete')
];
}
}
$this->getState()->setData($applications);
$this->getState()->recordsTotal = $this->applicationFactory->countLast();
return $this->render($request, $response);
}
/**
* Display the Authorize form.
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws InvalidArgumentException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function authorizeRequest(Request $request, Response $response)
{
// Pull authorize params from our session
/** @var AuthorizationRequest $authParams */
$authParams = $this->session->get('authParams');
if (!$authParams) {
throw new InvalidArgumentException(__('Authorisation Parameters missing from session.'), 'authParams');
}
if ($this->applicationFactory->checkAuthorised($authParams->getClient()->getIdentifier(), $this->getUser()->userId)) {
return $this->authorize($request->withParsedBody(['authorization' => 'Approve']), $response);
}
$client = $this->applicationFactory->getClientEntity($authParams->getClient()->getIdentifier())->load();
// Process any scopes.
$scopes = [];
$authScopes = $authParams->getScopes();
// if we have scopes in the request, make sure we only add the valid ones.
// the default scope is all, if it's not set on the Application, $scopes will still be empty here.
if ($authScopes !== null) {
$validScopes = $this->applicationScopeFactory->finalizeScopes(
$authScopes,
$authParams->getGrantTypeId(),
$client
);
// get all the valid scopes by their ID, we need to do this to present more details on the authorize form.
foreach ($validScopes as $scope) {
$scopes[] = $this->applicationScopeFactory->getById($scope->getIdentifier());
}
if (count($scopes) <= 0) {
throw new InvalidArgumentException(
__('This application has not requested access to anything.'),
'authParams'
);
}
// update scopes in auth request in session to scopes we actually present for approval
$authParams->setScopes($validScopes);
}
// Reasert the auth params.
$this->session->set('authParams', $authParams);
// Get, show page
$this->getState()->template = 'applications-authorize-page';
$this->getState()->setData([
'forceHide' => true,
'authParams' => $authParams,
'scopes' => $scopes,
'application' => $client
]);
return $this->render($request, $response);
}
/**
* Authorize an oAuth request
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Exception
*/
public function authorize(Request $request, Response $response)
{
// Pull authorize params from our session
/** @var AuthorizationRequest $authRequest */
$authRequest = $this->session->get('authParams');
if (!$authRequest) {
throw new InvalidArgumentException(__('Authorisation Parameters missing from session.'), 'authParams');
}
$sanitizedQueryParams = $this->getSanitizer($request->getParams());
$apiKeyPaths = $this->getConfig()->getApiKeyDetails();
$privateKey = $apiKeyPaths['privateKeyPath'];
$encryptionKey = $apiKeyPaths['encryptionKey'];
$server = new AuthorizationServer(
$this->applicationFactory,
new \Xibo\OAuth\AccessTokenRepository($this->getLog(), $this->pool, $this->applicationFactory),
$this->applicationScopeFactory,
$privateKey,
$encryptionKey
);
$server->enableGrantType(
new AuthCodeGrant(
new AuthCodeRepository(),
new RefreshTokenRepository($this->getLog(), $this->pool),
new \DateInterval('PT10M')
),
new \DateInterval('PT1H')
);
// get oauth User Entity and set the UserId to the current web userId
$authRequest->setUser($this->getUser());
// We are authorized
if ($sanitizedQueryParams->getString('authorization') === 'Approve') {
$authRequest->setAuthorizationApproved(true);
$this->applicationFactory->setApplicationApproved(
$authRequest->getClient()->getIdentifier(),
$authRequest->getUser()->getIdentifier(),
Carbon::now()->format(DateFormatHelper::getSystemFormat()),
$request->getAttribute('ip_address')
);
$this->getLog()->audit(
'Auth',
0,
'Application access approved',
[
'Application identifier ends with' => substr($authRequest->getClient()->getIdentifier(), -8),
'Application Name' => $authRequest->getClient()->getName()
]
);
} else {
$authRequest->setAuthorizationApproved(false);
}
// Redirect back to the specified redirect url
try {
return $server->completeAuthorizationRequest($authRequest, $response);
} catch (OAuthServerException $exception) {
if ($exception->hasRedirect()) {
return $response->withRedirect($exception->getRedirectUri());
} else {
throw $exception;
}
}
}
/**
* Form to register a new application.
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function addForm(Request $request, Response $response)
{
$this->getState()->template = 'applications-form-add';
return $this->render($request, $response);
}
/**
* Edit Application
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function editForm(Request $request, Response $response, $id)
{
// Get the client
$client = $this->applicationFactory->getById($id);
if ($client->userId != $this->getUser()->userId && $this->getUser()->getUserTypeId() != 1) {
throw new AccessDeniedException();
}
// Load this clients details.
$client->load();
$scopes = $this->applicationScopeFactory->query();
foreach ($scopes as $scope) {
/** @var ApplicationScope $scope */
$found = false;
foreach ($client->scopes as $checked) {
if ($checked->id == $scope->id) {
$found = true;
break;
}
}
$scope->setUnmatchedProperty('selected', $found ? 1 : 0);
}
// Render the view
$this->getState()->template = 'applications-form-edit';
$this->getState()->setData([
'client' => $client,
'scopes' => $scopes,
]);
return $this->render($request, $response);
}
/**
* Delete Application Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function deleteForm(Request $request, Response $response, $id)
{
// Get the client
$client = $this->applicationFactory->getById($id);
if ($client->userId != $this->getUser()->userId && $this->getUser()->getUserTypeId() != 1) {
throw new AccessDeniedException();
}
$this->getState()->template = 'applications-form-delete';
$this->getState()->setData([
'client' => $client,
]);
return $this->render($request, $response);
}
/**
* Register a new application with OAuth
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws InvalidArgumentException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function add(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$application = $this->applicationFactory->create();
$application->name = $sanitizedParams->getString('name');
if ($application->name == '') {
throw new InvalidArgumentException(__('Please enter Application name'), 'name');
}
$application->userId = $this->getUser()->userId;
$application->save();
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Added %s'), $application->name),
'data' => $application,
'id' => $application->key
]);
return $this->render($request, $response);
}
/**
* Edit Application
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws InvalidArgumentException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function edit(Request $request, Response $response, $id)
{
$this->getLog()->debug('Editing ' . $id);
// Get the client
$client = $this->applicationFactory->getById($id);
if ($client->userId != $this->getUser()->userId && $this->getUser()->getUserTypeId() != 1) {
throw new AccessDeniedException();
}
$sanitizedParams = $this->getSanitizer($request->getParams());
$client->name = $sanitizedParams->getString('name');
$client->authCode = $sanitizedParams->getCheckbox('authCode');
$client->clientCredentials = $sanitizedParams->getCheckbox('clientCredentials');
$client->isConfidential = $sanitizedParams->getCheckbox('isConfidential');
if ($sanitizedParams->getCheckbox('resetKeys') == 1) {
$client->resetSecret();
$this->pool->getItem('C_' . $client->key)->clear();
}
if ($client->authCode === 1) {
$client->description = $sanitizedParams->getString('description');
$client->logo = $sanitizedParams->getString('logo');
$client->coverImage = $sanitizedParams->getString('coverImage');
$client->companyName = $sanitizedParams->getString('companyName');
$client->termsUrl = $sanitizedParams->getString('termsUrl');
$client->privacyUrl = $sanitizedParams->getString('privacyUrl');
}
// Delete all the redirect urls and add them again
$client->load();
foreach ($client->redirectUris as $uri) {
$uri->delete();
}
$client->redirectUris = [];
// Do we have a redirect?
$redirectUris = $sanitizedParams->getArray('redirectUri');
foreach ($redirectUris as $redirectUri) {
if ($redirectUri == '') {
continue;
}
$redirect = $this->applicationRedirectUriFactory->create();
$redirect->redirectUri = $redirectUri;
$client->assignRedirectUri($redirect);
}
// clear scopes
$client->scopes = [];
// API Scopes
foreach ($this->applicationScopeFactory->query() as $scope) {
/** @var ApplicationScope $scope */
// See if this has been checked this time
$checked = $sanitizedParams->getCheckbox('scope_' . $scope->id);
// Assign scopes
if ($checked) {
$client->assignScope($scope);
}
}
// Change the ownership?
if ($sanitizedParams->getInt('userId') !== null) {
// Check we have permissions to view this user
$user = $this->userFactory->getById($sanitizedParams->getInt('userId'));
$this->getLog()->debug('Attempting to change ownership to ' . $user->userId . ' - ' . $user->userName);
if (!$this->getUser()->checkViewable($user)) {
throw new InvalidArgumentException(__('You do not have permission to assign this user'), 'userId');
}
$client->userId = $user->userId;
}
$client->save();
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Edited %s'), $client->name),
'data' => $client,
'id' => $client->key
]);
return $this->render($request, $response);
}
/**
* Delete application
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function delete(Request $request, Response $response, $id)
{
// Get the client
$client = $this->applicationFactory->getById($id);
if ($client->userId != $this->getUser()->userId && $this->getUser()->getUserTypeId() != 1) {
throw new AccessDeniedException();
}
$client->delete();
$this->pool->getItem('C_' . $client->key)->clear();
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $client->name)
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @param $userId
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws ControllerNotImplemented
* @throws GeneralException
* @throws InvalidArgumentException
*/
public function revokeAccess(Request $request, Response $response, $id, $userId)
{
if ($userId === null) {
throw new InvalidArgumentException(__('No User ID provided'));
}
if (empty($id)) {
throw new InvalidArgumentException(__('No Client id provided'));
}
$client = $this->applicationFactory->getClientEntity($id);
if ($this->getUser()->userId != $userId) {
throw new InvalidArgumentException(__('Access denied: You do not own this authorization.'));
}
// remove record in lk table
$this->applicationFactory->revokeAuthorised($userId, $client->key);
// clear cache for this clientId/userId pair, this is how we know the application is no longer approved
$this->pool->getItem('C_' . $client->key . '/' . $userId)->clear();
$this->getLog()->audit(
'Auth',
0,
'Application access revoked',
[
'Application identifier ends with' => substr($client->key, -8),
'Application Name' => $client->getName()
]
);
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Access to %s revoked'), $client->name)
]);
return $this->render($request, $response);
}
}