first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-06-08 17:09:23 -04:00
commit df3a033196
17887 changed files with 8637778 additions and 0 deletions
@@ -0,0 +1,117 @@
<?php
/**
* @file api/v1/_dois/BackendDoiHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPBackendDoiHandler
*
* @ingroup api_v1_backend
*
* @brief Handle API requests for backend operations.
*
*/
namespace PKP\API\v1\_dois;
use APP\facades\Repo;
use APP\core\Request;
use PKP\core\APIResponse;
use PKP\db\DAORegistry;
use PKP\handler\APIHandler;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\DoisEnabledPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\submission\GenreDAO;
use Slim\Http\Request as SlimRequest;
class PKPBackendDoiHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_endpoints = array_merge_recursive($this->_endpoints, [
'PUT' => [
[
'pattern' => $this->getEndpointPattern() . "/publications/{publicationId:\d+}",
'handler' => [$this, 'editPublication'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
]
]
]);
parent::__construct();
}
/**
* @param Request $request
* @param array $args
* @param array $roleAssignments
*
* @return bool
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
// This endpoint is not available at the site-wide level
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
$this->addPolicy(new DoisEnabledPolicy($request->getContext()));
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* @throws \Exception
*/
public function editPublication(SlimRequest $slimRequest, APIResponse $response, array $args): \Slim\Http\Response
{
$context = $this->getRequest()->getContext();
$publication = Repo::publication()->get($args['publicationId']);
if (!$publication) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$submission = Repo::submission()->get($publication->getData('submissionId'));
if ($submission->getData('contextId') !== $context->getId()) {
return $response->withStatus(403)->withJsonError('api.dois.403.editItemOutOfContext');
}
$params = $this->convertStringsToSchema(\PKP\services\PKPSchemaService::SCHEMA_PUBLICATION, $slimRequest->getParsedBody());
$doi = Repo::doi()->get((int) $params['doiId']);
if (!$doi) {
return $response->withStatus(404)->withJsonError('api.dois.404.doiNotFound');
}
Repo::publication()->edit($publication, ['doiId' => $doi->getId()]);
$publication = Repo::publication()->get($publication->getId());
$submission = Repo::submission()->get($publication->getData('submissionId'));
$userGroups = Repo::userGroup()->getCollector()
->filterByContextIds([$submission->getData('contextId')])
->getMany();
/** @var GenreDAO $genreDao */
$genreDao = DAORegistry::getDAO('GenreDAO');
$genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray();
return $response->withJson(Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->map($publication), 200);
}
}
+170
View File
@@ -0,0 +1,170 @@
<?php
/**
* @file api/v1/_email/PKPEmailHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPEmailHandler
*
* @ingroup api_v1_announcement
*
* @brief Handle API request to send bulk email
*
*/
namespace PKP\API\v1\_email;
use APP\facades\Repo;
use Illuminate\Support\Facades\Bus;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\jobs\bulk\BulkEmailSender;
use PKP\mail\Mailer;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use Psr\Http\Message\ServerRequestInterface;
class PKPEmailHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = '_email';
$this->_endpoints = [
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'create'],
'roles' => [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER],
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Create a jobs queue to send a bulk email to users in one or
* more user groups
*
* @param array $args arguments
*
* @return APIResponse
*/
public function create(ServerRequestInterface $slimRequest, APIResponse $response, array $args)
{
$context = $this->getRequest()->getContext();
$contextId = $context->getId();
if (!in_array($contextId, (array) $this->getRequest()->getSite()->getData('enableBulkEmails'))) {
return $response->withStatus(403)->withJsonError('api.emails.403.disabled');
}
$requestParams = $slimRequest->getParsedBody();
$params = [];
foreach ($requestParams as $param => $val) {
switch ($param) {
case 'userGroupIds':
if (!is_array($val)) {
$val = strlen(trim($val))
? explode(',', $val)
: [];
}
$params[$param] = array_map('intval', $val);
break;
case 'body':
case 'subject':
$params[$param] = $val;
break;
case 'copy':
$params[$param] = (bool) $val;
break;
}
}
$errors = [];
if (empty($params['body'])) {
$errors['body'] = [__('api.emails.400.missingBody')];
}
if (empty($params['subject'])) {
$errors['subject'] = [__('api.emails.400.missingSubject')];
}
if (empty($params['userGroupIds'])) {
$errors['userGroupIds'] = [__('api.emails.400.missingUserGroups')];
}
if ($errors) {
return $response->withJson($errors, 400);
}
foreach ($params['userGroupIds'] as $userGroupId) {
if (!Repo::userGroup()->contextHasGroup($contextId, $userGroupId)
|| in_array($userGroupId, (array) $context->getData('disableBulkEmailUserGroups'))) {
return $response->withJson([
'userGroupIds' => [__('api.emails.403.notAllowedUserGroup')],
], 400);
}
}
$userIds = Repo::user()->getCollector()
->filterByContextIds([$contextId])
->filterByUserGroupIds($params['userGroupIds'])
->getIds()
->toArray();
if (!empty($params['copy'])) {
$currentUserId = $this->getRequest()->getUser()->getId();
if (!in_array($currentUserId, $userIds)) {
$userIds[] = $currentUserId;
}
}
$batches = array_chunk($userIds, Mailer::BULK_EMAIL_SIZE_LIMIT);
$jobs = [];
foreach ($batches as $userIds) {
$jobs[] = new BulkEmailSender(
$userIds,
$contextId,
$params['subject'],
$params['body'],
$context->getData('contactEmail'),
$context->getData('contactName')
);
}
Bus::batch($jobs)->dispatch();
return $response->withJson([
'totalBulkJobs' => count($batches),
], 200);
}
}
@@ -0,0 +1,144 @@
<?php
/**
* @file api/v1/_library/PKPLibraryHandler.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2003-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPLibraryHandler
*
* @ingroup api_v1_announcement
*
* @brief Handle API requests for announcement operations.
*
*/
namespace PKP\API\v1\_library;
use APP\core\Application;
use APP\core\Services;
use APP\file\LibraryFileManager;
use PKP\context\LibraryFile;
use PKP\context\LibraryFileDAO;
use PKP\core\APIResponse;
use PKP\db\DAORegistry;
use PKP\handler\APIHandler;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\SubmissionAccessPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use Psr\Http\Message\ServerRequestInterface;
class PKPLibraryHandler extends APIHandler
{
public function __construct()
{
$this->_handlerPath = '_library';
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getLibrary'],
'roles' => [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR],
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
if ($request->getUserVar('includeSubmissionId')) {
$this->addPolicy(new SubmissionAccessPolicy($request, $args, $roleAssignments, 'includeSubmissionId'));
}
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a list of all files in the library
*
* @param array $args arguments
*
* @return APIResponse
*/
public function getLibrary(ServerRequestInterface $slimRequest, APIResponse $response, array $args)
{
/** @var LibraryFileDAO $libraryFileDao */
$libraryFileDao = DAORegistry::getDAO('LibraryFileDAO');
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
$context = $this->getRequest()->getContext();
$contextId = $context->getId();
$libraryFileManager = new LibraryFileManager($contextId);
$files = [];
$params = $slimRequest->getQueryParams();
if (isset($params['includeSubmissionId'])) {
$result = $libraryFileDao->getBySubmissionId($submission->getId());
while ($file = $result->next()) {
$files[] = $this->fileToResponse($file, $libraryFileManager);
}
}
$result = $libraryFileDao->getByContextId($contextId);
while ($file = $result->next()) {
$files[] = $this->fileToResponse($file, $libraryFileManager);
}
return $response->withJson([
'items' => $files,
'itemsMax' => count($files),
], 200);
}
/**
* Convert a file object to the JSON response object
*/
protected function fileToResponse(LibraryFile $file, LibraryFileManager $libraryFileManager): array
{
$request = Application::get()->getRequest();
$urlArgs = [
'libraryFileId' => $file->getId(),
];
if ($file->getSubmissionId()) {
$urlArgs['submissionId'] = $file->getSubmissionId();
}
return [
'id' => $file->getId(),
'filename' => $file->getServerFileName(),
'name' => $file->getName(null),
'mimetype' => $file->getFileType(),
'documentType' => Services::get('file')->getDocumentType($file->getFileType()),
'submissionId' => $file->getSubmissionId() ?? 0,
'type' => $file->getType(),
'typeName' => __($libraryFileManager->getTitleKeyFromType($file->getType())),
'url' => $request->getDispatcher()->url(
$request,
Application::ROUTE_COMPONENT,
null,
'api.file.FileApiHandler',
'downloadLibraryFile',
null,
$urlArgs
),
];
}
}
@@ -0,0 +1,131 @@
<?php
/**
* @file api/v1/_payments/PKPBackendPaymentsSettingsHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPBackendPaymentsSettingsHandler
*
* @ingroup api_v1_backend
*
* @brief A private API endpoint handler for payment settings. It may be
* possible to deprecate this when we have a working endpoint for plugin
* settings.
*/
namespace PKP\API\v1\_payments;
use APP\core\Services;
use Illuminate\Support\Collection;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\plugins\PluginRegistry;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\services\interfaces\EntityWriteInterface;
class PKPBackendPaymentsSettingsHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$rootPattern = '/{contextPath}/api/{version}/_payments';
$this->_endpoints = array_merge_recursive($this->_endpoints, [
'PUT' => [
[
'pattern' => $rootPattern,
'handler' => [$this, 'edit'],
'roles' => [
Role::ROLE_ID_SITE_ADMIN,
Role::ROLE_ID_MANAGER,
],
],
],
]);
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Receive requests to edit the payments form
*
* @param \Slim\Http\Request $slimRequest Slim request object
* @param \PKP\core\APIResponse $response object
*
* @return \PKP\core\APIResponse
*/
public function edit($slimRequest, $response, $args)
{
$request = $this->getRequest();
$context = $request->getContext();
$params = $slimRequest->getParsedBody();
$contextService = Services::get('context');
// Process query params to format incoming data as needed
foreach ($slimRequest->getParsedBody() as $param => $val) {
switch ($param) {
case 'paymentsEnabled':
$params[$param] = $val === 'true';
break;
case 'currency':
$params[$param] = (string) $val;
break;
}
}
if (isset($params['currency'])) {
$errors = $contextService->validate(
EntityWriteInterface::VALIDATE_ACTION_EDIT,
['currency' => $params['currency']],
$context->getSupportedFormLocales(),
$context->getPrimaryLocale()
);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
}
PluginRegistry::loadCategory('paymethod', true);
Hook::call(
'API::payments::settings::edit',
[
$slimRequest,
$request,
$params,
$updatedSettings = new Collection(),
$errors = new Collection()
]
);
if ($errors->isNotEmpty()) {
return $response->withStatus(400)->withJson($errors->toArray());
}
$context = $contextService->get($context->getId());
$contextService->edit($context, $params, $request);
return $response->withJson(array_merge($params, $updatedSettings->toArray()));
}
}
@@ -0,0 +1,254 @@
<?php
/**
* @file api/v1/_submissions/PKPBackendSubmissionsHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPBackendSubmissionsHandler
*
* @ingroup api_v1_backend
*
* @brief Handle API requests for backend operations.
*
*/
namespace PKP\API\v1\_submissions;
use APP\core\Application;
use APP\facades\Repo;
use APP\submission\Collector;
use PKP\core\APIResponse;
use PKP\db\DAORegistry;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\SubmissionAccessPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use Slim\Http\Request;
use Slim\Http\Response;
abstract class PKPBackendSubmissionsHandler extends APIHandler
{
/** @var int Max items that can be requested */
public const MAX_COUNT = 100;
/**
* Constructor
*/
public function __construct()
{
$rootPattern = '/{contextPath}/api/{version}/_submissions';
$this->_endpoints = array_merge_recursive($this->_endpoints, [
'GET' => [
[
'pattern' => "{$rootPattern}",
'handler' => [$this, 'getMany'],
'roles' => [
Role::ROLE_ID_SITE_ADMIN,
Role::ROLE_ID_MANAGER,
Role::ROLE_ID_SUB_EDITOR,
Role::ROLE_ID_AUTHOR,
Role::ROLE_ID_REVIEWER,
Role::ROLE_ID_ASSISTANT,
],
],
],
'DELETE' => [
[
'pattern' => "{$rootPattern}/{submissionId:\d+}",
'handler' => [$this, 'delete'],
'roles' => [
Role::ROLE_ID_SITE_ADMIN,
Role::ROLE_ID_MANAGER,
Role::ROLE_ID_AUTHOR,
],
],
],
]);
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize()
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
$routeName = $this->getSlimRequest()->getAttribute('route')->getName();
if (in_array($routeName, ['delete'])) {
$this->addPolicy(new SubmissionAccessPolicy($request, $args, $roleAssignments));
}
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a collection of submissions
*
* @param Request $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return Response
*/
public function getMany($slimRequest, $response, $args)
{
$request = Application::get()->getRequest();
$currentUser = $request->getUser();
$context = $request->getContext();
if (!$context) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$collector = $this->getSubmissionCollector($slimRequest->getQueryParams());
// Anyone not a manager or site admin can only access their assigned
// submissions
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
$canAccessUnassignedSubmission = !empty(array_intersect([Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER], $userRoles));
Hook::call('API::submissions::params', [$collector, $slimRequest]);
if (!$canAccessUnassignedSubmission) {
if (!is_array($collector->assignedTo)) {
$collector->assignedTo([$currentUser->getId()]);
} elseif ($collector->assignedTo != [$currentUser->getId()]) {
return $response->withStatus(403)->withJsonError('api.submissions.403.requestedOthersUnpublishedSubmissions');
}
}
$submissions = $collector->getMany();
$userGroups = Repo::userGroup()->getCollector()
->filterByContextIds([$context->getId()])
->getMany();
/** @var \PKP\submission\GenreDAO $genreDao */
$genreDao = DAORegistry::getDAO('GenreDAO');
$genres = $genreDao->getByContextId($context->getId())->toArray();
return $response->withJson([
'itemsMax' => $collector->limit(null)->offset(null)->getCount(),
'items' => Repo::submission()->getSchemaMap()->mapManyToSubmissionsList($submissions, $userGroups, $genres)->values(),
], 200);
}
/**
* Configure a submission Collector based on the query params
*/
protected function getSubmissionCollector(array $queryParams): Collector
{
$request = Application::get()->getRequest();
$context = $request->getContext();
$collector = Repo::submission()->getCollector()
->filterByContextIds([$context->getId()])
->limit(30)
->offset(0);
foreach ($queryParams as $param => $val) {
switch ($param) {
case 'orderBy':
if (in_array($val, [
$collector::ORDERBY_DATE_PUBLISHED,
$collector::ORDERBY_DATE_SUBMITTED,
$collector::ORDERBY_LAST_ACTIVITY,
$collector::ORDERBY_LAST_MODIFIED,
$collector::ORDERBY_SEQUENCE,
$collector::ORDERBY_TITLE,
])) {
$direction = isset($queryParams['orderDirection']) && $queryParams['orderDirection'] === $collector::ORDER_DIR_ASC
? $collector::ORDER_DIR_ASC
: $collector::ORDER_DIR_DESC;
$collector->orderBy($val, $direction);
}
break;
case 'status':
$collector->filterByStatus(array_map('intval', $this->paramToArray($val)));
break;
case 'stageIds':
$collector->filterByStageIds(array_map('intval', $this->paramToArray($val)));
break;
case 'categoryIds':
$collector->filterByCategoryIds(array_map('intval', $this->paramToArray($val)));
break;
case 'assignedTo':
$val = array_map('intval', $this->paramToArray($val));
if ($val == [\PKP\submission\Collector::UNASSIGNED]) {
$val = array_shift($val);
}
$collector->assignedTo($val);
break;
case 'daysInactive':
$collector->filterByDaysInactive((int) $val);
break;
case 'offset':
$collector->offset((int) $val);
break;
case 'searchPhrase':
$collector->searchPhrase($val);
break;
case 'count':
$collector->limit(min(self::MAX_COUNT, (int) $val));
break;
case 'isIncomplete':
$collector->filterByIncomplete(true);
break;
case 'isOverdue':
$collector->filterByOverdue(true);
break;
}
}
return $collector;
}
/**
* Delete a submission
*
* @param Request $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return Response
*/
public function delete($slimRequest, $response, $args)
{
$request = $this->getRequest();
$context = $request->getContext();
$submissionId = (int) $args['submissionId'];
$submission = Repo::submission()->get($submissionId);
if (!$submission) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
if ($context->getId() != $submission->getData('contextId')) {
return $response->withStatus(403)->withJsonError('api.submissions.403.deleteSubmissionOutOfContext');
}
if (!Repo::submission()->canCurrentUserDelete($submission)) {
return $response->withStatus(403)->withJsonError('api.submissions.403.unauthorizedDeleteSubmission');
}
Repo::submission()->delete($submission);
return $response->withJson(true);
}
}
@@ -0,0 +1,247 @@
<?php
/**
* @file api/v1/uploadPublicFile/PKPUploadPublicFileHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPUploadPublicFileHandler
*
* @ingroup api_v1_uploadPublicFile
*
* @brief Handle API requests to upload a file to a user's public directory.
*/
namespace PKP\API\v1\_uploadPublicFile;
use APP\core\Application;
use FilesystemIterator;
use PKP\config\Config;
use PKP\core\Core;
use PKP\core\PKPString;
use PKP\file\FileManager;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
class PKPUploadPublicFileHandler extends APIHandler
{
/**
* @copydoc APIHandler::__construct()
*/
public function __construct()
{
$this->_handlerPath = '_uploadPublicFile';
$roles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_REVIEWER, Role::ROLE_ID_AUTHOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_READER];
$this->_endpoints = [
'OPTIONS' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getOptions'],
'roles' => $roles,
],
],
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'uploadFile'],
'roles' => $roles,
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* A helper method which adds the necessary response headers to allow
* file uploads
*
* @param \PKP\core\APIResponse $response object
*
* @return \PKP\core\APIResponse
*/
private function getResponse($response)
{
return $response->withHeader('Access-Control-Allow-Headers', 'Content-Type, X-Requested-With, X-PINGOTHER, X-File-Name, Cache-Control');
}
/**
* Upload a requested file
*
* @param \Slim\Http\Request $slimRequest Slim request object
* @param \PKP\core\APIResponse $response object
* @param array $args arguments
*
* @return \PKP\core\APIResponse
*/
public function uploadFile($slimRequest, $response, $args)
{
$request = $this->getRequest();
if (empty($_FILES) || empty($_FILES['file'])) {
return $response->withStatus(400)->withJsonError('api.files.400.noUpload');
}
$siteDir = Core::getBaseDir() . '/' . Config::getVar('files', 'public_files_dir') . '/site';
if (!file_exists($siteDir) || !is_writeable($siteDir)) {
return $response->withStatus(500)->withJsonError('api.publicFiles.500.badFilesDir');
}
$userDir = $siteDir . '/images/' . $request->getUser()->getUsername();
$isUserAllowed = true;
$allowedDirSize = Config::getVar('files', 'public_user_dir_size', 5000) * 1024;
$allowedFileTypes = ['gif', 'jpg', 'png', 'webp'];
Hook::call('API::uploadPublicFile::permissions', [
&$userDir,
&$isUserAllowed,
&$allowedDirSize,
&$allowedFileTypes,
$request,
$this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES),
]);
// Allow plugins to control who can upload files
if (!$isUserAllowed) {
return $response->withStatus(403)->withJsonError('api.publicFiles.403.unauthorized');
}
// Don't allow user to exceed the alotted space in their public directory
$currentSize = 0;
if ($allowedDirSize > 0 && file_exists($userDir)) {
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($userDir, FilesystemIterator::SKIP_DOTS)) as $object) {
$currentSize += $object->getSize();
}
}
if (($currentSize + $_FILES['file']['size']) > $allowedDirSize) {
return $response->withStatus(413)->withJsonError('api.publicFiles.413.noDirSpace', [
'fileUploadSize' => ceil($_FILES['file']['size'] / 1024),
'dirSizeLeft' => ceil(($allowedDirSize - $currentSize) / 1024),
]);
}
$fileManager = new FileManager();
$filename = $fileManager->getUploadedFileName('file');
$filename = trim(
preg_replace(
"/[^a-z0-9\.\-]+/",
'',
str_replace(
[' ', '_', ':'],
'-',
strtolower($filename)
)
)
);
$extension = pathinfo(strtolower(trim($filename)), PATHINFO_EXTENSION);
// Only allow permitted file types
if (!in_array($extension, $allowedFileTypes)) {
return $response->withStatus(400)->withJsonError('api.publicFiles.400.extensionNotSupported', [
'fileTypes' => join(__('common.commaListSeparator'), $allowedFileTypes)
]);
}
// Perform additional checks on images
if (in_array($extension, ['gif', 'jpg', 'jpeg', 'png', 'jpe'])) {
if (getimagesize($_FILES['file']['tmp_name']) === false) {
return $response->withStatus(400)->withJsonError('api.publicFiles.400.invalidImage');
}
$extensionFromMimeType = $fileManager->getImageExtension(PKPString::mime_content_type($_FILES['file']['tmp_name']));
if ($extensionFromMimeType !== '.' . $extension) {
return $response->withStatus(400)->withJsonError('api.publicFiles.400.mimeTypeNotMatched');
}
}
// Save the file
$destinationPath = $this->_getFilename($siteDir . '/images/' . $request->getUser()->getUsername() . '/' . $filename, $fileManager);
$success = $fileManager->uploadFile('file', $destinationPath);
if ($success === false) {
if ($fileManager->uploadError($filename)) {
switch ($fileManager->getUploadErrorCode($filename)) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
return $response->withStatus(400)->withJsonError('api.files.400.fileSize', ['maxSize' => Application::getReadableMaxFileSize()]);
case UPLOAD_ERR_PARTIAL:
return $response->withStatus(400)->withJsonError('api.files.400.uploadFailed');
case UPLOAD_ERR_NO_FILE:
return $response->withStatus(400)->withJsonError('api.files.400.noUpload');
case UPLOAD_ERR_NO_TMP_DIR:
case UPLOAD_ERR_CANT_WRITE:
case UPLOAD_ERR_EXTENSION:
return $response->withStatus(400)->withJsonError('api.files.400.config');
}
}
return $response->withStatus(400)->withJsonError('api.files.400.uploadFailed');
}
return $this->getResponse($response->withJson([
'url' => $request->getBaseUrl() . '/' .
Config::getVar('files', 'public_files_dir') . '/site/images/' .
$request->getUser()->getUsername() . '/' .
pathinfo($destinationPath, PATHINFO_BASENAME),
]));
}
/**
* Respond affirmatively to a HTTP OPTIONS request with headers which allow
* file uploads
*
* @param \Slim\Http\Request $slimRequest Slim request object
* @param \PKP\core\APIResponse $response object
* @param array $args arguments
*
* @return \PKP\core\APIResponse
*/
public function getOptions($slimRequest, $response, $args)
{
return $this->getResponse($response);
}
/**
* A recursive function to get a filename that will not overwrite an
* existing file
*
* @param string $path Preferred filename
* @param FileManager $fileManager
*
* @return string
*/
private function _getFilename($path, $fileManager)
{
if ($fileManager->fileExists($path)) {
$pathParts = pathinfo($path);
$filename = $pathParts['filename'] . '-' . md5(microtime()) . '.' . $pathParts['extension'];
if (strlen($filename > 255)) {
$filename = substr($filename, -255, 255);
}
return $this->_getFilename($pathParts['dirname'] . '/' . $filename, $fileManager);
}
return $path;
}
}
@@ -0,0 +1,403 @@
<?php
/**
* @file api/v1/announcements/PKPAnnouncementHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPAnnouncementHandler
*
* @ingroup api_v1_announcement
*
* @brief Handle API requests for announcement operations.
*
*/
namespace PKP\API\v1\announcements;
use APP\core\Application;
use APP\core\Request;
use APP\facades\Repo;
use Exception;
use Illuminate\Support\Facades\Bus;
use PKP\announcement\Collector;
use PKP\config\Config;
use PKP\context\Context;
use PKP\core\exceptions\StoreTemporaryFileException;
use PKP\db\DAORegistry;
use PKP\facades\Locale;
use PKP\handler\APIHandler;
use PKP\jobs\notifications\NewAnnouncementNotifyUsers;
use PKP\mail\Mailer;
use PKP\notification\NotificationSubscriptionSettingsDAO;
use PKP\notification\PKPNotification;
use PKP\plugins\Hook;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\services\PKPSchemaService;
class PKPAnnouncementHandler extends APIHandler
{
/** @var int The default number of announcements to return in one request */
public const DEFAULT_COUNT = 30;
/** @var int The maximum number of announcements to return in one request */
public const MAX_COUNT = 100;
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'announcements';
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/{announcementId:\d+}',
'handler' => [$this, 'get'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'add'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
'PUT' => [
[
'pattern' => $this->getEndpointPattern() . '/{announcementId:\d+}',
'handler' => [$this, 'edit'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
'DELETE' => [
[
'pattern' => $this->getEndpointPattern() . '/{announcementId:\d+}',
'handler' => [$this, 'delete'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
if (!Config::getVar('features', 'site_announcements') && !$request->getContext()) {
return false;
}
if (!$request->getContext()) {
$roleAssignments = $this->getSiteRoleAssignments($roleAssignments);
}
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a single submission
*
* @param \Slim\Http\Request $slimRequest Slim request object
* @param \PKP\core\APIResponse $response object
* @param array $args arguments
*
* @return \PKP\core\APIResponse
*/
public function get($slimRequest, $response, $args)
{
$announcement = Repo::announcement()->get((int) $args['announcementId']);
if (!$announcement) {
return $response->withStatus(404)->withJsonError('api.announcements.404.announcementNotFound');
}
// The assocId in announcements should always point to the contextId
if ($announcement->getData('assocId') !== $this->getRequest()->getContext()?->getId()) {
return $response->withStatus(404)->withJsonError('api.announcements.400.contextsNotMatched');
}
return $response->withJson(Repo::announcement()->getSchemaMap()->map($announcement), 200);
}
/**
* Get a collection of announcements
*
* @param \Slim\Http\Request $slimRequest Slim request object
* @param \PKP\core\APIResponse $response object
* @param array $args arguments
*
* @return \PKP\core\APIResponse
*/
public function getMany($slimRequest, $response, $args)
{
$collector = Repo::announcement()->getCollector()
->limit(self::DEFAULT_COUNT)
->offset(0);
foreach ($slimRequest->getQueryParams() as $param => $val) {
switch ($param) {
case 'typeIds':
$collector->filterByTypeIds(
array_map('intval', $this->paramToArray($val))
);
break;
case 'count':
$collector->limit(min((int) $val, self::MAX_COUNT));
break;
case 'offset':
$collector->offset((int) $val);
break;
case 'searchPhrase':
$collector->searchPhrase($val);
break;
}
}
if ($this->getRequest()->getContext()) {
$collector->filterByContextIds([$this->getRequest()->getContext()->getId()]);
} else {
$collector->withSiteAnnouncements(Collector::SITE_ONLY);
}
Hook::call('API::submissions::params', [$collector, $slimRequest]);
$announcements = $collector->getMany();
return $response->withJson([
'itemsMax' => $collector->limit(null)->offset(null)->getCount(),
'items' => Repo::announcement()->getSchemaMap()->summarizeMany($announcements)->values(),
], 200);
}
/**
* Add an announcement
*
* @param \Slim\Http\Request $slimRequest Slim request object
* @param \PKP\core\APIResponse $response object
* @param array $args arguments
*
* @return \PKP\core\APIResponse
*/
public function add($slimRequest, $response, $args)
{
$request = $this->getRequest();
$context = $request->getContext();
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_ANNOUNCEMENT, $slimRequest->getParsedBody());
$params['assocType'] = Application::get()->getContextAssocType();
$params['assocId'] = $context?->getId();
$primaryLocale = $context ? $context->getPrimaryLocale() : $request->getSite()->getPrimaryLocale();
$allowedLocales = $context ? $context->getSupportedFormLocales() : $request->getSite()->getSupportedLocales();
$errors = Repo::announcement()->validate(null, $params, $allowedLocales, $primaryLocale);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$announcement = Repo::announcement()->newDataObject($params);
try {
$announcementId = Repo::announcement()->add($announcement);
} catch (StoreTemporaryFileException $e) {
$announcementId = $e->dataObject->getId();
if ($announcementId) {
$announcement = Repo::announcement()->get($announcementId);
Repo::announcement()->delete($announcement);
}
return $response->withStatus(400)->withJson([
'image' => [__('api.400.errorUploadingImage')]
]);
}
$announcement = Repo::announcement()->get($announcementId);
$sendEmail = (bool) filter_var($params['sendEmail'], FILTER_VALIDATE_BOOLEAN);
if ($context) {
$this->notifyUsers($request, $context, $announcementId, $sendEmail);
}
return $response->withJson(Repo::announcement()->getSchemaMap()->map($announcement), 200);
}
/**
* Edit an announcement
*
* @param \Slim\Http\Request $slimRequest Slim request object
* @param \PKP\core\APIResponse $response object
* @param array $args arguments
*
* @return \PKP\core\APIResponse
*/
public function edit($slimRequest, $response, $args)
{
$request = $this->getRequest();
$context = $request->getContext();
$announcement = Repo::announcement()->get((int) $args['announcementId']);
if (!$announcement) {
return $response->withStatus(404)->withJsonError('api.announcements.404.announcementNotFound');
}
if ($announcement->getData('assocType') !== Application::get()->getContextAssocType()) {
throw new Exception('Announcement has an assocType that did not match the context.');
}
// Don't allow to edit an announcement from one context from a different context's endpoint
if ($context?->getId() !== $announcement->getData('assocId')) {
return $response->withStatus(403)->withJsonError('api.announcements.400.contextsNotMatched');
}
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_ANNOUNCEMENT, $slimRequest->getParsedBody());
$params['id'] = $announcement->getId();
$params['typeId'] ??= null;
$primaryLocale = $context ? $context->getPrimaryLocale() : $request->getSite()->getPrimaryLocale();
$allowedLocales = $context ? $context->getSupportedFormLocales() : $request->getSite()->getSupportedLocales();
$errors = Repo::announcement()->validate($announcement, $params, $allowedLocales, $primaryLocale);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
try {
Repo::announcement()->edit($announcement, $params);
} catch (StoreTemporaryFileException $e) {
Repo::announcement()->delete($announcement);
return $response->withStatus(400)->withJson([
'image' => __('api.400.errorUploadingImage')
]);
}
$announcement = Repo::announcement()->get($announcement->getId());
return $response->withJson(Repo::announcement()->getSchemaMap()->map($announcement), 200);
}
/**
* Delete an announcement
*
* @param \Slim\Http\Request $slimRequest Slim request object
* @param \PKP\core\APIResponse $response object
* @param array $args arguments
*
* @return \PKP\core\APIResponse
*/
public function delete($slimRequest, $response, $args)
{
$request = $this->getRequest();
$announcement = Repo::announcement()->get((int) $args['announcementId']);
if (!$announcement) {
return $response->withStatus(404)->withJsonError('api.announcements.404.announcementNotFound');
}
if ($announcement->getData('assocType') !== Application::get()->getContextAssocType()) {
throw new Exception('Announcement has an assocType that did not match the context.');
}
// Don't allow to delete an announcement from one context from a different context's endpoint
if ($request->getContext()?->getId() !== $announcement->getData('assocId')) {
return $response->withStatus(403)->withJsonError('api.announcements.400.contextsNotMatched');
}
$announcementProps = Repo::announcement()->getSchemaMap()->map($announcement);
Repo::announcement()->delete($announcement);
return $response->withJson($announcementProps, 200);
}
/**
* Modify the role assignments so that only
* site admins have access
*/
protected function getSiteRoleAssignments(array $roleAssignments): array
{
return array_filter($roleAssignments, fn($key) => $key == Role::ROLE_ID_SITE_ADMIN, ARRAY_FILTER_USE_KEY);
}
/**
* Notify subscribed users
*
* This only works for context-level announcements. There is no way to
* determine users who have subscribed to site-level announcements.
*
* @param bool $sendEmail Whether or not the editor chose to notify users by email
*/
protected function notifyUsers(Request $request, Context $context, int $announcementId, bool $sendEmail): void
{
/** @var NotificationSubscriptionSettingsDAO $notificationSubscriptionSettingsDao */
$notificationSubscriptionSettingsDao = DAORegistry::getDAO('NotificationSubscriptionSettingsDAO');
// Notify users
$userIdsToNotify = $notificationSubscriptionSettingsDao->getSubscribedUserIds(
[NotificationSubscriptionSettingsDAO::BLOCKED_NOTIFICATION_KEY],
[PKPNotification::NOTIFICATION_TYPE_NEW_ANNOUNCEMENT],
[$context->getId()]
);
if ($sendEmail) {
$userIdsToMail = $notificationSubscriptionSettingsDao->getSubscribedUserIds(
[NotificationSubscriptionSettingsDAO::BLOCKED_NOTIFICATION_KEY, NotificationSubscriptionSettingsDAO::BLOCKED_EMAIL_NOTIFICATION_KEY],
[PKPNotification::NOTIFICATION_TYPE_NEW_ANNOUNCEMENT],
[$context->getId()]
);
$userIdsToNotifyAndMail = $userIdsToNotify->intersect($userIdsToMail);
$userIdsToNotify = $userIdsToNotify->diff($userIdsToMail);
}
$sender = $request->getUser();
$jobs = [];
foreach ($userIdsToNotify->chunk(PKPNotification::NOTIFICATION_CHUNK_SIZE_LIMIT) as $notifyUserIds) {
$jobs[] = new NewAnnouncementNotifyUsers(
$notifyUserIds,
$context->getId(),
$announcementId,
Locale::getPrimaryLocale()
);
}
if (isset($userIdsToNotifyAndMail)) {
foreach ($userIdsToNotifyAndMail->chunk(Mailer::BULK_EMAIL_SIZE_LIMIT) as $notifyAndMailUserIds) {
$jobs[] = new NewAnnouncementNotifyUsers(
$notifyAndMailUserIds,
$context->getId(),
$announcementId,
Locale::getPrimaryLocale(),
$sender
);
}
}
Bus::batch($jobs)->dispatch();
}
}
@@ -0,0 +1,708 @@
<?php
/**
* @file api/v1/contexts/PKPContextHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPContextHandler
*
* @ingroup api_v1_context
*
* @brief Base class to handle API requests for contexts (journals/presses).
*/
namespace PKP\API\v1\contexts;
use APP\core\Application;
use APP\core\Services;
use APP\plugins\IDoiRegistrationAgency;
use APP\services\ContextService;
use APP\template\TemplateManager;
use PKP\context\Context;
use PKP\core\APIResponse;
use PKP\db\DAORegistry;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\plugins\Plugin;
use PKP\plugins\PluginRegistry;
use PKP\plugins\ThemePlugin;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\security\RoleDAO;
use PKP\services\interfaces\EntityWriteInterface;
use PKP\services\PKPSchemaService;
use Slim\Http\Request as SlimRequest;
use Slim\Http\Response as SlimResponse;
class PKPContextHandler extends APIHandler
{
/** @var string One of the SCHEMA_... constants */
public $schemaName = PKPSchemaService::SCHEMA_CONTEXT;
/**
* @copydoc APIHandler::__construct()
*/
public function __construct()
{
$this->_handlerPath = 'contexts';
$roles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER];
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/{contextId:\d+}',
'handler' => [$this, 'get'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/{contextId:\d+}/theme',
'handler' => [$this, 'getTheme'],
'roles' => $roles,
],
],
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'add'],
'roles' => [Role::ROLE_ID_SITE_ADMIN],
],
],
'PUT' => [
[
'pattern' => $this->getEndpointPattern() . '/{contextId:\d+}',
'handler' => [$this, 'edit'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/{contextId:\d+}/theme',
'handler' => [$this, 'editTheme'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/{contextId:\d+}/registrationAgency',
'handler' => [$this, 'editDoiRegistrationAgencyPlugin'],
'roles' => $roles,
]
],
'DELETE' => [
[
'pattern' => $this->getEndpointPattern() . '/{contextId:\d+}',
'handler' => [$this, 'delete'],
'roles' => [Role::ROLE_ID_SITE_ADMIN],
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a collection of contexts
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function getMany($slimRequest, $response, $args)
{
$request = $this->getRequest();
$defaultParams = [
'count' => 20,
'offset' => 0,
];
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = [];
// Process query params to format incoming data as needed
foreach ($requestParams as $param => $val) {
switch ($param) {
case 'isEnabled':
$allowedParams[$param] = (bool) $val;
break;
case 'searchPhrase':
$allowedParams[$param] = trim($val);
break;
case 'count':
$allowedParams[$param] = min(100, (int) $val);
break;
case 'offset':
$allowedParams[$param] = (int) $val;
break;
}
}
Hook::call('API::contexts::params', [&$allowedParams, $slimRequest]);
// Anyone not a site admin should not be able to access contexts that are
// not enabled
if (empty($allowedParams['isEnabled'])) {
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
$canAccessDisabledContexts = !empty(array_intersect([Role::ROLE_ID_SITE_ADMIN], $userRoles));
if (!$canAccessDisabledContexts) {
return $response->withStatus(403)->withJsonError('api.contexts.403.requestedDisabledContexts');
}
}
$items = [];
$contextsIterator = Services::get('context')->getMany($allowedParams);
$propertyArgs = [
'request' => $request,
'slimRequest' => $slimRequest,
];
foreach ($contextsIterator as $context) {
$items[] = Services::get('context')->getSummaryProperties($context, $propertyArgs);
}
$data = [
'itemsMax' => Services::get('context')->getMax($allowedParams),
'items' => $items,
];
return $response->withJson($data, 200);
}
/**
* Get a single context
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function get($slimRequest, $response, $args)
{
$request = $this->getRequest();
$user = $request->getUser();
$contextService = Services::get('context');
$context = $contextService->get((int) $args['contextId']);
if (!$context) {
return $response->withStatus(404)->withJsonError('api.contexts.404.contextNotFound');
}
// Don't allow to get one context from a different context's endpoint
if ($request->getContext() && $request->getContext()->getId() !== $context->getId()) {
return $response->withStatus(403)->withJsonError('api.contexts.403.contextsDidNotMatch');
}
// A disabled journal can only be access by site admins and users with a
// manager role in that journal
if (!$context->getEnabled()) {
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
if (!in_array(Role::ROLE_ID_SITE_ADMIN, $userRoles)) {
$roleDao = DAORegistry::getDao('RoleDAO'); /** @var RoleDAO $roleDao */
if (!$roleDao->userHasRole($context->getId(), $user->getId(), Role::ROLE_ID_MANAGER)) {
return $response->withStatus(403)->withJsonError('api.contexts.403.notAllowed');
}
}
}
$data = $contextService->getFullProperties($context, [
'request' => $request,
'slimRequest' => $slimRequest
]);
return $response->withJson($data, 200);
}
/**
* Get the theme and any theme options for a context
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function getTheme($slimRequest, $response, $args)
{
$request = $this->getRequest();
$user = $request->getUser();
$contextService = Services::get('context');
$context = $contextService->get((int) $args['contextId']);
if (!$context) {
return $response->withStatus(404)->withJsonError('api.contexts.404.contextNotFound');
}
// Don't allow to get one context from a different context's endpoint
if ($request->getContext() && $request->getContext()->getId() !== $context->getId()) {
return $response->withStatus(403)->withJsonError('api.contexts.403.contextsDidNotMatch');
}
// A disabled journal can only be access by site admins and users with a
// manager role in that journal
if (!$context->getEnabled()) {
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
if (!in_array(Role::ROLE_ID_SITE_ADMIN, $userRoles)) {
$roleDao = DAORegistry::getDao('RoleDAO'); /** @var RoleDAO $roleDao */
if (!$roleDao->userHasRole($context->getId(), $user->getId(), Role::ROLE_ID_MANAGER)) {
return $response->withStatus(403)->withJsonError('api.contexts.403.notAllowed');
}
}
}
$allThemes = PluginRegistry::loadCategory('themes', true);
$activeTheme = null;
foreach ($allThemes as $theme) {
if ($context->getData('themePluginPath') === $theme->getDirName()) {
$activeTheme = $theme;
break;
}
}
if (!$activeTheme) {
return $response->withStatus(404)->withJsonError('api.themes.404.themeUnavailable');
}
$data = array_merge(
$activeTheme->getOptionValues($context->getId()),
['themePluginPath' => $theme->getDirName()]
);
ksort($data);
return $response->withJson($data, 200);
}
/**
* Add a context
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function add($slimRequest, $response, $args)
{
$request = $this->getRequest();
// This endpoint is only available at the site-wide level
if ($request->getContext()) {
return $response->withStatus(404)->withJsonError('api.submissions.404.siteWideEndpoint');
}
$site = $request->getSite();
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_CONTEXT, $slimRequest->getParsedBody());
$primaryLocale = $site->getPrimaryLocale();
$allowedLocales = $site->getSupportedLocales();
// If the site only supports a single locale, set the context's locales
if (count($allowedLocales) === 1) {
if (!isset($params['primaryLocale'])) {
$params['primaryLocale'] = $primaryLocale;
}
if (!isset($params['supportedLocales'])) {
$params['supportedLocales'] = $allowedLocales;
}
}
$contextService = Services::get('context'); /** @var ContextService $contextService */
$errors = $contextService->validate(EntityWriteInterface::VALIDATE_ACTION_ADD, $params, $allowedLocales, $primaryLocale);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$context = Application::getContextDAO()->newDataObject();
$context->setAllData($params);
$context = $contextService->add($context, $request);
$contextProps = $contextService->getFullProperties($context, [
'request' => $request,
'slimRequest' => $slimRequest
]);
return $response->withJson($contextProps, 200);
}
/**
* Edit a context
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function edit($slimRequest, $response, $args)
{
$request = $this->getRequest();
$requestContext = $request->getContext();
$contextId = (int) $args['contextId'];
// Don't allow to get one context from a different context's endpoint
if ($request->getContext() && $request->getContext()->getId() !== $contextId) {
return $response->withStatus(403)->withJsonError('api.contexts.403.contextsDidNotMatch');
}
// Don't allow to edit the context from the site-wide API, because the
// context's plugins will not be enabled
if (!$request->getContext()) {
return $response->withStatus(403)->withJsonError('api.contexts.403.requiresContext');
}
$contextService = Services::get('context');
$context = $contextService->get($contextId);
if (!$context) {
return $response->withStatus(404)->withJsonError('api.contexts.404.contextNotFound');
}
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
if (!$requestContext && !in_array(Role::ROLE_ID_SITE_ADMIN, $userRoles)) {
return $response->withStatus(403)->withJsonError('api.contexts.403.notAllowedEdit');
}
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_CONTEXT, $slimRequest->getParsedBody());
$params['id'] = $contextId;
$site = $request->getSite();
$primaryLocale = $context->getPrimaryLocale();
$allowedLocales = $context->getSupportedFormLocales();
$errors = $contextService->validate(EntityWriteInterface::VALIDATE_ACTION_EDIT, $params, $allowedLocales, $primaryLocale);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$context = $contextService->edit($context, $params, $request);
$contextProps = $contextService->getFullProperties($context, [
'request' => $request,
'slimRequest' => $slimRequest
]);
return $response->withJson($contextProps, 200);
}
/**
* Edit a context's theme and theme options
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function editTheme($slimRequest, $response, $args)
{
$request = $this->getRequest();
$requestContext = $request->getContext();
$contextId = (int) $args['contextId'];
// Don't allow to get one context from a different context's endpoint
if ($request->getContext() && $request->getContext()->getId() !== $contextId) {
return $response->withStatus(403)->withJsonError('api.contexts.403.contextsDidNotMatch');
}
// Don't allow to edit the context from the site-wide API, because the
// context's plugins will not be enabled
if (!$requestContext) {
return $response->withStatus(403)->withJsonError('api.contexts.403.requiresContext');
}
$contextService = Services::get('context');
$context = $contextService->get($contextId);
if (!$context) {
return $response->withStatus(404)->withJsonError('api.contexts.404.contextNotFound');
}
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
$allowedRoles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER];
if (!array_intersect($allowedRoles, $userRoles)) {
return $response->withStatus(403)->withJsonError('api.contexts.403.notAllowedEdit');
}
$params = $slimRequest->getParsedBody();
// Validate the themePluginPath and allow themes to perform their own validation
$themePluginPath = empty($params['themePluginPath']) ? null : $params['themePluginPath'];
if ($themePluginPath !== $context->getData('themePluginPath')) {
$errors = $contextService->validate(
EntityWriteInterface::VALIDATE_ACTION_EDIT,
['themePluginPath' => $themePluginPath],
$context->getSupportedFormLocales(),
$context->getPrimaryLocale()
);
if (!empty($errors)) {
return $response->withJson($errors, 400);
}
$newContext = $contextService->edit($context, ['themePluginPath' => $themePluginPath], $request);
}
// Get the appropriate theme plugin
/** @var iterable<ThemePlugin> */
$allThemes = PluginRegistry::loadCategory('themes', true);
/** @var ?ThemePlugin */
$selectedTheme = null;
foreach ($allThemes as $theme) {
if ($themePluginPath === $theme->getDirName()) {
$selectedTheme = $theme;
break;
}
}
// Run the theme's init() method if a new theme has been selected
if (isset($newContext)) {
$selectedTheme->init();
}
$errors = $selectedTheme->validateOptions($params, $themePluginPath, $context->getId(), $request);
if (!empty($errors)) {
return $response->withJson($errors, 400);
}
// Only accept params that are defined in the theme options
$options = $selectedTheme->getOptionsConfig();
foreach ($options as $optionName => $optionConfig) {
if (!array_key_exists($optionName, $params)) {
continue;
}
$selectedTheme->saveOption($optionName, $params[$optionName], $context->getId());
}
// Clear the template cache so that new settings can take effect
$templateMgr = TemplateManager::getManager(Application::get()->getRequest());
$templateMgr->clearTemplateCache();
$templateMgr->clearCssCache();
$data = array_merge(
$selectedTheme->getOptionValues($context->getId()),
['themePluginPath' => $themePluginPath]
);
ksort($data);
return $response->withJson($data, 200);
}
/** @param APIResponse $response */
public function editDoiRegistrationAgencyPlugin(SlimRequest $slimRequest, SlimResponse $response, array $args): SlimResponse
{
$request = $this->getRequest();
$requestContext = $request->getContext();
$contextId = (int) $args['contextId'];
// Don't allow to get one context from a different context's endpoint
if ($request->getContext() && $request->getContext()->getId() !== $contextId) {
return $response->withStatus(403)->withJsonError('api.contexts.403.contextsDidNotMatch');
}
// Don't allow to edit the context from the site-wide API, because the
// context's plugins will not be enabled
if (!$requestContext) {
return $response->withStatus(403)->withJsonError('api.contexts.403.requiresContext');
}
/** @var ContextService $contextService */
$contextService = Services::get('context');
$context = $contextService->get($contextId);
if (!$context) {
return $response->withStatus(404)->withJsonError('api.contexts.404.contextNotFound');
}
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
if (!array_intersect([Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER], $userRoles)) {
return $response->withStatus(403)->withJsonError('api.contexts.403.notAllowedEdit');
}
/** @var PKPSchemaService $schemaService */
$schemaService = Services::get('schema');
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_CONTEXT, $slimRequest->getParsedBody());
$contextFullProps = array_flip($schemaService->getFullProps(PKPSchemaService::SCHEMA_CONTEXT));
$contextParams = array_intersect_key(
$params,
$contextFullProps,
);
// Validate the registrationAgency and automatic deposit fields
// and allow agencies to perform their own validation.
if (!empty($contextParams)) {
$errors = $contextService->validate(
ContextService::VALIDATE_ACTION_EDIT,
$contextParams,
$context->getSupportedFormLocales(),
$context->getPrimaryLocale(),
);
if (!empty($errors)) {
return $response->withJson($errors, 400);
}
$contextService->edit(
$context,
$contextParams,
$request
);
}
// Return if no registration agency enabled;
if ($contextParams[Context::SETTING_CONFIGURED_REGISTRATION_AGENCY] === null) {
return $response->withJson($contextParams, 200);
}
// Get the appropriate agency plugin
$plugins = PluginRegistry::loadCategory('generic', true);
$selectedPlugin = null;
foreach ($plugins as $plugin) {
if (
$contextParams[Context::SETTING_CONFIGURED_REGISTRATION_AGENCY] === $plugin->getName()
) {
$selectedPlugin = $plugin;
break;
}
}
// Check if it's a registration agency plugin
if (!$selectedPlugin instanceof IDoiRegistrationAgency) {
return $response->withStatus(400)->withJsonError('api.dois.400.invalidPluginType');
}
// If it's a new/different registration agency plugin, update the enabled DOI types based on
// allowed types per the registration agency plugin
if (
$context->getData(Context::SETTING_CONFIGURED_REGISTRATION_AGENCY) !== $contextParams[Context::SETTING_CONFIGURED_REGISTRATION_AGENCY] &&
$contextParams[Context::SETTING_CONFIGURED_REGISTRATION_AGENCY] !== null
) {
/** @var Context $newContext */
$newContext = $contextService->get($contextId);
$enabledPubObjectTypes = $newContext->getEnabledDoiTypes();
$allowedPubObjectTypes = $selectedPlugin->getAllowedDoiTypes();
$filteredPubObjectTypes = array_intersect($enabledPubObjectTypes, $allowedPubObjectTypes);
if ($filteredPubObjectTypes != $enabledPubObjectTypes) {
$contextService->edit(
$newContext,
[Context::SETTING_ENABLED_DOI_TYPES => $filteredPubObjectTypes],
$request
);
}
}
$settingsObject = $selectedPlugin->getSettingsObject();
$params = $this->convertStringsToSchema($settingsObject::class, $slimRequest->getParsedBody());
$pluginParams = array_intersect_key(
$params,
(array) $settingsObject->getSchema()->properties,
);
// Validate plugin settings
$errors = $settingsObject->validate($pluginParams);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$this->updateRegistrationAgencyPluginSettings(
$contextId,
$selectedPlugin,
$settingsObject::class,
$pluginParams,
);
return $response->withJson(
array_merge($contextParams, $pluginParams),
200,
);
}
/**
* Delete a context
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function delete($slimRequest, $response, $args)
{
// This endpoint is only available at the site-wide level
if ($this->getRequest()->getContext()) {
return $response->withStatus(404)->withJsonError('api.submissions.404.siteWideEndpoint');
}
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
if (!in_array(Role::ROLE_ID_SITE_ADMIN, $userRoles)) {
$response->withStatus(403)->withJsonError('api.contexts.403.notAllowedDelete');
}
$contextId = (int) $args['contextId'];
$contextService = Services::get('context');
$context = $contextService->get($contextId);
if (!$context) {
return $response->withStatus(404)->withJsonError('api.contexts.404.contextNotFound');
}
$contextProps = $contextService->getSummaryProperties($context, [
'request' => $this->getRequest(),
'slimRequest' => $slimRequest
]);
$contextService->delete($context);
return $response->withJson($contextProps, 200);
}
/**
* Updates a settings plugin according to a given schema. Used in lieu of a generic plugin settings management workflow.
*
* @param Plugin $plugin Currently configured registration agency plugin. Should also implement IDoiRegistrationAgency
* @param string $schemaName Name of RegistrationAgencySettings child class used as schema name
* @param array $props Plugin properties to update
*/
protected function updateRegistrationAgencyPluginSettings(int $contextId, Plugin $plugin, string $schemaName, array $props): void
{
/** @var PKPSchemaService $schemaService */
$schemaService = Services::get('schema');
$sanitizedProps = $schemaService->sanitize($schemaName, $props);
foreach ($sanitizedProps as $fieldName => $value) {
$plugin->updateSetting($contextId, $fieldName, $value);
}
}
}
+731
View File
@@ -0,0 +1,731 @@
<?php
/**
* @file api/v1/dois/PKPDoiHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPDoiHandler
*
* @ingroup api_v1_dois
*
* @brief Handle API requests for DOI operations.
*
*/
namespace PKP\API\v1\dois;
use APP\core\Application;
use APP\facades\Repo;
use APP\submission\Submission;
use PKP\context\Context;
use PKP\core\APIResponse;
use PKP\doi\Doi;
use PKP\doi\exceptions\DoiException;
use PKP\file\TemporaryFileManager;
use PKP\handler\APIHandler;
use PKP\jobs\doi\DepositSubmission;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\DoisEnabledPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\services\PKPSchemaService;
use Slim\Http\Request as SlimRequest;
use Slim\Http\Response;
class PKPDoiHandler extends APIHandler
{
/** @var int The default number of DOIs to return in one request */
public const DEFAULT_COUNT = 30;
/** @var int The maximum number of DOIs to return in one request */
public const MAX_COUNT = 100;
/** @var array Handlers that must be authorized to access a submission */
public $requiresSubmissionAccess = [];
/** @var array Handlers that must be authorized to write to a publication */
public $requiresPublicationWriteAccess = [];
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'dois';
$this->_endpoints = array_merge_recursive($this->_endpoints, [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/{doiId:\d+}',
'handler' => [$this, 'get'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/exports/{fileId:\d+}',
'handler' => [$this, 'getExportedFile'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
]
],
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'add'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/submissions/assignDois',
'handler' => [$this, 'assignSubmissionDois'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
'PUT' => [
[
'pattern' => $this->getEndpointPattern() . '/{doiId:\d+}',
'handler' => [$this, 'edit'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/submissions/export',
'handler' => [$this, 'exportSubmissions'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/submissions/deposit',
'handler' => [$this, 'depositSubmissions'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/submissions/markRegistered',
'handler' => [$this, 'markSubmissionsRegistered'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/submissions/markUnregistered',
'handler' => [$this, 'markSubmissionsUnregistered'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/submissions/markStale',
'handler' => [$this, 'markSubmissionsStale'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/depositAll',
'handler' => [$this, 'depositAllDois'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN]
],
],
'DELETE' => [
[
'pattern' => $this->getEndpointPattern() . '/{doiId:\d+}',
'handler' => [$this, 'delete'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
]);
parent::__construct();
}
/**
* @param \APP\core\Request $request
* @param array $args
* @param array $roleAssignments
*
* @return bool
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
// This endpoint is not available at the site-wide level
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
// DOIs must be enabled to access DOI API endpoints
$this->addPolicy(new DoisEnabledPolicy($request->getContext()));
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a single DOI
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
*/
public function get(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$doi = Repo::doi()->get((int) $args['doiId']);
if (!$doi) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound"');
}
// The contextId should always point to the requested contextId
if ($doi->getData('contextId') !== $this->getRequest()->getContext()->getId()) {
return $response->withStatus(403)->withJsonError('api.dois.403.contextsNotMatched');
}
return $response->withJson(Repo::doi()->getSchemaMap()->map($doi), 200);
}
/**
* Get a collection of DOIs
*/
public function getMany(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$collector = Repo::doi()->getCollector()
->limit(self::DEFAULT_COUNT)
->offset(0);
foreach ($slimRequest->getQueryParams() as $param => $val) {
switch ($param) {
case 'count':
$collector->limit(min((int) $val, self::MAX_COUNT));
break;
case 'offset':
$collector->offset((int) $val);
break;
case 'status':
$collector->filterByStatus(array_map('intval', $this->paramToArray($val)));
break;
}
}
$collector->filterByContextIds([$this->getRequest()->getContext()->getId()]);
Hook::call('API::dois::params', [$collector, $slimRequest]);
$dois = $collector->getMany();
return $response->withJson(
[
'itemsMax' => $collector->limit(null)->offset(0)->getCount(),
'items' => Repo::doi()->getSchemaMap()->summarizeMany($dois)->values(),
],
200
);
}
/**
* Add a DOI
*/
public function add(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$request = $this->getRequest();
$context = $request->getContext();
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_DOI, $slimRequest->getParsedBody());
$params['contextId'] = $context->getId();
$errors = Repo::doi()->validate(null, $params);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$doi = Repo::doi()->newDataObject($params);
$id = Repo::doi()->add($doi);
if ($id === null) {
return $response->withStatus(400)->withJsonError('api.dois.400.creationFailed');
}
$doi = Repo::doi()->get($id);
return $response->withJson(Repo::doi()->getSchemaMap()->map($doi), 200);
}
/**
* Edit a DOI.
*
* When a pub object type and id are provided as body parameters, the DOI should only be modified for that pub object.
* To prevent the DOI from being modified for other objects it may be assigned to, we must create a new DOI
* and assign it to the object instead of editing the old DOI.
*
* When a pub object type and id are NOT provided, this function will only edit the DOI with ID of `doiId`
* without any side effects.
*/
public function edit(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$doi = Repo::doi()->get((int) $args['doiId']);
if (!$doi) {
return $response->withStatus(404)->withJsonError('api.dois.404.doiNotFound');
}
// The contextId should always point to the requested contextId
if ($doi->getData('contextId') !== $this->getRequest()->getContext()->getId()) {
return $response->withStatus(403)->withJsonError('api.dois.403.editItemOutOfContext');
}
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_DOI, $slimRequest->getParsedBody());
$errors = Repo::doi()->validate($doi, $params);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$pubObjectType = $slimRequest->getParsedBodyParam('pubObjectType');
$pubObjectId = $slimRequest->getParsedBodyParam('pubObjectId');
// Default behaviour, only edits DOI
if (empty($pubObjectType) && empty($pubObjectId)) {
Repo::doi()->edit($doi, $params);
$doi = Repo::doi()->get($doi->getId());
return $response->withJson(Repo::doi()->getSchemaMap()->map($doi), 200);
}
$pubObjectHandler = $this->getPubObjectHandler($pubObjectType);
if (is_null($pubObjectHandler)) {
return $response->withStatus(403)->withJsonError('api.dois.403.pubTypeNotRecognized');
}
// Check pubObject for doiId
$pubObject = $this->getViaPubObjectHandler($pubObjectHandler, $pubObjectId);
if ($pubObject?->getData('doiId') != $doi->getId()) {
return $response->withStatus(404)->withJsonError('api.dois.404.pubObjectNotFound');
}
// Copy DOI object data
$newDoi = clone $doi;
$newDoi->unsetData('id');
$newDoi->setAllData(array_merge($newDoi->getAllData(), ['doi' => $params['doi']]));
$newDoiId = Repo::doi()->add($newDoi);
// Update pubObject with new DOI and remove elsewhere if no longer in use
$this->editViaPubObjectHandler($pubObjectHandler, $pubObject, $newDoiId);
if (!Repo::doi()->isAssigned($doi->getId(), $pubObjectType)) {
Repo::doi()->delete($doi);
}
return $response->withJson(Repo::doi()->getSchemaMap()->map($newDoi), 200);
}
/**
* Delete a DOI
*
* When a pub object type and id are provided as body parameters, the DOI should only be deleted for that object.
* To prevent the DOI from being removed for other objects it may be assigned to, we remove the doiId from the
* pubObject then check if it's in use anywhere else before removing the DOI object directly.
*/
public function delete(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$doi = Repo::doi()->get((int) $args['doiId']);
if (!$doi) {
return $response->withStatus(404)->withJsonError('api.dois.404.doiNotFound');
}
// The contextId should always point to the requested contextId
if ($doi->getData('contextId') !== $this->getRequest()->getContext()->getId()) {
return $response->withStatus(403)->withJsonError('api.dois.403.editItemOutOfContext');
}
$doiProps = Repo::doi()->getSchemaMap()->map($doi);
$pubObjectType = $slimRequest->getParsedBodyParam('pubObjectType');
$pubObjectId = $slimRequest->getParsedBodyParam('pubObjectId');
// Default behaviour, directly delete DOI
if (empty($pubObjectType) && empty($pubObjectId)) {
Repo::doi()->delete($doi);
return $response->withJson($doiProps, 200);
}
$pubObjectHandler = $this->getPubObjectHandler($pubObjectType);
if (is_null($pubObjectHandler)) {
return $response->withStatus(403)->withJsonError('api.dois.403.pubTypeNotRecognized');
}
// Check pubObject for doiId
$pubObject = $this->getViaPubObjectHandler($pubObjectHandler, $pubObjectId);
if ($pubObject?->getData('doiId') != $doi->getId()) {
return $response->withStatus(404)->withJsonError('api.dois.404.pubObjectNotFound');
}
// Remove reference to DOI from pubObject and remove DOI object if no longer in use elsewhere
$this->editViaPubObjectHandler($pubObjectHandler, $pubObject, null);
if (!Repo::doi()->isAssigned($doi->getId(), $pubObjectType)) {
Repo::doi()->delete($doi);
}
return $response->withJson($doiProps, 200);
}
/**
* Export XML for configured DOI registration agency
*/
public function exportSubmissions(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
// Retrieve and validate submissions
$requestIds = $slimRequest->getParsedBody()['ids'] ?? [];
if (!count($requestIds)) {
return $response->withStatus(404)->withJsonError('api.dois.404.noPubObjectIncluded');
}
$context = $this->getRequest()->getContext();
$validIds = Repo::submission()
->getCollector()
->filterByContextIds([$context->getId()])
->filterByStatus([Submission::STATUS_PUBLISHED])
->getIds()
->toArray();
$invalidIds = array_diff($requestIds, $validIds);
if (count($invalidIds)) {
return $response->withStatus(400)->withJsonError('api.dois.400.invalidPubObjectIncluded');
}
/** @var Submission[] $submissions */
$submissions = [];
foreach ($requestIds as $id) {
$submissions[] = Repo::submission()->get($id);
}
if (empty($submissions[0])) {
return $response->withStatus(404)->withJsonError('api.dois.404.doiNotFound');
}
$agency = $context->getConfiguredDoiAgency();
if ($agency === null) {
return $response->withStatus(400)->withJsonError('api.dois.400.noRegistrationAgencyConfigured');
}
// Invoke IDoiRegistrationAgency::exportSubmissions
$responseData = $agency->exportSubmissions($submissions, $context);
if (!empty($responseData['xmlErrors'])) {
return $response->withStatus(400)->withJsonError('api.dois.400.xmlExportFailed');
}
return $response->withJson(['temporaryFileId' => $responseData['temporaryFileId']], 200);
}
/**
* Deposit XML for configured DOI registration agency
*/
public function depositSubmissions(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
// Retrieve and validate the submissions
$requestIds = $slimRequest->getParsedBody()['ids'] ?? [];
if (!count($requestIds)) {
return $response->withStatus(404)->withJsonError('api.dois.404.noPubObjectIncluded');
}
/** @var Context $context */
$context = $this->getRequest()->getContext();
$validIds = Repo::submission()
->getCollector()
->filterByContextIds([$context->getId()])
->filterByStatus([Submission::STATUS_PUBLISHED])
->getIds()
->toArray();
$invalidIds = array_diff($requestIds, $validIds);
if (count($invalidIds)) {
return $response->withStatus(400)->withJsonError('api.dois.400.invalidPubObjectIncluded');
}
$agency = $context->getConfiguredDoiAgency();
if ($agency === null) {
return $response->withStatus(400)->withJsonError('api.dois.400.noRegistrationAgencyConfigured');
}
$doiIdsToUpdate = [];
foreach ($requestIds as $submissionId) {
dispatch(new DepositSubmission($submissionId, $context, $agency));
$doiIdsToUpdate = array_merge($doiIdsToUpdate, Repo::doi()->getDoisForSubmission($submissionId));
}
Repo::doi()->markSubmitted($doiIdsToUpdate);
return $response->withStatus(200);
}
/**
* Mark submission DOIs as registered with a DOI registration agency.
*/
public function markSubmissionsRegistered(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
// Retrieve submissions
$requestIds = $slimRequest->getParsedBody()['ids'] ?? [];
if (!count($requestIds)) {
return $response->withStatus(404)->withJsonError('api.dois.404.noPubObjectIncluded');
}
$context = $this->getRequest()->getContext();
$validIds = Repo::submission()
->getCollector()
->filterByContextIds([$context->getId()])
->filterByStatus([Submission::STATUS_PUBLISHED])
->getIds()
->toArray();
$invalidIds = array_diff($requestIds, $validIds);
if (count($invalidIds)) {
$failedDoiActions = array_map(function (int $id) {
$submissionTitle = Repo::submission()->get($id)?->getCurrentPublication()->getLocalizedFullTitle() ?? '[' . __('api.dois.404.submissionNotFound') . ']';
return new DoiException(DoiException::SUBMISSION_NOT_PUBLISHED, $submissionTitle, $submissionTitle);
}, $invalidIds);
return $response->withJson(
[
'failedDoiActions' => array_map(
function (DoiException $item) {
return $item->getMessage();
},
$failedDoiActions
)
],
400
);
}
foreach ($requestIds as $id) {
$doiIds = Repo::doi()->getDoisForSubmission($id);
foreach ($doiIds as $doiId) {
Repo::doi()->markRegistered($doiId);
}
}
return $response->withStatus(200);
}
public function depositAllDois(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$context = $this->getRequest()->getContext();
Repo::doi()->depositAll($context);
return $response->withStatus(200);
}
/**
* Mark submission DOIs as no longer registered with a DOI registration agency.
*/
public function markSubmissionsUnregistered(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
// Retrieve submissions
$requestIds = $slimRequest->getParsedBody()['ids'] ?? [];
if (!count($requestIds)) {
return $response->withStatus(404)->withJsonError('api.dois.404.noPubObjectIncluded');
}
$context = $this->getRequest()->getContext();
$validIds = Repo::submission()
->getCollector()
->filterByContextIds([$context->getId()])
->getIds()
->toArray();
$invalidIds = array_diff($requestIds, $validIds);
if (count($invalidIds)) {
$failedDoiActions = array_map(function (int $id) {
return new DoiException(DoiException::INCORRECT_SUBMISSION_CONTEXT, $id, $id);
}, $invalidIds);
return $response->withJson(
[
'failedDoiActions' => array_map(
function (DoiException $item) {
return $item->getMessage();
},
$failedDoiActions
)
],
400
);
}
foreach ($requestIds as $id) {
$doiIds = Repo::doi()->getDoisForSubmission($id);
foreach ($doiIds as $doiId) {
Repo::doi()->markUnregistered($doiId);
}
}
return $response->withStatus(200);
}
/**
* Mark submission DOIs as stale, indicating a need to be resubmitted to registration agency with updated metadata.
*/
public function markSubmissionsStale(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
// Retrieve submissions
$requestIds = $slimRequest->getParsedBody()['ids'] ?? [];
if (!count($requestIds)) {
return $response->withStatus(404)->withJsonError('api.dois.404.noPubObjectIncluded');
}
$context = $this->getRequest()->getContext();
$validIds = Repo::submission()
->getCollector()
->filterByContextIds([$context->getId()])
->filterByStatus([Submission::STATUS_PUBLISHED])
// Items can only be considered stale if they have been deposited/queued for deposit in the first place
->filterByDoiStatuses([Doi::STATUS_SUBMITTED, Doi::STATUS_REGISTERED])
->getIds()
->toArray();
$invalidIds = array_diff($requestIds, $validIds);
if (count($invalidIds)) {
$failedDoiActions = array_map(function (int $id) {
$submissionTitle = Repo::submission()->get($id)?->getCurrentPublication()->getLocalizedFullTitle() ?? '[' . __('api.dois.404.submissionNotFound') . ']';
return new DoiException(DoiException::INCORRECT_STALE_STATUS, $submissionTitle, $submissionTitle);
}, $invalidIds);
return $response->withJson(
[
'failedDoiActions' => array_map(
function (DoiException $item) {
return $item->getMessage();
},
$failedDoiActions
)
],
400
);
}
foreach ($requestIds as $id) {
$doiIds = Repo::doi()->getDoisForSubmission($id);
Repo::doi()->markStale($doiIds);
}
return $response->withStatus(200);
}
/**
* Assign DOIs to submissions
*/
public function assignSubmissionDois(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
// Retrieve submissions
$requestIds = $slimRequest->getParsedBody()['ids'] ?? [];
if ($requestIds == null) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$context = $this->getRequest()->getContext();
$doiPrefix = $context->getData(Context::SETTING_DOI_PREFIX);
if (empty($doiPrefix)) {
return $response->withStatus(403)->withJsonError('api.dois.403.prefixRequired');
}
$failedDoiActions = [];
// Assign DOIs
foreach ($requestIds as $id) {
$submission = Repo::submission()->get($id);
if ($submission !== null) {
if ($submission->getData('contextId') !== $context->getId()) {
$creationFailureResults = [
new DoiException(
DoiException::INCORRECT_SUBMISSION_CONTEXT,
$id,
$id
)
];
} else {
$creationFailureResults = Repo::submission()->createDois($submission);
}
$failedDoiActions = array_merge($failedDoiActions, $creationFailureResults);
}
}
if (!empty($failedDoiActions)) {
return $response->withJson(
[
'failedDoiActions' => array_map(
function (DoiException $item) {
return $item->getMessage();
},
$failedDoiActions
)
],
400
);
}
return $response->withJson(['failedDoiActions' => $failedDoiActions], 200);
}
/**
* Download exported DOI XML from temporary file ID
*/
public function getExportedFile(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$fileId = $args['fileId'];
$currentUser = Application::get()->getRequest()->getUser();
$tempFileManager = new TemporaryFileManager();
$isSuccess = $tempFileManager->downloadById($fileId, $currentUser->getId());
if (!$isSuccess) {
return $response->withStatus(403)->withJsonError('api.403.unauthorized');
}
return $response->withStatus(200);
}
/**
* Gets a "handler" (either a repo or DAO) for a pub object to perform DOI-related operations.
* See PKPDoiHandler::edit() and PKPDoiHandler::delete().
*
* @param string $type One of Repo::doi()::TYPE_*
*
* @return mixed Returns either a repo or, for pub objects without repos, a DAO
*/
protected function getPubObjectHandler(string $type): mixed
{
return match ($type) {
Repo::doi()::TYPE_PUBLICATION => Repo::publication(),
Repo::doi()::TYPE_REPRESENTATION => Repo::galley(),
default => null,
};
}
/**
* Retrieve the pub object with the given ID.
*
* @param mixed $pubObjectHandler Either a repo or DAO for the pub object type
*
* @return mixed The actual pub object
*/
protected function getViaPubObjectHandler(mixed $pubObjectHandler, int $pubObjectId): mixed
{
return $pubObjectHandler->get($pubObjectId);
}
/**
* Edit the DOI ID for the given pub object via the "handler" (repo or DAO).
*
* @param mixed $pubObjectHandler Either a repo or DAO for the pub object type
* @param mixed $pubObject The pub object th edit
*/
protected function editViaPubObjectHandler(mixed $pubObjectHandler, mixed $pubObject, ?int $doiId): void
{
$pubObjectHandler->edit($pubObject, ['doiId' => $doiId]);
}
}
@@ -0,0 +1,295 @@
<?php
/**
* @file api/v1/emailTemplates/PKPEmailTemplateHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPEmailTemplateHandler
*
* @ingroup api_v1_email_templates
*
* @brief Base class to handle API requests for email templates.
*/
namespace PKP\API\v1\emailTemplates;
use APP\core\Application;
use PKP\core\APIResponse;
use PKP\facades\Repo;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextRequiredPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\services\PKPSchemaService;
use Slim\Http\Request as SlimRequest;
use Slim\Http\Response;
class PKPEmailTemplateHandler extends APIHandler
{
public const MAX_PER_PAGE = 100;
/**
* @copydoc APIHandler::__construct()
*/
public function __construct()
{
$this->_handlerPath = 'emailTemplates';
$roles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER];
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => array_merge($roles, [Role::ROLE_ID_SUB_EDITOR, ROLE::ROLE_ID_ASSISTANT]),
],
[
'pattern' => $this->getEndpointPattern() . '/{key}',
'handler' => [$this, 'get'],
'roles' => array_merge($roles, [Role::ROLE_ID_SUB_EDITOR, ROLE::ROLE_ID_ASSISTANT]),
],
],
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'add'],
'roles' => $roles,
],
],
'PUT' => [
[
'pattern' => $this->getEndpointPattern() . '/{key}',
'handler' => [$this, 'edit'],
'roles' => $roles,
],
],
'DELETE' => [
[
'pattern' => $this->getEndpointPattern() . '/restoreDefaults',
'handler' => [$this, 'restoreDefaults'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/{key}',
'handler' => [$this, 'delete'],
'roles' => $roles,
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
// This endpoint is not available at the site-wide level
$this->addPolicy(new ContextRequiredPolicy($request));
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a collection of email templates
*/
public function getMany(SlimRequest $slimRequest, Response $response, array $args): Response
{
$request = $this->getRequest();
$collector = Repo::emailTemplate()->getCollector($request->getContext()->getId());
// Process query params to format incoming data as needed
foreach ($slimRequest->getQueryParams() as $param => $val) {
switch ($param) {
case 'alternateTo':
$collector->alternateTo($this->paramToArray($val));
break;
case 'isModified':
$collector->isModified((bool) $val);
break;
case 'searchPhrase':
$collector->searchPhrase(trim($val));
break;
case 'count':
$collector->limit(min((int) $val, self::MAX_PER_PAGE));
break;
case 'offset':
$collector->offset((int) $val);
break;
}
}
Hook::call('API::emailTemplates::params', [$collector, $slimRequest]);
$emailTemplates = $collector->getMany();
return $response->withJson([
'itemsMax' => $collector->limit(null)->offset(null)->getCount(),
'items' => Repo::emailTemplate()->getSchemaMap()->summarizeMany($emailTemplates),
], 200);
}
/**
* Get a single email template
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function get($slimRequest, $response, $args)
{
$request = $this->getRequest();
$emailTemplate = Repo::emailTemplate()->getByKey($request->getContext()->getId(), $args['key']);
if (!$emailTemplate) {
return $response->withStatus(404)->withJsonError('api.emailTemplates.404.templateNotFound');
}
return $response->withJson(Repo::emailTemplate()->getSchemaMap()->map($emailTemplate), 200);
}
/**
* Add an email template
*
* @param SlimRequest $slimRequest Slim request object
* @param Response $response object
* @param array $args arguments
*
* @return Response
*/
public function add($slimRequest, $response, $args)
{
$request = $this->getRequest();
$requestContext = $request->getContext();
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_EMAIL_TEMPLATE, $slimRequest->getParsedBody());
$params['contextId'] = $requestContext->getId();
$errors = Repo::emailTemplate()->validate(null, $params, $requestContext);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$emailTemplate = Repo::emailTemplate()->newDataObject($params);
Repo::emailTemplate()->add($emailTemplate);
$emailTemplate = Repo::emailTemplate()->getByKey($emailTemplate->getData('contextId'), $emailTemplate->getData('key'));
return $response->withJson(Repo::emailTemplate()->getSchemaMap()->map($emailTemplate), 200);
}
/**
* Edit an email template
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function edit($slimRequest, $response, $args)
{
$request = $this->getRequest();
$requestContext = $request->getContext();
$emailTemplate = Repo::emailTemplate()->getByKey($requestContext->getId(), $args['key']);
if (!$emailTemplate) {
return $response->withStatus(404)->withJsonError('api.emailTemplates.404.templateNotFound');
}
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_EMAIL_TEMPLATE, $slimRequest->getParsedBody());
$params['key'] = $args['key'];
// Only allow admins to change the context an email template is attached to.
// Set the contextId if it has not been passed or the user is not an admin
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
if (isset($params['contextId'])
&& !in_array(Role::ROLE_ID_SITE_ADMIN, $userRoles)
&& $params['contextId'] !== $requestContext->getId()) {
return $response->withStatus(403)->withJsonError('api.emailTemplates.403.notAllowedChangeContext');
} elseif (!isset($params['contextId'])) {
$params['contextId'] = $requestContext->getId();
}
$errors = Repo::emailTemplate()->validate(
$emailTemplate,
$params,
$requestContext
);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
Repo::emailTemplate()->edit($emailTemplate, $params);
$emailTemplate = Repo::emailTemplate()->getByKey(
// context ID is null if edited for the first time
$emailTemplate->getData('contextId') ?? $params['contextId'],
$emailTemplate->getData('key')
);
return $response->withJson(Repo::emailTemplate()->getSchemaMap()->map($emailTemplate), 200);
}
/**
* Delete an email template
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function delete($slimRequest, $response, $args)
{
$request = $this->getRequest();
$requestContext = $request->getContext();
$emailTemplate = Repo::emailTemplate()->getByKey($requestContext->getId(), $args['key']);
// Only custom email templates can be deleted, so return 404 if no id exists
if (!$emailTemplate || !$emailTemplate->getData('id')) {
return $response->withStatus(404)->withJsonError('api.emailTemplates.404.templateNotFound');
}
$props = Repo::emailTemplate()->getSchemaMap()->map($emailTemplate);
Repo::emailTemplate()->delete($emailTemplate);
return $response->withJson($props, 200);
}
/**
* Restore defaults in the email template settings
*
* @param SlimRequest $slimRequest Slim request object
* @param Response $response object
* @param array $args arguments
*
* @return Response
*/
public function restoreDefaults($slimRequest, $response, $args)
{
$contextId = $this->getRequest()->getContext()->getId();
$deletedKeys = Repo::emailTemplate()->restoreDefaults($contextId);
return $response->withJson($deletedKeys, 200);
}
}
@@ -0,0 +1,302 @@
<?php
/**
* @file api/v1/highlights/HighlightsHandler.php
*
* Copyright (c) 2014-2023 Simon Fraser University
* Copyright (c) 2003-2023 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class HighlightsHandler
*
* @ingroup api_v1_highlights
*
* @brief Handle API requests for highlights.
*
*/
namespace PKP\API\v1\highlights;
use APP\facades\Repo;
use Exception;
use PKP\core\APIResponse;
use PKP\core\exceptions\StoreTemporaryFileException;
use PKP\handler\APIHandler;
use PKP\highlight\Collector;
use PKP\plugins\Hook;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\services\PKPSchemaService;
use Slim\Http\Request as SlimRequest;
class HighlightsHandler extends APIHandler
{
/** @var int The maximum number of highlights to return in one request */
public const MAX_COUNT = 100;
public function __construct()
{
$this->_handlerPath = 'highlights';
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/{highlightId:\d+}',
'handler' => [$this, 'get'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'add'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
'PUT' => [
[
'pattern' => $this->getEndpointPattern() . '/{highlightId:\d+}',
'handler' => [$this, 'edit'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/order',
'handler' => [$this, 'order'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
'DELETE' => [
[
'pattern' => $this->getEndpointPattern() . '/{highlightId:\d+}',
'handler' => [$this, 'delete'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
if (!$request->getContext()) {
$roleAssignments = $this->getSiteRoleAssignments($roleAssignments);
}
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a single highlight
*/
public function get(SlimRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$highlight = Repo::highlight()->get((int) $args['highlightId'], $this->getRequest()->getContext());
if (!$highlight) {
return $response->withStatus(404)->withJsonError('api.highlights.404.highlightNotFound');
}
return $response->withJson(
Repo::highlight()
->getSchemaMap()
->map($highlight)
, 200
);
}
/**
* Get a collection of highlights
*/
public function getMany(SlimRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$collector = Repo::highlight()->getCollector()
->limit(self::MAX_COUNT)
->offset(0);
if ($this->getRequest()->getContext()) {
$collector->filterByContextIds([$this->getRequest()->getContext()->getId()]);
} else {
$collector->withSiteHighlights(Collector::SITE_ONLY);
}
Hook::run('API::highlights::params', [$collector, $slimRequest]);
$highlights = $collector->getMany();
return $response->withJson([
'itemsMax' => $collector->limit(null)->offset(null)->getCount(),
'items' => Repo::highlight()->getSchemaMap()->summarizeMany($highlights)->values(),
], 200);
}
/**
* Add a highlight
*/
public function add(SlimRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$context = $this->getRequest()->getContext();
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_HIGHLIGHT, $slimRequest->getParsedBody());
$params['contextId'] = $context?->getId();
if (!$params['sequence']) {
$params['sequence'] = Repo::highlight()->getNextSequence($context?->getId());
}
$errors = Repo::highlight()->validate(null, $params, $context);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$highlight = Repo::highlight()->newDataObject($params);
try {
$highlightId = Repo::highlight()->add($highlight);
} catch (StoreTemporaryFileException $e) {
$highlight = Repo::highlight()->get($highlightId, $context?->getId());
Repo::highlight()->delete($highlight);
return $response->withStatus(400)->withJson([
'image' => __('api.400.errorUploadingImage')
]);
}
$highlight = Repo::highlight()->get($highlightId, $context?->getId());
return $response->withJson(Repo::highlight()->getSchemaMap()->map($highlight), 200);
}
/**
* Edit a highlight
*/
public function edit(SlimRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$context = $this->getRequest()->getContext();
$highlight = Repo::highlight()->get((int) $args['highlightId'], $context?->getId());
if (!$highlight) {
return $response->withStatus(404)->withJsonError('api.highlights.404.highlightNotFound');
}
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_HIGHLIGHT, $slimRequest->getParsedBody());
$params['id'] = $highlight->getId();
// Not allowed to change the context of a highlight through the API
unset($params['contextId']);
$errors = Repo::highlight()->validate($highlight, $params, $context);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
try {
Repo::highlight()->edit($highlight, $params);
} catch (Exception $e) {
Repo::highlight()->delete($highlight);
return $response->withStatus(400)->withJson([
'image' => __('api.highlights.400.errorUploadingImage')
]);
}
$highlight = Repo::highlight()->get($highlight->getId(), $context?->getId());
return $response->withJson(Repo::highlight()->getSchemaMap()->map($highlight), 200);
}
/**
* Order the highlights
*/
public function order(SlimRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$context = $this->getRequest()->getContext();
$params = $slimRequest->getParsedBody();
$sequence = (array) $params['sequence'];
if (empty($sequence)) {
return $response->withStatus(400)->withJson(['sequence' => __('api.highlights.400.noOrderData')]);
}
$highlights = array_map(
function($item) use ($context) {
return isset($item['id']) && isset($item['sequence'])
? Repo::highlight()->get($item['id'], $context?->getId())
: null;
},
$sequence
);
if (in_array(null, $highlights)) {
return $response->withStatus(400)->withJson(['sequence' => __('api.highlights.400.orderHighlightNotFound')]);
}
foreach ($highlights as $index => $highlight) {
Repo::highlight()->edit($highlight, ['sequence' => $sequence[$index]['sequence']]);
}
$collector = Repo::highlight()
->getCollector()
->limit(self::MAX_COUNT);
if ($context) {
$collector->filterByContextIds([$context->getId()]);
} else {
$collector->withSiteHighlights(Collector::SITE_ONLY);
}
$highlights = $collector->getMany();
return $response->withJson([
'items' => Repo::highlight()->getSchemaMap()->summarizeMany($highlights)->values(),
'itemsMax' => $highlights->count(),
], 200);
}
/**
* Delete a highlight
*/
public function delete(SlimRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$context = $this->getRequest()->getContext();
$highlight = Repo::highlight()->get((int) $args['highlightId'], $context?->getId());
if (!$highlight) {
return $response->withStatus(404)->withJsonError('api.highlights.404.highlightNotFound');
}
$highlightProps = Repo::highlight()->getSchemaMap()->map($highlight);
Repo::highlight()->delete($highlight);
return $response->withJson($highlightProps, 200);
}
/**
* Modify the role assignments so that only
* site admins have access
*/
protected function getSiteRoleAssignments(array $roleAssignments): array
{
return array_filter($roleAssignments, fn($key) => $key == Role::ROLE_ID_SITE_ADMIN, ARRAY_FILTER_USE_KEY);
}
}
@@ -0,0 +1,235 @@
<?php
/**
* @file api/v1/institutions/PKPInstitutionHandler.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPInstitutionHandler
*
* @ingroup api_v1_institutions
*
* @brief Handle API requests for institution operations.
*
*/
namespace PKP\API\v1\institutions;
use APP\facades\Repo;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextRequiredPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\services\PKPSchemaService;
use Slim\Http\Request as SlimHttpRequest;
class PKPInstitutionHandler extends APIHandler
{
/** @var int The default number of institutions to return in one request */
public const DEFAULT_COUNT = 30;
/** @var int The maximum number of institutions to return in one request */
public const MAX_COUNT = 100;
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'institutions';
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => [Role::ROLE_ID_MANAGER],
],
[
'pattern' => $this->getEndpointPattern() . '/{institutionId:\d+}',
'handler' => [$this, 'get'],
'roles' => [Role::ROLE_ID_MANAGER],
],
],
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'add'],
'roles' => [Role::ROLE_ID_MANAGER],
],
],
'PUT' => [
[
'pattern' => $this->getEndpointPattern() . '/{institutionId:\d+}',
'handler' => [$this, 'edit'],
'roles' => [Role::ROLE_ID_MANAGER],
],
],
'DELETE' => [
[
'pattern' => $this->getEndpointPattern() . '/{institutionId:\d+}',
'handler' => [$this, 'delete'],
'roles' => [Role::ROLE_ID_MANAGER],
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
$this->addPolicy(new ContextRequiredPolicy($request));
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a single institution
*/
public function get(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
if (!Repo::institution()->exists((int) $args['institutionId'], $this->getRequest()->getContext()->getId())) {
return $response->withStatus(404)->withJsonError('api.institutions.404.institutionNotFound');
}
$institution = Repo::institution()->get((int) $args['institutionId']);
return $response->withJson(Repo::institution()->getSchemaMap()->map($institution), 200);
}
/**
* Get a collection of institutions
*/
public function getMany(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$collector = Repo::institution()->getCollector()
->limit(self::DEFAULT_COUNT)
->offset(0);
foreach ($slimRequest->getQueryParams() as $param => $val) {
switch ($param) {
case 'count':
$collector->limit(min((int) $val, self::MAX_COUNT));
break;
case 'offset':
$collector->offset((int) $val);
break;
case 'searchPhrase':
$collector->searchPhrase($val);
break;
}
}
$collector->filterByContextIds([$this->getRequest()->getContext()->getId()]);
Hook::call('API::institutions::params', [$collector, $slimRequest]);
$institutions = $collector->getMany();
return $response->withJson([
'itemsMax' => $collector->limit(null)->offset(null)->getCount(),
'items' => Repo::institution()->getSchemaMap()->summarizeMany($institutions->values())->values(),
], 200);
}
/**
* Add an institution
*
* @throws \Exception For sending a request to the API endpoint of a particular context.
*/
public function add(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$request = $this->getRequest();
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_INSTITUTION, $slimRequest->getParsedBody());
$params['contextId'] = $request->getContext()->getId();
// Convert IP ranges string to array
if (!empty($params['ipRanges'])) {
$params['ipRanges'] = $this->convertIpToArray($params['ipRanges']);
}
$primaryLocale = $request->getContext()->getPrimaryLocale();
$allowedLocales = $request->getContext()->getSupportedFormLocales();
$errors = Repo::institution()->validate(null, $params, $allowedLocales, $primaryLocale);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$institution = Repo::institution()->newDataObject($params);
$id = Repo::institution()->add($institution);
$institution = Repo::institution()->get($id);
return $response->withJson(Repo::institution()->getSchemaMap()->map($institution), 200);
}
/**
* Edit an institution
*/
public function edit(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$request = $this->getRequest();
$context = $request->getContext();
if (!Repo::institution()->exists((int) $args['institutionId'], $context->getId())) {
return $response->withStatus(404)->withJsonError('api.institutions.404.institutionNotFound');
}
$institution = Repo::institution()->get((int) $args['institutionId']);
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_INSTITUTION, $slimRequest->getParsedBody());
$params['id'] = $institution->getId();
$params['contextId'] = $context->getId();
// Convert IP ranges string to array
if (!empty($params['ipRanges'])) {
$params['ipRanges'] = $this->convertIpToArray($params['ipRanges']);
}
$primaryLocale = $context->getPrimaryLocale();
$allowedLocales = $context->getSupportedFormLocales();
$errors = Repo::institution()->validate($institution, $params, $allowedLocales, $primaryLocale);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
Repo::institution()->edit($institution, $params);
$institution = Repo::institution()->get($institution->getId());
return $response->withJson(Repo::institution()->getSchemaMap()->map($institution), 200);
}
/**
* Delete an institution
*/
public function delete(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
if (!Repo::institution()->exists((int) $args['institutionId'], $this->getRequest()->getContext()->getId())) {
return $response->withStatus(404)->withJsonError('api.institutions.404.institutionNotFound');
}
$institution = Repo::institution()->get((int) $args['institutionId']);
$institutionProps = Repo::institution()->getSchemaMap()->map($institution);
Repo::institution()->delete($institution);
return $response->withJson($institutionProps, 200);
}
/**
* Convert IP ranges string to array
*/
protected function convertIpToArray(string $ipString): array
{
return array_map('trim', explode(PHP_EOL, trim($ipString)));
}
}
+204
View File
@@ -0,0 +1,204 @@
<?php
/**
* @file api/v1/jobs/PKPJobHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPJobHandler
*
* @ingroup api_v1_jobs
*
* @brief Handle API requests for jobs
*
*/
namespace PKP\API\v1\jobs;
use APP\facades\Repo;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use Slim\Http\Request as SlimRequest;
use Slim\Http\Response;
class PKPJobHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_apiForAdmin = true;
$this->_handlerPath = 'jobs';
$roles = [Role::ROLE_ID_SITE_ADMIN];
$this->_endpoints = array_merge_recursive($this->_endpoints, [
'GET' => [
[
'pattern' => $this->getEndpointPattern() . '/all',
'handler' => [$this, 'getJobs'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/failed/all',
'handler' => [$this, 'getFailedJobs'],
'roles' => $roles,
],
],
'POST' => [
[
'pattern' => $this->getEndpointPattern() . '/redispatch/{jobId:\d+}',
'handler' => [$this, 'redispatchFailedJob'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/redispatch/all',
'handler' => [$this, 'redispatchAllFailedJob'],
'roles' => $roles,
],
],
'DELETE' => [
[
'pattern' => $this->getEndpointPattern() . '/failed/delete/{jobId:\d+}',
'handler' => [$this, 'deleteFailedJob'],
'roles' => $roles,
],
],
]);
parent::__construct();
}
/**
* @param \APP\core\Request $request
* @param array $args
* @param array $roleAssignments
*
* @return bool
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get all pending jobs in the queue waiting to get executed
*/
public function getJobs(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$params = $slimRequest->getQueryParams();
$jobs = Repo::job()
->setOutputFormat(Repo::failedJob()::OUTPUT_HTTP)
->setPage($params['page'] ?? 1)
->showJobs();
return $response->withJson([
'data' => $jobs->all(),
'total' => Repo::job()->total(),
'pagination' => [
'lastPage' => $jobs->lastPage(),
'currentPage' => $jobs->currentPage(),
],
], 200);
}
/**
* Get all failed jobs in the failed list
*/
public function getFailedJobs(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$params = $slimRequest->getQueryParams();
$failedJobs = Repo::failedJob()
->setOutputFormat(Repo::failedJob()::OUTPUT_HTTP)
->setPage($params['page'] ?? 1)
->showJobs();
return $response->withJson([
'data' => $failedJobs->all(),
'total' => Repo::failedJob()->total(),
'pagination' => [
'lastPage' => $failedJobs->lastPage(),
'currentPage' => $failedJobs->currentPage(),
],
], 200);
}
/**
* Redispatch all failed jobs back to queue
* It will only redispatch failed jobs that has valid payload attribute
*/
public function redispatchAllFailedJob(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
if (Repo::failedJob()->total() <= 0) {
return $response->withStatus(406)->withJson([
'errorMessage' => __('api.jobs.406.failedJobEmpty')
]);
}
$redispatableFailedJobs = Repo::failedJob()->getRedispatchableJobsInQueue(null, ['id']);
return Repo::failedJob()->redispatchToQueue(null, $redispatableFailedJobs->pluck('id')->toArray())
? $response->withJson(['message' => __('api.jobs.200.allFailedJobRedispatchedSucceed')], 200)
: $response->withStatus(400)->withJson(['errorMessage' => __('api.jobs.400.failedJobRedispatchedFailed')]);
}
/**
* Redispatch a failed job back to queue
*/
public function redispatchFailedJob(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$failedJob = Repo::failedJob()->get((int) $args['jobId']);
if (!$failedJob) {
return $response->withStatus(404)->withJson([
'errorMessage' => __('api.jobs.404.failedJobNotFound')
]);
}
if (!$failedJob->payload) {
return $response->withStatus(406)->withJson([
'errorMessage' => __('api.jobs.406.failedJobPayloadMissing')
]);
}
return Repo::failedJob()->redispatchToQueue(null, [$failedJob->id])
? $response->withJson(['message' => __('api.jobs.200.failedJobRedispatchedSucceed')], 200)
: $response->withStatus(400)->withJson(['errorMessage' => __('api.jobs.400.failedJobRedispatchedFailed')]);
}
/**
* Delete a failed job from failed list
*/
public function deleteFailedJob(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$failedJob = Repo::failedJob()->get((int) $args['jobId']);
if (!$failedJob) {
return $response->withStatus(404)->withJson([
'errorMessage' => __('api.jobs.404.failedJobNotFound')
]);
}
return $failedJob->delete()
? $response->withJson(['message' => __('api.jobs.200.failedJobDeleteSucceed')], 200)
: $response->withStatus(400)->withJson(['errorMessage' => __('api.jobs.400.failedJobDeleteFailed')]);
}
}
@@ -0,0 +1,106 @@
<?php
/**
* @file api/v1/mailables/MailableHandler.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class MailableHandler
*
* @ingroup api_v1_mailables
*
* @brief Base class to handle API requests for mailables.
*/
namespace PKP\API\v1\mailables;
use APP\facades\Repo;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\security\authorization\ContextRequiredPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use Slim\Http\Request as SlimRequest;
use Slim\Http\Response;
class MailableHandler extends APIHandler
{
/**
* @copydoc APIHandler::__construct()
*/
public function __construct()
{
$this->_handlerPath = 'mailables';
$roles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER];
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/{id}',
'handler' => [$this, 'get'],
'roles' => $roles,
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
// This endpoint is not available at the site-wide level
$this->addPolicy(new ContextRequiredPolicy($request));
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get an array of mailables
*/
public function getMany(SlimRequest $slimRequest, Response $response, array $args): Response
{
$mailables = Repo::mailable()->getMany(
$this->getRequest()->getContext(),
$slimRequest->getQueryParam('searchPhrase')
)
->map(fn (string $class) => Repo::mailable()->summarizeMailable($class))
->sortBy('name');
return $response->withJson($mailables->values(), 200);
}
/**
* Get a mailable by its class name
*
* @param APIResponse $response
*/
public function get(SlimRequest $slimRequest, Response $response, array $args): Response
{
$context = $this->getRequest()->getContext();
$mailable = Repo::mailable()->get($args['id'], $context);
if (!$mailable) {
return $response->withStatus(404)->withJsonError('api.mailables.404.mailableNotFound');
}
return $response->withJson(Repo::mailable()->describeMailable($mailable, $context->getId()), 200);
}
}
+256
View File
@@ -0,0 +1,256 @@
<?php
/**
* @file api/v1/site/PKPSiteHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPSiteHandler
*
* @ingroup api_v1_users
*
* @brief Base class to handle API requests for the site object.
*/
namespace PKP\API\v1\site;
use APP\core\Application;
use APP\core\Services;
use APP\template\TemplateManager;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\plugins\PluginRegistry;
use PKP\plugins\ThemePlugin;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\services\PKPSchemaService;
use Slim\Http\Request as SlimRequest;
class PKPSiteHandler extends APIHandler
{
/** @var string One of the SCHEMA_... constants */
public $schemaName = PKPSchemaService::SCHEMA_SITE;
/**
* @copydoc APIHandler::__construct()
*/
public function __construct()
{
$this->_handlerPath = 'site';
$roles = [Role::ROLE_ID_SITE_ADMIN];
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'get'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/theme',
'handler' => [$this, 'getTheme'],
'roles' => $roles,
],
],
'PUT' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'edit'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/theme',
'handler' => [$this, 'editTheme'],
'roles' => $roles,
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get the site
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function get($slimRequest, $response, $args)
{
$request = $this->getRequest();
$siteProps = Services::get('site')
->getFullProperties($request->getSite(), [
'request' => $request,
]);
return $response->withJson($siteProps, 200);
}
/**
* Get the active theme on the site
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function getTheme($slimRequest, $response, $args)
{
$site = $this->getRequest()->getSite();
/** @var ThemePlugin[] */
$allThemes = PluginRegistry::loadCategory('themes', true);
/** @var ?ThemePlugin */
$activeTheme = null;
foreach ($allThemes as $theme) {
if ($site->getData('themePluginPath') === $theme->getDirName()) {
$activeTheme = $theme;
break;
}
}
if (!$activeTheme) {
return $response->withStatus(404)->withJsonError('api.themes.404.themeUnavailable');
}
$data = array_merge(
$activeTheme->getOptionValues(\PKP\core\PKPApplication::CONTEXT_ID_NONE),
['themePluginPath' => $theme->getDirName()]
);
ksort($data);
return $response->withJson($data, 200);
}
/**
* Edit the site
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function edit($slimRequest, $response, $args)
{
$request = $this->getRequest();
$site = $request->getSite();
$siteService = Services::get('site');
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_SITE, $slimRequest->getParsedBody());
$errors = $siteService->validate($params, $site->getSupportedLocales(), $site->getPrimaryLocale());
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$site = $siteService->edit($site, $params, $request);
$siteProps = $siteService->getFullProperties($site, [
'request' => $request,
'slimRequest' => $slimRequest
]);
return $response->withJson($siteProps, 200);
}
/**
* Edit the active theme and theme options on the site
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function editTheme($slimRequest, $response, $args)
{
$request = $this->getRequest();
$site = $request->getSite();
$siteService = Services::get('site');
$params = $slimRequest->getParsedBody();
// Validate the themePluginPath and allow themes to perform their own validation
$themePluginPath = empty($params['themePluginPath']) ? null : $params['themePluginPath'];
if ($themePluginPath !== $site->getData('themePluginPath')) {
$errors = $siteService->validate(
['themePluginPath' => $themePluginPath],
$site->getSupportedLocales(),
$site->getPrimaryLocale()
);
if (!empty($errors)) {
return $response->withJson($errors, 400);
}
$newSite = $siteService->edit($site, ['themePluginPath' => $themePluginPath], $request);
}
// Get the appropriate theme plugin
/** @var iterable<ThemePlugin> */
$allThemes = PluginRegistry::loadCategory('themes', true);
/** @var ?ThemePlugin */
$selectedTheme = null;
foreach ($allThemes as $theme) {
if ($themePluginPath === $theme->getDirName()) {
$selectedTheme = $theme;
break;
}
}
// Run the theme's init() method if a new theme has been selected
if (isset($newSite)) {
$selectedTheme->init();
}
$errors = $selectedTheme->validateOptions($params, $themePluginPath, \PKP\core\PKPApplication::CONTEXT_ID_NONE, $request);
if (!empty($errors)) {
return $response->withJson($errors, 400);
}
// Only accept params that are defined in the theme options
$options = $selectedTheme->getOptionsConfig();
foreach ($options as $optionName => $optionConfig) {
if (!array_key_exists($optionName, $params)) {
continue;
}
$selectedTheme->saveOption($optionName, $params[$optionName], \PKP\core\PKPApplication::CONTEXT_ID_NONE);
}
// Clear the template cache so that new settings can take effect
$templateMgr = TemplateManager::getManager(Application::get()->getRequest());
$templateMgr->clearTemplateCache();
$templateMgr->clearCssCache();
$data = array_merge(
$selectedTheme->getOptionValues(\PKP\core\PKPApplication::CONTEXT_ID_NONE),
['themePluginPath' => $themePluginPath]
);
ksort($data);
return $response->withJson($data, 200);
}
}
@@ -0,0 +1,443 @@
<?php
/**
* @file api/v1/stats/contexts/PKPStatsContextHandler.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPStatsContextHandler
*
* @ingroup api_v1_stats
*
* @brief Handle API requests for context statistics.
*
*/
namespace PKP\API\v1\stats\contexts;
use APP\core\Services;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\statistics\PKPStatisticsHelper;
use Slim\Http\Request as SlimHttpRequest;
class PKPStatsContextHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'stats/contexts';
$roles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER];
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => [Role::ROLE_ID_SITE_ADMIN]
],
[
'pattern' => $this->getEndpointPattern() . '/timeline',
'handler' => [$this, 'getManyTimeline'],
'roles' => [Role::ROLE_ID_SITE_ADMIN]
],
[
'pattern' => $this->getEndpointPattern() . '/{contextId:\d+}',
'handler' => [$this, 'get'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/{contextId:\d+}/timeline',
'handler' => [$this, 'getTimeline'],
'roles' => $roles
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize()
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get total views of the homepages for a set of contexts
*/
public function getMany(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$defaultParams = [
'count' => 30,
'offset' => 0,
'orderDirection' => PKPStatisticsHelper::STATISTICS_ORDER_DESC,
];
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, [
'dateStart',
'dateEnd',
'count',
'offset',
'orderDirection',
'searchPhrase',
'contextIds',
]);
Hook::call('API::stats::contexts::params', [&$allowedParams, $slimRequest]);
$result = $this->_validateStatDates($allowedParams);
if ($result !== true) {
return $response->withStatus(400)->withJsonError($result);
}
if (!in_array($allowedParams['orderDirection'], [PKPStatisticsHelper::STATISTICS_ORDER_ASC, PKPStatisticsHelper::STATISTICS_ORDER_DESC])) {
return $response->withStatus(400)->withJsonError('api.stats.400.invalidOrderDirection');
}
// Identify contexts which should be included in the results when a searchPhrase is passed
if (!empty($allowedParams['searchPhrase'])) {
$allowedContextIds = empty($allowedParams['contextIds']) ? [] : $allowedParams['contextIds'];
$allowedParams['contextIds'] = $this->_processSearchPhrase($allowedParams['searchPhrase'], $allowedContextIds);
if (empty($allowedParams['contextIds'])) {
if ($responseCSV) {
$csvColumnNames = $this->_getContextReportColumnNames();
return $response->withCSV([], $csvColumnNames, 0);
}
return $response->withJson([
'items' => [],
'itemsMax' => 0,
], 200);
}
}
// Get a list of contexts with their total views matching the params
$statsService = Services::get('contextStats');
$totalMetrics = $statsService->getTotals($allowedParams);
// Get the stats for each context
$items = [];
foreach ($totalMetrics as $totalMetric) {
$contextId = $totalMetric->context_id;
$contextViews = $totalMetric->metric;
if ($responseCSV) {
$items[] = $this->getItemForCSV($contextId, $contextViews);
} else {
$items[] = $this->getItemForJSON($slimRequest, $contextId, $contextViews);
}
}
$itemsMax = $statsService->getCount($allowedParams);
if ($responseCSV) {
$csvColumnNames = $this->_getContextReportColumnNames();
return $response->withCSV($items, $csvColumnNames, $itemsMax);
}
return $response->withJson([
'items' => $items,
'itemsMax' => $itemsMax,
], 200);
}
/**
* Get a monthly or daily timeline of total views for a set of contexts
*/
public function getManyTimeline(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$defaultParams = [
'timelineInterval' => PKPStatisticsHelper::STATISTICS_DIMENSION_MONTH,
];
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, [
'dateStart',
'dateEnd',
'timelineInterval',
'searchPhrase',
'contextIds',
]);
Hook::call('API::stats::contexts::timeline::params', [&$allowedParams, $slimRequest]);
if (!$this->isValidTimelineInterval($allowedParams['timelineInterval'])) {
return $response->withStatus(400)->withJsonError('api.stats.400.wrongTimelineInterval');
}
$result = $this->_validateStatDates($allowedParams);
if ($result !== true) {
return $response->withStatus(400)->withJsonError($result);
}
// Identify contexts which should be included in the results when a searchPhrase is passed
if (!empty($allowedParams['searchPhrase'])) {
$allowedContextIds = empty($allowedParams['contextIds']) ? [] : $allowedParams['contextIds'];
$allowedParams['contextIds'] = $this->_processSearchPhrase($allowedParams['searchPhrase'], $allowedContextIds);
if (empty($allowedParams['contextIds'])) {
$dateStart = empty($allowedParams['dateStart']) ? PKPStatisticsHelper::STATISTICS_EARLIEST_DATE : $allowedParams['dateStart'];
$dateEnd = empty($allowedParams['dateEnd']) ? date('Ymd', strtotime('yesterday')) : $allowedParams['dateEnd'];
$emptyTimeline = Services::get('contextStats')->getEmptyTimelineIntervals($dateStart, $dateEnd, $allowedParams['timelineInterval']);
if ($responseCSV) {
$csvColumnNames = Services::get('contextStats')->getTimelineReportColumnNames();
return $response->withCSV($emptyTimeline, $csvColumnNames, 0);
}
return $response->withJson($emptyTimeline, 200);
}
}
$data = Services::get('contextStats')->getTimeline($allowedParams['timelineInterval'], $allowedParams);
if ($responseCSV) {
$csvColumnNames = Services::get('contextStats')->getTimelineReportColumnNames();
return $response->withCSV($data, $csvColumnNames, count($data));
}
return $response->withJson($data, 200);
}
/**
* Get a single context's usage statistics
*/
public function get(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$request = $this->getRequest();
$context = Services::get('context')->get((int) $args['contextId']);
if (!$context) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
// Don't allow to get one context from a different context's endpoint
if ($request->getContext() && $request->getContext()->getId() !== $context->getId()) {
return $response->withStatus(403)->withJsonError('api.contexts.403.contextsDidNotMatch');
}
$allowedParams = $this->_processAllowedParams($slimRequest->getQueryParams(), [
'dateStart',
'dateEnd',
]);
Hook::call('API::stats::context::params', [&$allowedParams, $slimRequest]);
$result = $this->_validateStatDates($allowedParams);
if ($result !== true) {
return $response->withStatus(400)->withJsonError($result);
}
$dateStart = array_key_exists('dateStart', $allowedParams) ? $allowedParams['dateStart'] : null;
$dateEnd = array_key_exists('dateEnd', $allowedParams) ? $allowedParams['dateEnd'] : null;
$statsService = Services::get('contextStats');
$contextViews = $statsService->getTotal($context->getId(), $dateStart, $dateEnd);
// Get basic context details for display
$propertyArgs = [
'request' => $request,
'slimRequest' => $slimRequest,
];
$contextProps = Services::get('context')->getSummaryProperties($context, $propertyArgs);
if ($responseCSV) {
$csvColumnNames = $this->_getContextReportColumnNames();
$items = [$this->getItemForCSV($context->getId(), $contextViews)];
return $response->withCSV($items, $csvColumnNames, 1);
}
return $response->withJson([
'total' => $contextViews,
'context' => $contextProps
], 200);
}
/**
* Get a monthly or daily timeline of total views for a context
*/
public function getTimeline(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$request = $this->getRequest();
$context = Services::get('context')->get((int) $args['contextId']);
if (!$context) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
// Don't allow to get one context from a different context's endpoint
if ($request->getContext() && $request->getContext()->getId() !== $context->getId()) {
return $response->withStatus(403)->withJsonError('api.contexts.403.contextsDidNotMatch');
}
$defaultParams = [
'timelineInterval' => PKPStatisticsHelper::STATISTICS_DIMENSION_MONTH,
];
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, [
'dateStart',
'dateEnd',
'timelineInterval',
]);
$allowedParams['contextIds'] = [$context->getId()];
Hook::call('API::stats::context::timeline::params', [&$allowedParams, $slimRequest]);
if (!$this->isValidTimelineInterval($allowedParams['timelineInterval'])) {
return $response->withStatus(400)->withJsonError('api.stats.400.wrongTimelineInterval');
}
$result = $this->_validateStatDates($allowedParams);
if ($result !== true) {
return $response->withStatus(400)->withJsonError($result);
}
$statsService = Services::get('contextStats');
$data = $statsService->getTimeline($allowedParams['timelineInterval'], $allowedParams);
if ($responseCSV) {
$csvColumnNames = Services::get('contextStats')->getTimelineReportColumnNames();
return $response->withCSV($data, $csvColumnNames, count($data));
}
return $response->withJson($data, 200);
}
/**
* A helper method to filter and sanitize the request params
*
* Only allows the specified params through and enforces variable
* type where needed.
*/
protected function _processAllowedParams(array $requestParams, array $allowedParams): array
{
$returnParams = [];
foreach ($requestParams as $requestParam => $value) {
if (!in_array($requestParam, $allowedParams)) {
continue;
}
switch ($requestParam) {
case 'dateStart':
case 'dateEnd':
case 'timelineInterval':
$returnParams[$requestParam] = $value;
break;
case 'count':
$returnParams[$requestParam] = min(100, (int) $value);
break;
case 'offset':
$returnParams[$requestParam] = (int) $value;
break;
case 'orderDirection':
$returnParams[$requestParam] = strtoupper($value);
break;
case 'contextIds':
if (is_string($value) && strpos($value, ',') > -1) {
$value = explode(',', $value);
} elseif (!is_array($value)) {
$value = [$value];
}
$returnParams[$requestParam] = array_map('intval', $value);
break;
}
}
return $returnParams;
}
/**
* A helper method to get the contextIds param when a searchPhase
* param is also passed.
*
* If the searchPhrase and contextIds params were both passed in the
* request, then we only return IDs that match both conditions.
*/
protected function _processSearchPhrase(string $searchPhrase, array $contextIds = []): array
{
$searchPhraseContextIds = Services::get('context')->getIds(['searchPhrase' => $searchPhrase]);
if (!empty($contextIds)) {
return array_intersect($contextIds, $searchPhraseContextIds->toArray());
}
return $searchPhraseContextIds->toArray();
}
/**
* Get CSV report columns
*/
protected function _getContextReportColumnNames(): array
{
return [
__('common.id'),
__('common.title'),
__('stats.total'),
];
}
/**
* Get CSV row with context index page metrics
*/
protected function getItemForCSV(int $contextId, int $contextViews): array
{
// Get context title for display
$contexts = Services::get('context')->getManySummary([]);
$context = array_filter($contexts, function ($context) use ($contextId) {
return $context->id == $contextId;
});
$title = current($context)->name;
return [
$contextId,
$title,
$contextViews
];
}
/**
* Get JSON data with context index page metrics
*/
protected function getItemForJSON(SlimHttpRequest $slimRequest, int $contextId, int $contextViews): array
{
// Get basic context details for display
$propertyArgs = [
'request' => $this->getRequest(),
'slimRequest' => $slimRequest,
];
$context = Services::get('context')->get($contextId);
$contextProps = Services::get('context')->getSummaryProperties($context, $propertyArgs);
return [
'total' => $contextViews,
'context' => $contextProps,
];
}
/**
* Check if the timeline interval is valid
*/
protected function isValidTimelineInterval(string $interval): bool
{
return in_array($interval, [
PKPStatisticsHelper::STATISTICS_DIMENSION_DAY,
PKPStatisticsHelper::STATISTICS_DIMENSION_MONTH
]);
}
}
@@ -0,0 +1,176 @@
<?php
/**
* @file api/v1/stats/editorial/PKPStatsEditorialHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPStatsEditorialHandler
*
* @ingroup api_v1_stats
*
* @brief Handle API requests for publication statistics.
*
*/
namespace PKP\API\v1\stats\editorial;
use APP\core\Services;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use Slim\Http\Request;
abstract class PKPStatsEditorialHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'stats/editorial';
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'get'],
'roles' => [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR],
],
[
'pattern' => $this->getEndpointPattern() . '/averages',
'handler' => [$this, 'getAverages'],
'roles' => [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR],
],
],
];
parent::__construct();
}
/** The name of the section ids query param for this application */
abstract public function getSectionIdsQueryParam();
/**
* @copydoc PKPHandler::authorize()
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get editorial stats
*
* Returns information on submissions received, accepted, declined,
* average response times and more.
*
* @param Request $slimRequest Slim request object
* @param APIResponse $response Response
* @param array $args
*
* @return APIResponse Response
*/
public function get($slimRequest, $response, $args)
{
$request = $this->getRequest();
if (!$request->getContext()) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$params = [];
$sectionIdsQueryParam = $this->getSectionIdsQueryParam();
foreach ($slimRequest->getQueryParams() as $param => $value) {
switch ($param) {
case 'dateStart':
case 'dateEnd':
$params[$param] = $value;
break;
case $sectionIdsQueryParam:
if (is_string($value) && str_contains($value, ',')) {
$value = explode(',', $value);
} elseif (!is_array($value)) {
$value = [$value];
}
$params[$param] = array_map('intval', $value);
break;
}
}
Hook::call('API::stats::editorial::params', [&$params, $slimRequest]);
$params['contextIds'] = [$request->getContext()->getId()];
$result = $this->_validateStatDates($params);
if ($result !== true) {
return $response->withStatus(400)->withJsonError($result);
}
return $response->withJson(array_map(
function ($item) {
$item['name'] = __($item['name']);
return $item;
},
Services::get('editorialStats')->getOverview($params)
));
}
/**
* Get yearly averages of editorial stats
*
* Returns information on average submissions received, accepted
* and declined per year.
*
* @param Request $slimRequest Slim request object
* @param APIResponse $response Response
* @param array $args
*
* @return APIResponse Response
*/
public function getAverages($slimRequest, $response, $args)
{
$request = $this->getRequest();
if (!$request->getContext()) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$params = [];
$sectionIdsQueryParam = $this->getSectionIdsQueryParam();
foreach ($slimRequest->getQueryParams() as $param => $value) {
switch ($param) {
case $sectionIdsQueryParam:
if (is_string($value) && str_contains($value, ',')) {
$value = explode(',', $value);
} elseif (!is_array($value)) {
$value = [$value];
}
$params[$param] = array_map('intval', $value);
break;
}
}
Hook::call('API::stats::editorial::averages::params', [&$params, $slimRequest]);
$params['contextIds'] = [$request->getContext()->getId()];
return $response->withJson(Services::get('editorialStats')->getAverages($params));
}
}
@@ -0,0 +1,980 @@
<?php
/**
* @file api/v1/stats/publications/PKPStatsPublicationHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPStatsPublicationHandler
*
* @ingroup api_v1_stats
*
* @brief Handle API requests for submission statistics.
*
*/
namespace PKP\API\v1\stats\publications;
use APP\core\Application;
use APP\core\Services;
use APP\facades\Repo;
use APP\statistics\StatisticsHelper;
use APP\submission\Submission;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\SubmissionAccessPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use Slim\Http\Request as SlimHttpRequest;
use Sokil\IsoCodes\IsoCodesFactory;
abstract class PKPStatsPublicationHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'stats/publications';
$roles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR];
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/timeline',
'handler' => [$this, 'getManyTimeline'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}',
'handler' => [$this, 'get'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/timeline',
'handler' => [$this, 'getTimeline'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/files',
'handler' => [$this, 'getManyFiles'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/countries',
'handler' => [$this, 'getManyCountries'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/regions',
'handler' => [$this, 'getManyRegions'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/cities',
'handler' => [$this, 'getManyCities'],
'roles' => $roles
],
],
];
parent::__construct();
}
/** The name of the section ids query param for this application */
abstract public function getSectionIdsQueryParam();
/**
* @copydoc PKPHandler::authorize()
*/
public function authorize($request, &$args, $roleAssignments)
{
$routeName = null;
$slimRequest = $this->getSlimRequest();
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
if (!is_null($slimRequest) && ($route = $slimRequest->getAttribute('route'))) {
$routeName = $route->getName();
}
if (in_array($routeName, ['get', 'getTimeline'])) {
$this->addPolicy(new SubmissionAccessPolicy($request, $args, $roleAssignments));
}
return parent::authorize($request, $args, $roleAssignments);
}
/**
* A helper method to filter and sanitize the application specific request params
*/
protected function _processAppSpecificAllowedParams(string $requestParam, mixed $value, array &$returnParams): void
{
}
/**
* Get allowed parameters for getMany methods:
* getMany(), getManyFiles(), getManyCountries(), getManyRegions(), getManyCities
*/
protected function getManyAllowedParams()
{
$allowedParams = [
'dateStart',
'dateEnd',
'count',
'offset',
'orderDirection',
'searchPhrase',
'submissionIds',
];
$allowedParams[] = $this->getSectionIdsQueryParam();
return $allowedParams;
}
/**
* Get allowed parameters for getManyTimeline method
*/
protected function getManyTimelineAllowedParams()
{
$allowedParams = [
'dateStart',
'dateEnd',
'timelineInterval',
'searchPhrase',
'submissionIds',
'type'
];
$allowedParams[] = $this->getSectionIdsQueryParam();
return $allowedParams;
}
/**
* Get usage stats for a set of publications
*
* Returns total views by abstract, all galleys, pdf galleys,
* html galleys, and other galleys.
*/
public function getMany(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$defaultParams = [
'orderDirection' => StatisticsHelper::STATISTICS_ORDER_DESC,
];
$initAllowedParams = $this->getManyAllowedParams();
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, $initAllowedParams);
Hook::call('API::stats::publications::params', [&$allowedParams, $slimRequest]);
// Check/validate, filter and sanitize the request params
try {
$allowedParams = $this->validateParams($allowedParams);
} catch (\Exception $e) {
if ($e->getCode() == 200) {
if ($responseCSV) {
$csvColumnNames = $this->_getSubmissionReportColumnNames();
return $response->withCSV([], $csvColumnNames, 0);
}
return $response->withJson([
'items' => [],
'itemsMax' => 0,
], 200);
}
return $response->withStatus($e->getCode())->withJsonError($e->getMessage());
}
$statsService = Services::get('publicationStats');
// Get a list of top submissions by total views
$totalMetrics = $statsService->getTotals($allowedParams);
// Get the stats for each submission
$items = [];
foreach ($totalMetrics as $totalMetric) {
$submissionId = $totalMetric->submission_id;
// get abstract, pdf, html and other views for the submission
$dateStart = array_key_exists('dateStart', $allowedParams) ? $allowedParams['dateStart'] : null;
$dateEnd = array_key_exists('dateEnd', $allowedParams) ? $allowedParams['dateEnd'] : null;
$metricsByType = $statsService->getTotalsByType($submissionId, $this->getRequest()->getContext()->getId(), $dateStart, $dateEnd);
if ($responseCSV) {
$items[] = $this->getItemForCSV($submissionId, $metricsByType['abstract'], $metricsByType['pdf'], $metricsByType['html'], $metricsByType['other']);
} else {
$items[] = $this->getItemForJSON($submissionId, $metricsByType['abstract'], $metricsByType['pdf'], $metricsByType['html'], $metricsByType['other']);
}
}
// Get the total count of submissions
$itemsMax = $statsService->getCount($allowedParams);
if ($responseCSV) {
$csvColumnNames = $this->_getSubmissionReportColumnNames();
return $response->withCSV($items, $csvColumnNames, $itemsMax);
}
return $response->withJson([
'items' => $items,
'itemsMax' => $itemsMax,
], 200);
}
/**
* Get the total abstract or files views for a set of submissions
* in a timeline broken down by month or day
*/
public function getManyTimeline(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$defaultParams = [
'timelineInterval' => StatisticsHelper::STATISTICS_DIMENSION_MONTH,
];
$initAllowedParams = $this->getManyTimelineAllowedParams();
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, $initAllowedParams);
Hook::call('API::stats::publications::timeline::params', [&$allowedParams, $slimRequest]);
$statsService = Services::get('publicationStats');
// Check/validate, filter and sanitize the request params
try {
$allowedParams = $this->validateParams($allowedParams);
} catch (\Exception $e) {
if ($e->getCode() == 200) {
$dateStart = empty($allowedParams['dateStart']) ? StatisticsHelper::STATISTICS_EARLIEST_DATE : $allowedParams['dateStart'];
$dateEnd = empty($allowedParams['dateEnd']) ? date('Ymd', strtotime('yesterday')) : $allowedParams['dateEnd'];
$emptyTimeline = $statsService->getEmptyTimelineIntervals($dateStart, $dateEnd, $allowedParams['timelineInterval']);
if ($responseCSV) {
$csvColumnNames = $statsService->getTimelineReportColumnNames();
return $response->withCSV($emptyTimeline, $csvColumnNames, 0);
}
return $response->withJson($emptyTimeline, 200);
}
return $response->withStatus($e->getCode())->withJsonError($e->getMessage());
}
$allowedParams['assocTypes'] = [Application::ASSOC_TYPE_SUBMISSION];
if (array_key_exists('type', $allowedParams) && $allowedParams['type'] == 'files') {
$allowedParams['assocTypes'] = [Application::ASSOC_TYPE_SUBMISSION_FILE];
};
$data = $statsService->getTimeline($allowedParams['timelineInterval'], $allowedParams);
if ($responseCSV) {
$csvColumnNames = $statsService->getTimelineReportColumnNames();
return $response->withCSV($data, $csvColumnNames, count($data));
}
return $response->withJson($data, 200);
}
/**
* Get a single submission's usage statistics
*/
public function get(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$request = $this->getRequest();
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
$allowedParams = $this->_processAllowedParams($slimRequest->getQueryParams(), [
'dateStart',
'dateEnd',
]);
Hook::call('API::stats::publication::params', [&$allowedParams, $slimRequest]);
$result = $this->_validateStatDates($allowedParams);
if ($result !== true) {
return $response->withStatus(400)->withJsonError($result);
}
$statsService = Services::get('publicationStats');
// get abstract, pdf, html and other views for the submission
$dateStart = array_key_exists('dateStart', $allowedParams) ? $allowedParams['dateStart'] : null;
$dateEnd = array_key_exists('dateEnd', $allowedParams) ? $allowedParams['dateEnd'] : null;
$metricsByType = $statsService->getTotalsByType($submission->getId(), $request->getContext()->getId(), $dateStart, $dateEnd);
$galleyViews = $metricsByType['pdf'] + $metricsByType['html'] + $metricsByType['other'];
return $response->withJson([
'abstractViews' => $metricsByType['abstract'],
'galleyViews' => $galleyViews,
'pdfViews' => $metricsByType['pdf'],
'htmlViews' => $metricsByType['html'],
'otherViews' => $metricsByType['other'],
'publication' => Repo::submission()->getSchemaMap()->mapToStats($submission),
], 200);
}
/**
* Get the total abstract of files views for a submission broken down by
* month or day
*/
public function getTimeline(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$request = $this->getRequest();
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
$defaultParams = [
'timelineInterval' => StatisticsHelper::STATISTICS_DIMENSION_MONTH,
];
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, [
'dateStart',
'dateEnd',
'timelineInterval',
'type'
]);
Hook::call('API::stats::publication::timeline::params', [&$allowedParams, $slimRequest]);
$allowedParams['contextIds'] = [$request->getContext()->getId()];
$allowedParams['submissionIds'] = [$submission->getId()];
$allowedParams['assocTypes'] = [Application::ASSOC_TYPE_SUBMISSION];
if (array_key_exists('type', $allowedParams) && $allowedParams['type'] == 'files') {
$allowedParams['assocTypes'] = [Application::ASSOC_TYPE_SUBMISSION_FILE];
};
$result = $this->_validateStatDates($allowedParams);
if ($result !== true) {
return $response->withStatus(400)->withJsonError($result);
}
if (!in_array($allowedParams['timelineInterval'], [StatisticsHelper::STATISTICS_DIMENSION_DAY, StatisticsHelper::STATISTICS_DIMENSION_MONTH])) {
return $response->withStatus(400)->withJsonError('api.stats.400.invalidTimelineInterval');
}
$statsService = Services::get('publicationStats');
$data = $statsService->getTimeline($allowedParams['timelineInterval'], $allowedParams);
return $response->withJson($data, 200);
}
/**
* Get total usage stats for a set of submission files.
*/
public function getManyFiles(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$defaultParams = [
'orderDirection' => StatisticsHelper::STATISTICS_ORDER_DESC,
];
$initAllowedParams = $this->getManyAllowedParams();
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, $initAllowedParams);
Hook::call('API::stats::publications::files::params', [&$allowedParams, $slimRequest]);
// Check/validate, filter and sanitize the request params
try {
$allowedParams = $this->validateParams($allowedParams);
} catch (\Exception $e) {
if ($e->getCode() == 200) {
if ($responseCSV) {
$csvColumnNames = $this->_getFileReportColumnNames();
return $response->withCSV([], $csvColumnNames, 0);
}
return $response->withJson([
'items' => [],
'itemsMax' => 0,
], 200);
}
return $response->withStatus($e->getCode())->withJsonError($e->getMessage());
}
$statsService = Services::get('publicationStats');
$filesMetrics = $statsService->getFilesTotals($allowedParams);
$items = $submissionTitles = [];
foreach ($filesMetrics as $fileMetric) {
$submissionId = $fileMetric->submission_id;
$submissionFileId = $fileMetric->submission_file_id;
$downloads = $fileMetric->metric;
$type = $fileMetric->assoc_type;
if (!isset($submissionTitles[$submissionId])) {
$submission = Repo::submission()->get($submissionId);
$submissionTitles[$submissionId] = $submission->getCurrentPublication()->getLocalizedTitle();
}
if ($responseCSV) {
$items[] = $this->getFilesCSVItem($submissionFileId, $downloads, $type, $submissionId, $submissionTitles[$submissionId]);
} else {
$items[] = $this->getFilesJSONItem($submissionFileId, $downloads, $type, $submissionId, $submissionTitles[$submissionId]);
}
}
// Get the total count of submissions files
$itemsMax = $statsService->getFilesCount($allowedParams);
if ($responseCSV) {
$csvColumnNames = $this->_getFileReportColumnNames();
return $response->withCSV($items, $csvColumnNames, $itemsMax);
}
return $response->withJson([
'items' => $items,
'itemsMax' => $itemsMax,
], 200);
}
/**
* Get usage stats for a set of countries
*
* Returns total count of views, downloads, unique views and unique downloads in a country
*/
public function getManyCountries(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$defaultParams = [
'orderDirection' => StatisticsHelper::STATISTICS_ORDER_DESC,
];
$initAllowedParams = $this->getManyAllowedParams();
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, $initAllowedParams);
Hook::call('API::stats::publications::countries::params', [&$allowedParams, $slimRequest]);
// Check/validate, filter and sanitize the request params
try {
$allowedParams = $this->validateParams($allowedParams);
} catch (\Exception $e) {
if ($e->getCode() == 200) {
if ($responseCSV) {
$csvColumnNames = $this->_getGeoReportColumnNames(StatisticsHelper::STATISTICS_DIMENSION_COUNTRY);
return $response->withCSV([], $csvColumnNames, 0);
}
return $response->withJson([
'items' => [],
'itemsMax' => 0,
], 200);
}
return $response->withStatus($e->getCode())->withJsonError($e->getMessage());
}
$statsService = Services::get('geoStats');
// Get a list of top countries by total views
$totals = $statsService->getTotals($allowedParams, StatisticsHelper::STATISTICS_DIMENSION_COUNTRY);
// Get the stats for each country
$items = [];
$isoCodes = app(IsoCodesFactory::class);
foreach ($totals as $total) {
$country = !empty($total->country) ? $isoCodes->getCountries()->getByAlpha2($total->country) : null;
$countryName = $country ? $country->getLocalName() : $total->country;
$metric = $total->metric;
$metric_unique = $total->metric_unique;
if ($responseCSV) {
$items[] = $this->getGeoCSVItem($metric, $metric_unique, $countryName);
} else {
$items[] = $this->getGeoJSONItem($metric, $metric_unique, $countryName);
}
}
// Get the total count of countries
$itemsMax = $statsService->getCount($allowedParams, StatisticsHelper::STATISTICS_DIMENSION_COUNTRY);
if ($responseCSV) {
$csvColumnNames = $this->_getGeoReportColumnNames(StatisticsHelper::STATISTICS_DIMENSION_COUNTRY);
return $response->withCSV($items, $csvColumnNames, $itemsMax);
}
return $response->withJson([
'items' => $items,
'itemsMax' => $itemsMax,
], 200);
}
/**
* Get usage stats for set of regions
*
* Returns total count of views, downloads, unique views and unique downloads in a region
*/
public function getManyRegions(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$defaultParams = [
'orderDirection' => StatisticsHelper::STATISTICS_ORDER_DESC,
];
$initAllowedParams = $this->getManyAllowedParams();
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, $initAllowedParams);
Hook::call('API::stats::publications::regions::params', [&$allowedParams, $slimRequest]);
// Check/validate, filter and sanitize the request params
try {
$allowedParams = $this->validateParams($allowedParams);
} catch (\Exception $e) {
if ($e->getCode() == 200) {
if ($responseCSV) {
$csvColumnNames = $this->_getGeoReportColumnNames(StatisticsHelper::STATISTICS_DIMENSION_REGION);
return $response->withCSV([], $csvColumnNames, 0);
}
return $response->withJson([
'items' => [],
'itemsMax' => 0,
], 200);
}
return $response->withStatus($e->getCode())->withJsonError($e->getMessage());
}
$statsService = Services::get('geoStats');
// Get a list of top regions by total views
$totals = $statsService->getTotals($allowedParams, StatisticsHelper::STATISTICS_DIMENSION_REGION);
// Get the stats for each region
$items = [];
$isoCodes = app(IsoCodesFactory::class);
foreach ($totals as $total) {
$country = !empty($total->country) ? $isoCodes->getCountries()->getByAlpha2($total->country) : null;
$countryName = $country ? $country->getLocalName() : __('stats.unknown');
$regionName = !empty($total->region) ? $total->region : __('stats.unknown');
if (!empty($total->country) && !empty($total->region)) {
$regionCode = $total->country . '-' . $total->region;
$region = $isoCodes->getSubdivisions()->getByCode($regionCode);
$regionName = $region ? $region->getLocalName() : $regionCode;
}
$metric = $total->metric;
$metric_unique = $total->metric_unique;
if ($responseCSV) {
$items[] = $this->getGeoCSVItem($metric, $metric_unique, $countryName, $regionName);
} else {
$items[] = $this->getGeoJSONItem($metric, $metric_unique, $countryName, $regionName);
}
}
// Get the total count of regions
$itemsMax = $statsService->getCount($allowedParams, StatisticsHelper::STATISTICS_DIMENSION_REGION);
if ($responseCSV) {
$csvColumnNames = $this->_getGeoReportColumnNames(StatisticsHelper::STATISTICS_DIMENSION_REGION);
return $response->withCSV($items, $csvColumnNames, $itemsMax);
}
return $response->withJson([
'items' => $items,
'itemsMax' => $itemsMax,
], 200);
}
/**
* Get usage stats for set of cities
*
* Returns total count of views, downloads, unique views and unique downloads in a city
*/
public function getManyCities(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$defaultParams = [
'orderDirection' => StatisticsHelper::STATISTICS_ORDER_DESC,
];
$initAllowedParams = $this->getManyAllowedParams();
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, $initAllowedParams);
Hook::call('API::stats::publications::cities::params', [&$allowedParams, $slimRequest]);
// Check/validate, filter and sanitize the request params
try {
$allowedParams = $this->validateParams($allowedParams);
} catch (\Exception $e) {
if ($e->getCode() == 200) {
if ($responseCSV) {
$csvColumnNames = $this->_getGeoReportColumnNames(StatisticsHelper::STATISTICS_DIMENSION_CITY);
return $response->withCSV([], $csvColumnNames, 0);
}
return $response->withJson([
'items' => [],
'itemsMax' => 0,
], 200);
}
return $response->withStatus($e->getCode())->withJsonError($e->getMessage());
}
$statsService = Services::get('geoStats');
// Get a list of top cities by total views
$totals = $statsService->getTotals($allowedParams, StatisticsHelper::STATISTICS_DIMENSION_CITY);
// Get the stats for each city
$items = [];
$isoCodes = app(IsoCodesFactory::class);
foreach ($totals as $total) {
$country = !empty($total->country) ? $isoCodes->getCountries()->getByAlpha2($total->country) : null;
$countryName = $country ? $country->getLocalName() : __('stats.unknown');
$regionName = !empty($total->region) ? $total->region : __('stats.unknown');
if (!empty($total->country) && !empty($total->region)) {
$regionCode = $total->country . '-' . $total->region;
$region = $isoCodes->getSubdivisions()->getByCode($regionCode);
$regionName = $region ? $region->getLocalName() : $regionCode;
}
$cityName = !empty($total->city) ? $total->city : __('stats.unknown');
$metric = $total->metric;
$metric_unique = $total->metric_unique;
if ($responseCSV) {
$items[] = $this->getGeoCSVItem($metric, $metric_unique, $countryName, $regionName, $cityName);
} else {
$items[] = $this->getGeoJSONItem($metric, $metric_unique, $countryName, $regionName, $cityName);
}
}
// Get the total count of cities
$itemsMax = $statsService->getCount($allowedParams, StatisticsHelper::STATISTICS_DIMENSION_CITY);
if ($responseCSV) {
$csvColumnNames = $this->_getGeoReportColumnNames(StatisticsHelper::STATISTICS_DIMENSION_CITY);
return $response->withCSV($items, $csvColumnNames, $itemsMax);
}
return $response->withJson([
'items' => $items,
'itemsMax' => $itemsMax,
], 200);
}
/**
* Validate, filter, sanitize the params
*
* @throws \Exception
*/
protected function validateParams(array $allowedParams): array
{
$request = $this->getRequest();
$allowedParams['contextIds'] = [$request->getContext()->getId()];
$result = $this->_validateStatDates($allowedParams);
if ($result !== true) {
throw new \Exception($result, 400);
}
if (array_key_exists('orderDirection', $allowedParams) && !in_array($allowedParams['orderDirection'], [StatisticsHelper::STATISTICS_ORDER_ASC, StatisticsHelper::STATISTICS_ORDER_DESC])) {
throw new \Exception('api.stats.400.invalidOrderDirection', 400);
}
if (array_key_exists('timelineInterval', $allowedParams) && !$this->isValidTimelineInterval($allowedParams['timelineInterval'])) {
throw new \Exception('api.stats.400.invalidTimelineInterval', 400);
}
// Identify submissions which should be included in the results when a searchPhrase is passed
if (!empty($allowedParams['searchPhrase'])) {
$allowedSubmissionIds = empty($allowedParams['submissionIds']) ? [] : $allowedParams['submissionIds'];
$allowedParams['submissionIds'] = $this->_processSearchPhrase($allowedParams['searchPhrase'], $allowedSubmissionIds);
if (empty($allowedParams['submissionIds'])) {
throw new \Exception('', 200);
}
unset($allowedParams['searchPhrase']);
}
return $allowedParams;
}
/**
* Filter and sanitize the request param
*/
protected function _processParam(string $requestParam, mixed $value): array
{
$returnParams = [];
$sectionIdsQueryParam = $this->getSectionIdsQueryParam();
switch ($requestParam) {
case 'dateStart':
case 'dateEnd':
case 'timelineInterval':
case 'searchPhrase':
case 'type':
$returnParams[$requestParam] = $value;
break;
case 'count':
$returnParams[$requestParam] = min(100, (int) $value);
break;
case 'offset':
$returnParams[$requestParam] = (int) $value;
break;
case 'orderDirection':
$returnParams[$requestParam] = strtoupper($value);
break;
case $sectionIdsQueryParam:
if (is_string($value) && str_contains($value, ',')) {
$value = explode(',', $value);
} elseif (!is_array($value)) {
$value = [$value];
}
$returnParams['pkpSectionIds'] = array_map('intval', $value);
break;
case 'submissionIds':
if (is_string($value) && str_contains($value, ',')) {
$value = explode(',', $value);
} elseif (!is_array($value)) {
$value = [$value];
}
$returnParams[$requestParam] = array_map('intval', $value);
break;
}
return $returnParams;
}
/**
* A helper method to filter and sanitize the request params
*
* Only allows the specified params through and enforces variable
* type where needed.
*/
protected function _processAllowedParams(array $requestParams, array $allowedParams): array
{
$returnParams = [];
foreach ($requestParams as $requestParam => $value) {
if (!in_array($requestParam, $allowedParams)) {
continue;
}
$returnParams += $this->_processParam($requestParam, $value);
}
// Get the context's earliest date of publication if no start date is set
if (in_array('dateStart', $allowedParams) && !isset($returnParams['dateStart'])) {
$dateRange = Repo::publication()->getDateBoundaries(
Repo::publication()
->getCollector()
->filterByContextIds([$this->getRequest()->getContext()->getId()])
);
$returnParams['dateStart'] = $dateRange->min_date_published;
}
return $returnParams;
}
/**
* A helper method to get the submissionIds param when a searchPhase
* param is also passed.
*
* If the searchPhrase and submissionIds params were both passed in the
* request, then we only return IDs that match both conditions.
*/
protected function _processSearchPhrase(string $searchPhrase, array $submissionIds = []): array
{
$searchPhraseSubmissionIds = Repo::submission()
->getCollector()
->filterByContextIds([Application::get()->getRequest()->getContext()->getId()])
->filterByStatus([Submission::STATUS_PUBLISHED])
->searchPhrase($searchPhrase)
->getIds();
if (!empty($submissionIds)) {
$submissionIds = array_intersect($submissionIds, $searchPhraseSubmissionIds->toArray());
} else {
$submissionIds = $searchPhraseSubmissionIds->toArray();
}
return $submissionIds;
}
/**
* Get column names for the submission CSV report
*/
protected function _getSubmissionReportColumnNames(): array
{
return [
__('common.id'),
__('common.title'),
__('stats.total'),
__('submission.abstractViews'),
__('stats.fileViews'),
__('stats.pdf'),
__('stats.html'),
__('common.other')
];
}
/**
* Get column names for the file CSV report
*/
protected function _getFileReportColumnNames(): array
{
return [
__('common.publication') . ' ' . __('common.id'),
__('submission.title'),
__('common.file') . ' ' . __('common.id'),
__('common.fileName'),
__('common.type'),
__('stats.fileViews'),
];
}
/**
* Get column names for the country, region and city CSV report
*/
protected function _getGeoReportColumnNames(string $scale, bool $withPublication = false): array
{
$publicationColumns = [];
if ($withPublication) {
$publicationColumns = [
__('common.id'),
__('common.title')
];
}
$scaleColumns = [];
if ($scale == StatisticsHelper::STATISTICS_DIMENSION_CITY) {
$scaleColumns = [
__('stats.city'),
__('stats.region'),
__('common.country')
];
} elseif ($scale == StatisticsHelper::STATISTICS_DIMENSION_REGION) {
$scaleColumns = [__('stats.region'), __('common.country')];
} elseif ($scale == StatisticsHelper::STATISTICS_DIMENSION_COUNTRY) {
$scaleColumns = [__('common.country'),];
}
return array_merge(
$publicationColumns,
$scaleColumns,
[__('stats.total'), __('stats.unique')]
);
}
/**
* Get the CSV row with submission metrics
*/
protected function getItemForCSV(int $submissionId, int $abstractViews, int $pdfViews, int $htmlViews, int $otherViews): array
{
$galleyViews = $pdfViews + $htmlViews + $otherViews;
$totalViews = $abstractViews + $galleyViews;
// Get submission title for display
$submission = Repo::submission()->get($submissionId);
$submissionTitle = $submission->getCurrentPublication()->getLocalizedTitle();
return [
$submissionId,
$submissionTitle,
$totalViews,
$abstractViews,
$galleyViews,
$pdfViews,
$htmlViews,
$otherViews
];
}
/**
* Get the JSON data with submission metrics
*/
protected function getItemForJSON(int $submissionId, int $abstractViews, int $pdfViews, int $htmlViews, int $otherViews): array
{
$galleyViews = $pdfViews + $htmlViews + $otherViews;
// Get basic submission details for display
$submission = Repo::submission()->get($submissionId);
$submissionProps = Repo::submission()->getSchemaMap()->mapToStats($submission);
return [
'abstractViews' => $abstractViews,
'galleyViews' => $galleyViews,
'pdfViews' => $pdfViews,
'htmlViews' => $htmlViews,
'otherViews' => $otherViews,
'publication' => $submissionProps,
];
}
/**
* Get CSV row with file metrics
*/
protected function getFilesCSVItem(int $submissionFileId, int $downloads, int $assocType, int $submissionId, string $submissionTitle): array
{
// Get submission file title for display
$submissionFile = Repo::submissionFile()->get($submissionFileId);
$title = $submissionFile->getLocalizedData('name');
$type = $assocType == Application::ASSOC_TYPE_SUBMISSION_FILE ? __('stats.file.type.primaryFile') : __('stats.file.type.suppFile');
return [
$submissionId,
$submissionTitle,
$submissionFileId,
$title,
$type,
$downloads
];
}
/**
* Get JSON data with file metrics
*/
protected function getFilesJSONItem(int $submissionFileId, int $downloads, int $assocType, int $submissionId, string $submissionTitle): array
{
// Get submission file title for display
$submissionFile = Repo::submissionFile()->get($submissionFileId);
$title = $submissionFile->getLocalizedData('name');
$type = $assocType == Application::ASSOC_TYPE_SUBMISSION_FILE ? __('stats.file.type.primaryFile') : __('stats.file.type.suppFile');
return [
'submissionId' => $submissionId,
'submissionTitle' => $submissionTitle,
'submissionFileId' => $submissionFileId,
'fileName' => $title,
'fileType' => $type,
'downloads' => $downloads
];
}
/**
* Get CSV row with geographical (country, region, and/or city) metrics
*/
protected function getGeoCSVItem(int $metric, int $metric_unique, string $country, ?string $region = null, ?string $city = null): array
{
$item = [];
if (isset($city)) {
$item[] = $city;
}
if (isset($region)) {
$item[] = $region;
}
return array_merge($item, [$country, $metric, $metric_unique]);
}
/**
* Get JSON data with geographical (country, region, and/or city) metrics
*/
protected function getGeoJSONItem(int $metric, int $metric_unique, string $country, ?string $region = null, ?string $city = null): array
{
$item = [];
if (isset($city)) {
$item['city'] = $city;
}
if (isset($region)) {
$item['region'] = $region;
}
return array_merge(
$item,
[
'country' => $country,
'total' => $metric,
'unique' => $metric_unique
]
);
}
/**
* Check if the timeline interval is valid
*/
protected function isValidTimelineInterval(string $interval): bool
{
return in_array($interval, [
StatisticsHelper::STATISTICS_DIMENSION_DAY,
StatisticsHelper::STATISTICS_DIMENSION_MONTH
]);
}
}
@@ -0,0 +1,358 @@
<?php
/**
* @file api/v1/stats/sushi/PKPStatsSushiHandler.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPStatsSushiHandler
*
* @ingroup api_v1_stats
*
* @brief Handle API requests for COUNTER R5 SUSHI statistics.
*
*/
namespace PKP\API\v1\stats\sushi;
use APP\core\Application;
use APP\facades\Repo;
use APP\sushi\PR;
use APP\sushi\PR_P1;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\security\authorization\ContextRequiredPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\sushi\CounterR5Report;
use PKP\sushi\SushiException;
use PKP\validation\ValidatorFactory;
use Slim\Http\Request as SlimHttpRequest;
class PKPStatsSushiHandler extends APIHandler
{
/** @var bool Whether the API is public */
public $isPublic = true;
/**
* Constructor
*/
public function __construct()
{
$site = Application::get()->getRequest()->getSite();
$context = Application::get()->getRequest()->getContext();
if (($site->getData('isSushiApiPublic') !== null && !$site->getData('isSushiApiPublic')) ||
($context->getData('isSushiApiPublic') !== null && !$context->getData('isSushiApiPublic'))) {
$this->isPublic = false;
}
$this->_handlerPath = 'stats/sushi';
$roles = $this->isPublic ? null : [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER];
$this->_endpoints = [
'GET' => $this->getGETDefinitions($roles)
];
parent::__construct();
}
/**
* Get this API's endpoints definitions
*/
protected function getGETDefinitions(array $roles = null): array
{
return [
[
'pattern' => $this->getEndpointPattern() . '/status',
'handler' => [$this, 'getStatus'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/members',
'handler' => [$this, 'getMembers'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/reports',
'handler' => [$this, 'getReports'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/reports/pr',
'handler' => [$this, 'getReportsPR'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/reports/pr_p1',
'handler' => [$this, 'getReportsPR1'],
'roles' => $roles
],
];
}
/**
* @copydoc PKPHandler::authorize()
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new ContextRequiredPolicy($request));
if (!$this->isPublic) {
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
}
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get the current status of the reporting service
*/
public function getStatus(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$request = $this->getRequest();
$context = $request->getContext();
// use only the name in the context primary locale to be consistent
$contextName = $context->getName($context->getPrimaryLocale());
return $response->withJson([
'Description' => __('sushi.status.description', ['contextName' => $contextName]),
'Service_Active' => true,
], 200);
}
/**
* Get the list of consortium members related to a Customer_ID
*/
public function getMembers(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$request = $this->getRequest();
$context = $request->getContext();
$site = $request->getSite();
$params = $slimRequest->getQueryParams();
if (!isset($params['customer_id'])) {
// error: missing required customer_id
return $response->withJson([
'Code' => 1030,
'Severity' => 'Fatal',
'Message' => 'Insufficient Information to Process Request',
'Data' => __('sushi.exception.1030.missing', ['params' => 'customer_id'])
], 400);
}
$platformId = $context->getPath();
if ($site->getData('isSiteSushiPlatform')) {
$platformId = $site->getData('sushiPlatformID');
}
$institutionName = $institutionId = null;
$customerId = $params['customer_id'];
if (is_numeric($customerId)) {
$customerId = (int) $customerId;
if ($customerId == 0) {
$institutionName = 'The World';
} else {
$institution = Repo::institution()->get($customerId);
if (isset($institution) && $institution->getContextId() == $context->getId()) {
$institutionId = [];
$institutionName = $institution->getLocalizedName();
if (!empty($institution->getROR())) {
$institutionId[] = ['Type' => 'ROR', 'Value' => $institution->getROR()];
}
$institutionId[] = ['Type' => 'Proprietary', 'Value' => $platformId . ':' . $customerId];
}
}
}
if (!isset($institutionName)) {
// error: invalid customer_id
return $response->withJson([
'Code' => 1030,
'Severity' => 'Fatal',
'Message' => 'Insufficient Information to Process Request',
'Data' => __('sushi.exception.1030.invalid', ['params' => 'customer_id'])
], 400);
}
$item = [
'Customer_ID' => $customerId,
'Name' => $institutionName,
];
if (isset($institutionId)) {
$item['Institution_ID'] = $institutionId;
}
return $response->withJson([$item], 200);
}
/**
* Get list of reports supported by the API
*/
public function getReports(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$items = $this->getReportList();
return $response->withJson($items, 200);
}
/**
* Get the application specific list of reports supported by the API
*/
protected function getReportList(): array
{
return [
[
'Report_Name' => 'Platform Master Report',
'Report_ID' => 'PR',
'Release' => '5',
'Report_Description' => __('sushi.reports.pr.description'),
'Path' => 'reports/pr'
],
[
'Report_Name' => 'Platform Usage',
'Report_ID' => 'PR_P1',
'Release' => '5',
'Report_Description' => __('sushi.reports.pr_p1.description'),
'Path' => 'reports/pr_p1'
],
];
}
/**
* COUNTER 'Platform Usage' [PR_P1].
* A customizable report summarizing activity across the Platform (journal, press, or server).
*/
public function getReportsPR(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
return $this->getReportResponse(new PR(), $slimRequest, $response, $args);
}
/**
* COUNTER 'Platform Master Report' [PR].
* This is a Standard View of the Platform Master Report that presents usage for the overall Platform broken down by Metric_Type
*/
public function getReportsPR1(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
return $this->getReportResponse(new PR_P1(), $slimRequest, $response, $args);
}
/** Validate user input for TSV reports */
protected function _validateUserInput(CounterR5Report $report, array $params): array
{
$request = $this->getRequest();
$context = $request->getContext();
$earliestDate = CounterR5Report::getEarliestDate();
$lastDate = CounterR5Report::getLastDate();
$submissionIds = Repo::submission()->getCollector()->filterByContextIds([$context->getId()])->getIds()->implode(',');
$rules = [
'begin_date' => [
'regex:/^\d{4}-\d{2}(-\d{2})?$/',
'after_or_equal:' . $earliestDate,
'before_or_equal:end_date',
],
'end_date' => [
'regex:/^\d{4}-\d{2}(-\d{2})?$/',
'before_or_equal:' . $lastDate,
'after_or_equal:begin_date',
],
'item_id' => [
// TO-ASK: shell this rather be just validation for positive integer?
'in:' . $submissionIds,
],
'yop' => [
'regex:/^\d{4}((\||-)\d{4})*$/',
],
];
$reportId = $report->getID();
if (in_array($reportId, ['PR', 'TR', 'IR'])) {
$rules['metric_type'] = ['required'];
}
$errors = [];
$validator = ValidatorFactory::make(
$params,
$rules,
[
'begin_date.regex' => __(
'manager.statistics.counterR5Report.settings.wrongDateFormat'
),
'end_date.regex' => __(
'manager.statistics.counterR5Report.settings.wrongDateFormat'
),
'begin_date.after_or_equal' => __(
'stats.dateRange.invalidStartDateMin'
),
'end_date.before_or_equal' => __(
'stats.dateRange.invalidEndDateMax'
),
'begin_date.before_or_equal' => __(
'stats.dateRange.invalidDateRange'
),
'end_date.after_or_equal' => __(
'stats.dateRange.invalidDateRange'
),
'item_id.*' => __(
'manager.statistics.counterR5Report.settings.wrongItemId'
),
'yop.regex' => __(
'manager.statistics.counterR5Report.settings.wrongYOPFormat'
),
]
);
if ($validator->fails()) {
$errors = $validator->errors()->getMessages();
}
return $errors;
}
/**
* Get the requested report
*/
protected function getReportResponse(CounterR5Report $report, SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseTSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_TSV) ? true : false;
$params = $slimRequest->getQueryParams();
if ($responseTSV) {
$errors = $this->_validateUserInput($report, $params);
if (!empty($errors)) {
return $response->withJson($errors, 400);
}
}
try {
$report->processReportParams($this->getRequest(), $params);
} catch (SushiException $e) {
return $response->withJson($e->getResponseData(), $e->getHttpStatusCode());
}
if ($responseTSV) {
$reportHeader = $report->getTSVReportHeader();
$reportColumnNames = $report->getTSVColumnNames();
$reportItems = $report->getTSVReportItems();
// consider 3030 error (no usage available)
$key = array_search('3030', array_column($report->warnings, 'Code'));
if ($key !== false) {
$error = $report->warnings[$key]['Code'] . ':' . $report->warnings[$key]['Message'] . '(' . $report->warnings[$key]['Data'] . ')';
foreach ($reportHeader as &$headerRow) {
if (in_array('Exceptions', $headerRow)) {
$headerRow[1] =
$headerRow[1] == '' ?
$error :
$headerRow[1] . ';' . $error;
}
}
}
$report = array_merge($reportHeader, [['']], $reportColumnNames, $reportItems);
return $response->withCSV($report, [], count($reportItems), APIResponse::RESPONSE_TSV);
}
$reportHeader = $report->getReportHeader();
$reportItems = $report->getReportItems();
return $response->withJson([
'Report_Header' => $reportHeader,
'Report_Items' => $reportItems,
], 200);
}
}
@@ -0,0 +1,130 @@
<?php
/**
* @file api/v1/stats/users/PKPStatsUserHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPStatsUserHandler
*
* @ingroup api_v1_stats
*
* @brief Handle API requests for publication statistics.
*
*/
namespace PKP\API\v1\stats\users;
use APP\facades\Repo;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
class PKPStatsUserHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'stats/users';
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'get'],
'roles' => [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR],
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize()
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get user stats
*
* Returns the count of users broken down by roles
*
* @param \Slim\Http\Request $slimRequest Slim request object
* @param object $response Response
* @param array $args
*
* @return object Response
*/
public function get($slimRequest, $response, $args)
{
$request = $this->getRequest();
if (!$request->getContext()) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$collector = Repo::user()->getCollector();
$dateParams = [];
foreach ($slimRequest->getQueryParams() as $param => $value) {
switch ($param) {
case 'registeredAfter':
$collector->filterRegisteredAfter($value);
$dateParams['dateStart'] = $value;
break;
case 'registeredBefore':
$collector->filterRegisteredBefore($value);
$dateParams['dateEnd'] = $value;
break;
case 'status': switch ($value) {
case 'disabled':
$collector->filterByStatus($collector::STATUS_DISABLED);
break;
case 'all':
$collector->filterByStatus($collector::STATUS_ALL);
break;
default:
case 'active':
$collector->filterByStatus($collector::STATUS_ACTIVE);
break;
}
}
}
Hook::call('API::stats::users::params', [$collector, $slimRequest]);
$collector->filterByContextIds([$request->getContext()->getId()]);
$result = $this->_validateStatDates($dateParams);
if ($result !== true) {
return $response->withStatus(400)->withJsonError($result);
}
return $response->withJson(array_map(
function ($item) {
$item['name'] = __($item['name']);
return $item;
},
Repo::user()->getRolesOverview($collector)
));
}
}
@@ -0,0 +1,590 @@
<?php
/**
* @file api/v1/submissions/PKPSubmissionFileHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPSubmissionFileHandler
*
* @ingroup api_v1_submission
*
* @brief Handle API requests for submission operations.
*
*/
namespace PKP\API\v1\submissions;
use APP\core\Application;
use APP\core\Services;
use APP\facades\Repo;
use PKP\core\APIResponse;
use PKP\db\DAORegistry;
use PKP\file\FileManager;
use PKP\handler\APIHandler;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\internal\SubmissionFileStageAccessPolicy;
use PKP\security\authorization\SubmissionAccessPolicy;
use PKP\security\authorization\SubmissionFileAccessPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\services\PKPSchemaService;
use PKP\submission\GenreDAO;
use PKP\submission\reviewRound\ReviewRoundDAO;
use PKP\submissionFile\SubmissionFile;
class PKPSubmissionFileHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'submissions/{submissionId:\d+}/files';
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_AUTHOR],
],
[
'pattern' => $this->getEndpointPattern() . '/{submissionFileId:\d+}',
'handler' => [$this, 'get'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_AUTHOR],
],
],
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'add'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_AUTHOR],
],
],
'PUT' => [
[
'pattern' => $this->getEndpointPattern() . '/{submissionFileId:\d+}',
'handler' => [$this, 'edit'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_AUTHOR],
],
[
'pattern' => $this->getEndpointPattern() . '/{submissionFileId:\d+}/copy',
'handler' => [$this, 'copy'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_SUB_EDITOR],
],
],
'DELETE' => [
[
'pattern' => $this->getEndpointPattern() . '/{submissionFileId:\d+}',
'handler' => [$this, 'delete'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_AUTHOR],
],
],
];
parent::__construct();
}
//
// Implement methods from PKPHandler
//
public function authorize($request, &$args, $roleAssignments)
{
$route = $this->getSlimRequest()->getAttribute('route');
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
$this->addPolicy(new SubmissionAccessPolicy($request, $args, $roleAssignments));
if ($route->getName() === 'add') {
$params = $this->getSlimRequest()->getParsedBody();
$fileStage = isset($params['fileStage']) ? (int) $params['fileStage'] : 0;
$this->addPolicy(new SubmissionFileStageAccessPolicy($fileStage, SubmissionFileAccessPolicy::SUBMISSION_FILE_ACCESS_MODIFY, 'api.submissionFiles.403.unauthorizedFileStageIdWrite'));
} elseif ($route->getName() === 'getMany') {
// Anyone passing SubmissionAccessPolicy is allowed to access getMany,
// but the endpoint will return different files depending on the user's
// stage assignments.
} else {
$accessMode = $this->getSlimRequest()->getMethod() === 'GET'
? SubmissionFileAccessPolicy::SUBMISSION_FILE_ACCESS_READ
: SubmissionFileAccessPolicy::SUBMISSION_FILE_ACCESS_MODIFY;
$this->addPolicy(new SubmissionFileAccessPolicy($request, $args, $roleAssignments, $accessMode, (int) $route->getArgument('submissionFileId')));
}
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a collection of submission files
*
* @param \Slim\Http\Request $slimRequest
* @param APIResponse $response
* @param array $args arguments
*
* @return APIResponse
*/
public function getMany($slimRequest, $response, $args)
{
$request = $this->getRequest();
$params = [];
foreach ($slimRequest->getQueryParams() as $param => $val) {
switch ($param) {
case 'fileStages':
case 'reviewRoundIds':
if (is_string($val)) {
$val = explode(',', $val);
} elseif (!is_array($val)) {
$val = [$val];
}
$params[$param] = array_map('intval', $val);
break;
}
}
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
$stageAssignments = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ACCESSIBLE_WORKFLOW_STAGES);
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
// @see PKP\submissionFile\Repository::getAssignedFileStages() for excluded file stages
$allowedFileStages = [
SubmissionFile::SUBMISSION_FILE_SUBMISSION,
SubmissionFile::SUBMISSION_FILE_REVIEW_FILE,
SubmissionFile::SUBMISSION_FILE_FINAL,
SubmissionFile::SUBMISSION_FILE_COPYEDIT,
SubmissionFile::SUBMISSION_FILE_PROOF,
SubmissionFile::SUBMISSION_FILE_PRODUCTION_READY,
SubmissionFile::SUBMISSION_FILE_ATTACHMENT,
SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION,
SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_FILE,
SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_REVISION,
];
// Managers can access files for submissions they are not assigned to
if (!$stageAssignments && !count(array_intersect([Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN], $userRoles))) {
return $response->withStatus(403)->withJsonError('api.403.unauthorized');
}
// Set the allowed file stages based on stage assignment
// @see PKP\submissionFile\Repository::getAssignedFileStages() for excluded file stages
if ($stageAssignments) {
$allowedFileStages = Repo::submissionFile()
->getAssignedFileStages(
$stageAssignments,
SubmissionFileAccessPolicy::SUBMISSION_FILE_ACCESS_READ
);
}
$fileStages = empty($params['fileStages'])
? $allowedFileStages
: $params['fileStages'];
foreach ($fileStages as $fileStage) {
if (!in_array($fileStage, $allowedFileStages)) {
return $response->withStatus(403)->withJsonError('api.submissionFiles.403.unauthorizedFileStageId');
}
}
$collector = Repo::submissionFile()
->getCollector()
->filterBySubmissionIds([$submission->getId()])
->filterByFileStages($fileStages);
// Filter by requested review round ids
if (!empty($params['reviewRoundIds'])) {
$reviewRoundIds = $params['reviewRoundIds'];
$allowedReviewRoundIds = [];
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao*/
if (!empty(array_intersect([SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_FILE, SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_REVISION], $fileStages))) {
$result = $reviewRoundDao->getBySubmissionId($submission->getId(), WORKFLOW_STAGE_ID_INTERNAL_REVIEW);
while ($reviewRound = $result->next()) {
$allowedReviewRoundIds[] = $reviewRound->getId();
}
}
if (!empty(array_intersect([SubmissionFile::SUBMISSION_FILE_REVIEW_FILE, SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION], $fileStages))) {
$result = $reviewRoundDao->getBySubmissionId($submission->getId(), WORKFLOW_STAGE_ID_EXTERNAL_REVIEW);
while ($reviewRound = $result->next()) {
$allowedReviewRoundIds[] = $reviewRound->getId();
}
}
foreach ($reviewRoundIds as $reviewRoundId) {
if (!in_array($reviewRoundId, $allowedReviewRoundIds)) {
return $response->withStatus(403)->withJsonError('api.submissionFiles.403.unauthorizedReviewRound');
}
}
$collector->filterByReviewRoundIds($reviewRoundIds);
}
$files = $collector->getMany();
$items = Repo::submissionFile()
->getSchemaMap()
->summarizeMany($files, $this->getFileGenres());
$data = [
'itemsMax' => $files->count(),
'items' => $items->values(),
];
return $response->withJson($data, 200);
}
/**
* Get a single submission file
*
* @param \Slim\Http\Request $slimRequest
* @param APIResponse $response
* @param array $args arguments
*
* @return APIResponse
*/
public function get($slimRequest, $response, $args)
{
$submissionFile = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION_FILE);
$data = Repo::submissionFile()
->getSchemaMap()
->map($submissionFile, $this->getFileGenres());
return $response->withJson($data, 200);
}
/**
* Add a new submission file
*
* @param \Slim\Http\Request $slimRequest
* @param APIResponse $response
* @param array $args arguments
*
* @return APIResponse
*/
public function add($slimRequest, $response, $args)
{
$request = $this->getRequest();
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
if (empty($_FILES)) {
return $response->withStatus(400)->withJsonError('api.files.400.noUpload');
}
if ($_FILES['file']['error'] !== UPLOAD_ERR_OK) {
return $this->getUploadErrorResponse($response, $_FILES['file']['error']);
}
$fileManager = new FileManager();
$extension = $fileManager->parseFileExtension($_FILES['file']['name']);
$submissionDir = Repo::submissionFile()
->getSubmissionDir(
$request->getContext()->getId(),
$submission->getId()
);
$fileId = Services::get('file')->add(
$_FILES['file']['tmp_name'],
$submissionDir . '/' . uniqid() . '.' . $extension
);
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_SUBMISSION_FILE, $slimRequest->getParsedBody());
$params['fileId'] = $fileId;
$params['submissionId'] = $submission->getId();
$params['uploaderUserId'] = (int) $request->getUser()->getId();
$primaryLocale = $request->getContext()->getPrimaryLocale();
$allowedLocales = $request->getContext()->getData('supportedSubmissionLocales');
// Set the name if not passed with the request
if (empty($params['name'])) {
$params['name'][$primaryLocale] = $_FILES['file']['name'];
}
// If no genre has been set and there is only one genre possible, set it automatically
if (empty($params['genreId'])) {
/** @var GenreDAO */
$genreDao = DAORegistry::getDAO('GenreDAO');
$genres = $genreDao->getEnabledByContextId($request->getContext()->getId());
[$firstGenre, $secondGenre] = [$genres->next(), $genres->next()];
if ($firstGenre && !$secondGenre) {
$params['genreId'] = $firstGenre->getId();
}
}
$errors = Repo::submissionFile()
->validate(
null,
$params,
$allowedLocales,
$primaryLocale
);
if (!empty($errors)) {
Services::get('file')->delete($fileId);
return $response->withStatus(400)->withJson($errors);
}
// Review attachments and discussion files can not be uploaded through this API endpoint
$notAllowedFileStages = [
SubmissionFile::SUBMISSION_FILE_NOTE,
SubmissionFile::SUBMISSION_FILE_REVIEW_ATTACHMENT,
SubmissionFile::SUBMISSION_FILE_QUERY,
];
if (in_array($params['fileStage'], $notAllowedFileStages)) {
Services::get('file')->delete($fileId);
return $response->withStatus(400)->withJsonError('api.submissionFiles.403.unauthorizedFileStageIdWrite');
}
// A valid review round is required when uploading to a review file stage
$reviewFileStages = [
SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_FILE,
SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_REVISION,
SubmissionFile::SUBMISSION_FILE_REVIEW_FILE,
SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION,
];
if (in_array($params['fileStage'], $reviewFileStages)) {
if (empty($params['assocType']) || $params['assocType'] !== Application::ASSOC_TYPE_REVIEW_ROUND || empty($params['assocId'])) {
Services::get('file')->delete($fileId);
return $response->withStatus(400)->withJsonError('api.submissionFiles.400.missingReviewRoundAssocType');
}
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao */
$reviewRound = $reviewRoundDao->getById($params['assocId']);
$stageId = in_array($params['fileStage'], [SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_FILE, SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_REVISION])
? WORKFLOW_STAGE_ID_INTERNAL_REVIEW
: WORKFLOW_STAGE_ID_EXTERNAL_REVIEW;
if (!$reviewRound
|| $reviewRound->getData('submissionId') != $params['submissionId']
|| $reviewRound->getData('stageId') != $stageId) {
Services::get('file')->delete($fileId);
return $response->withStatus(400)->withJsonError('api.submissionFiles.400.reviewRoundSubmissionNotMatch');
}
}
$submissionFile = Repo::submissionFile()
->newDataObject($params);
$submissionFileId = Repo::submissionFile()
->add($submissionFile);
$submissionFile = Repo::submissionFile()
->get($submissionFileId);
$data = Repo::submissionFile()
->getSchemaMap()
->map($submissionFile, $this->getFileGenres());
return $response->withJson($data, 200);
}
/**
* Edit a submission file
*
* @param \Slim\Http\Request $slimRequest
* @param APIResponse $response
* @param array $args arguments
*
* @return APIResponse
*/
public function edit($slimRequest, $response, $args)
{
$request = $this->getRequest();
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
$submissionFile = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION_FILE);
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_SUBMISSION_FILE, $slimRequest->getParsedBody());
// Don't allow these properties to be modified
unset($params['submissionId'], $params['fileId'], $params['uploaderUserId']);
if (empty($params) && empty($_FILES['file'])) {
return $response->withStatus(400)->withJsonError('api.submissionsFiles.400.noParams');
}
$primaryLocale = $request->getContext()->getPrimaryLocale();
$allowedLocales = $request->getContext()->getData('supportedSubmissionLocales');
$errors = Repo::submissionFile()
->validate(
$submissionFile,
$params,
$allowedLocales,
$primaryLocale
);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
// Upload a new file
if (!empty($_FILES['file'])) {
if ($_FILES['file']['error'] !== UPLOAD_ERR_OK) {
return $this->getUploadErrorResponse($response, $_FILES['file']['error']);
}
$fileManager = new FileManager();
$extension = $fileManager->parseFileExtension($_FILES['file']['name']);
$submissionDir = Repo::submissionFile()
->getSubmissionDir(
$request->getContext()->getId(),
$submission->getId()
);
$fileId = Services::get('file')->add(
$_FILES['file']['tmp_name'],
$submissionDir . '/' . uniqid() . '.' . $extension
);
$params['fileId'] = $fileId;
$params['uploaderUserId'] = $request->getUser()->getId();
if (empty($params['name'])) {
$params['name'][$primaryLocale] = $_FILES['file']['name'];
}
}
Repo::submissionFile()
->edit(
$submissionFile,
$params
);
$submissionFile = Repo::submissionFile()
->get($submissionFile->getId());
$data = Repo::submissionFile()
->getSchemaMap()
->map($submissionFile, $this->getFileGenres());
return $response->withJson($data, 200);
}
/**
* Copy a submission file to another file stage
*
* @param \Slim\Http\Request $slimRequest
* @param APIResponse $response
* @param array $args arguments
*
* @return APIResponse
*/
public function copy($slimRequest, $response, $args)
{
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
$submissionFile = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION_FILE);
$params = $slimRequest->getParsedBody();
if (empty($params['toFileStage'])) {
return $response->withStatus(400)->withJsonError('api.submissionFiles.400.noFileStageId');
}
$toFileStage = (int) $params['toFileStage'];
if (!in_array($toFileStage, Repo::submissionFile()->getFileStages())) {
return $response->withStatus(400)->withJsonError('api.submissionFiles.400.invalidFileStage');
}
// Expect a review round id when copying to a review stage, or use the latest
// round in that stage by default
$reviewRoundId = null;
if (in_array($toFileStage, Repo::submissionFile()->reviewFileStages)) {
if (!empty($params['reviewRoundId'])) {
$reviewRoundId = (int) $params['reviewRoundId'];
/** @var ReviewRoundDAO $reviewRoundDao */
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO');
$reviewRound = $reviewRoundDao->getById($reviewRoundId);
if (!$reviewRound || $reviewRound->getSubmissionId() != $submission->getId()) {
return $response->withStatus(400)->withJsonError('api.submissionFiles.400.reviewRoundSubmissionNotMatch');
}
} else {
// Use the latest review round of the appropriate stage
$stageId = in_array($toFileStage, SubmissionFile::INTERNAL_REVIEW_STAGES)
? WORKFLOW_STAGE_ID_INTERNAL_REVIEW
: WORKFLOW_STAGE_ID_EXTERNAL_REVIEW;
/** @var ReviewRoundDAO $reviewRoundDao */
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO');
$reviewRound = $reviewRoundDao->getLastReviewRoundBySubmissionId($submission->getId(), $stageId);
if ($reviewRound) {
$reviewRoundId = $reviewRound->getId();
}
}
if ($reviewRoundId === null) {
return $response->withStatus(400)->withJsonError('api.submissionFiles.400.reviewRoundIdRequired');
}
}
$newSubmissionFileId = Repo::submissionFile()->copy(
$submissionFile,
$toFileStage,
$reviewRoundId
);
$newSubmissionFile = Repo::submissionFile()->get($newSubmissionFileId);
$data = Repo::submissionFile()
->getSchemaMap()
->map($newSubmissionFile, $this->getFileGenres());
return $response->withJson($data, 200);
}
/**
* Delete a submission file
*
* @param \Slim\Http\Request $slimRequest
* @param APIResponse $response
* @param array $args arguments
*
* @return APIResponse
*/
public function delete($slimRequest, $response, $args)
{
$submissionFile = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION_FILE);
$data = Repo::submissionFile()
->getSchemaMap()
->map($submissionFile, $this->getFileGenres());
Repo::submissionFile()->delete($submissionFile);
return $response->withJson($data, 200);
}
/**
* Helper method to get the file genres for the current context
*
* @return \PKP\submission\Genre[]
*/
protected function getFileGenres(): array
{
/** @var GenreDAO $genreDao */
$genreDao = DAORegistry::getDAO('GenreDAO');
return $genreDao->getByContextId($this->getRequest()->getContext()->getId())->toArray();
}
/**
* Helper method to get the appropriate response when an error
* has occurred during a file upload
*
* @param APIResponse $response
* @param int $error One of the UPLOAD_ERR_ constants
*
* @return APIResponse
*/
private function getUploadErrorResponse($response, $error)
{
switch ($error) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
return $response->withStatus(400)->withJsonError('api.files.400.fileSize', ['maxSize' => Application::getReadableMaxFileSize()]);
case UPLOAD_ERR_PARTIAL:
return $response->withStatus(400)->withJsonError('api.files.400.uploadFailed');
case UPLOAD_ERR_NO_FILE:
return $response->withStatus(400)->withJsonError('api.files.400.noUpload');
case UPLOAD_ERR_NO_TMP_DIR:
case UPLOAD_ERR_CANT_WRITE:
case UPLOAD_ERR_EXTENSION:
return $response->withStatus(400)->withJsonError('api.files.400.config');
}
return $response->withStatus(400)->withJsonError('api.files.400.uploadFailed');
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,150 @@
<?php
/**
* @file api/v1/temporaryFiles/PKPTemporaryFilesHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPTemporaryFilesHandler
*
* @ingroup api_v1_users
*
* @brief Handle API requests to upload a file and receive a temporary file ID.
*/
namespace PKP\API\v1\temporaryFiles;
use APP\core\Application;
use APP\core\Services;
use PKP\file\TemporaryFileManager;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use Slim\Http\Request as SlimRequest;
class PKPTemporaryFilesHandler extends APIHandler
{
/**
* @copydoc APIHandler::__construct()
*/
public function __construct()
{
$this->_handlerPath = 'temporaryFiles';
$roles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_REVIEWER, Role::ROLE_ID_AUTHOR, Role::ROLE_ID_ASSISTANT];
$this->_endpoints = [
'OPTIONS' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getOptions'],
'roles' => $roles,
],
],
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'uploadFile'],
'roles' => $roles,
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* A helper method which adds the necessary response headers to allow
* file uploads
*
* @param APIResponse $response object
*
* @return APIResponse
*/
private function getResponse($response)
{
return $response->withHeader('Access-Control-Allow-Headers', 'Content-Type, X-Requested-With, X-PINGOTHER, X-File-Name, Cache-Control');
}
/**
* Upload a requested file
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function uploadFile($slimRequest, $response, $args)
{
$request = $this->getRequest();
if (empty($_FILES)) {
return $response->withStatus(400)->withJsonError('api.files.400.noUpload');
}
$temporaryFileManager = new TemporaryFileManager();
$fileName = $temporaryFileManager->getFirstUploadedPostName();
$uploadedFile = $temporaryFileManager->handleUpload($fileName, $request->getUser()->getId());
if ($uploadedFile === false) {
if ($temporaryFileManager->uploadError($fileName)) {
switch ($temporaryFileManager->getUploadErrorCode($fileName)) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
return $response->withStatus(400)->withJsonError('api.files.400.fileSize', ['maxSize' => Application::getReadableMaxFileSize()]);
case UPLOAD_ERR_PARTIAL:
return $response->withStatus(400)->withJsonError('api.files.400.uploadFailed');
case UPLOAD_ERR_NO_FILE:
return $response->withStatus(400)->withJsonError('api.files.400.noUpload');
case UPLOAD_ERR_NO_TMP_DIR:
case UPLOAD_ERR_CANT_WRITE:
case UPLOAD_ERR_EXTENSION:
return $response->withStatus(400)->withJsonError('api.files.400.config');
}
}
return $response->withStatus(400)->withJsonError('api.files.400.uploadFailed');
}
return $this->getResponse($response->withJson([
'id' => $uploadedFile->getId(),
'name' => $uploadedFile->getData('originalFileName'),
'mimetype' => $uploadedFile->getData('filetype'),
'documentType' => Services::get('file')->getDocumentType($uploadedFile->getData('filetype')),
]));
}
/**
* Respond affirmatively to a HTTP OPTIONS request with headers which allow
* file uploads
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function getOptions($slimRequest, $response, $args)
{
return $this->getResponse($response);
}
}
+377
View File
@@ -0,0 +1,377 @@
<?php
/**
* @file api/v1/users/PKPUserHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPUserHandler
*
* @ingroup api_v1_users
*
* @brief Base class to handle API requests for user operations.
*
*/
namespace PKP\API\v1\users;
use APP\facades\Repo;
use Exception;
use PKP\core\APIResponse;
use PKP\facades\Locale;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use Slim\Http\Request as SlimRequest;
class PKPUserHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'users';
$roles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR];
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/reviewers',
'handler' => [$this, 'getReviewers'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/{userId:\d+}',
'handler' => [$this, 'get'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/report',
'handler' => [$this, 'getReport'],
'roles' => $roles
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize()
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a collection of users
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function getMany($slimRequest, $response, $args)
{
$request = $this->getRequest();
$context = $request->getContext();
if (!$context) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$params = $this->_processAllowedParams($slimRequest->getQueryParams(), [
'assignedToCategory',
'assignedToSection',
'assignedToSubmission',
'assignedToSubmissionStage',
'count',
'offset',
'orderBy',
'orderDirection',
'roleIds',
'searchPhrase',
'status',
]);
$params['contextId'] = $context->getId();
Hook::call('API::users::params', [&$params, $slimRequest]);
$collector = Repo::user()->getCollector();
// Convert from $params array to what the Collector expects
$orderBy = null;
switch ($params['orderBy'] ?? 'id') {
case 'id': $orderBy = $collector::ORDERBY_ID;
break;
case 'givenName': $orderBy = $collector::ORDERBY_GIVENNAME;
break;
case 'familyName': $orderBy = $collector::ORDERBY_FAMILYNAME;
break;
default: throw new Exception('Unknown orderBy specified');
}
$orderDirection = null;
switch ($params['orderDirection'] ?? 'ASC') {
case 'ASC': $orderDirection = $collector::ORDER_DIR_ASC;
break;
case 'DESC': $orderDirection = $collector::ORDER_DIR_DESC;
break;
default: throw new Exception('Unknown orderDirection specified');
}
$collector->assignedTo($params['assignedToSubmission'] ?? null, $params['assignedToSubmissionStage'] ?? null)
->assignedToSectionIds(isset($params['assignedToSection']) ? [$params['assignedToSection']] : null)
->assignedToCategoryIds(isset($params['assignedToCategory']) ? [$params['assignedToCategory']] : null)
->filterByRoleIds($params['roleIds'] ?? null)
->searchPhrase($params['searchPhrase'] ?? null)
->orderBy($orderBy, $orderDirection, [Locale::getLocale(), $request->getSite()->getPrimaryLocale()])
->limit($params['count'] ?? null)
->offset($params['offset'] ?? null)
->filterByStatus($params['status'] ?? $collector::STATUS_ALL);
$users = $collector->getMany();
$map = Repo::user()->getSchemaMap();
$items = [];
foreach ($users as $user) {
$items[] = $map->summarize($user);
}
return $response->withJson([
'itemsMax' => $collector->limit(null)->offset(null)->getCount(),
'items' => $items,
], 200);
}
/**
* Get a single user
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function get($slimRequest, $response, $args)
{
$request = $this->getRequest();
if (!empty($args['userId'])) {
$user = Repo::user()->get($args['userId']);
}
if (!$user) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$data = Repo::user()->getSchemaMap()->map($user);
return $response->withJson($data, 200);
}
/**
* Get a collection of reviewers
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function getReviewers($slimRequest, $response, $args)
{
$request = $this->getRequest();
$context = $request->getContext();
if (!$context) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$params = $this->_processAllowedParams($slimRequest->getQueryParams(), [
'averageCompletion',
'count',
'daysSinceLastAssignment',
'offset',
'orderBy',
'orderDirection',
'reviewerRating',
'reviewsActive',
'reviewsCompleted',
'reviewStage',
'searchPhrase',
'reviewerIds',
'status',
]);
Hook::call('API::users::reviewers::params', [&$params, $slimRequest]);
$collector = Repo::user()->getCollector()
->filterByContextIds([$context->getId()])
->includeReviewerData()
->filterByRoleIds([Role::ROLE_ID_REVIEWER])
->filterByWorkflowStageIds([$params['reviewStage']])
->searchPhrase($params['searchPhrase'] ?? null)
->filterByReviewerRating($params['reviewerRating'] ?? null)
->filterByReviewsCompleted($params['reviewsCompleted'][0] ?? null)
->filterByReviewsActive(...($params['reviewsActive'] ?? []))
->filterByDaysSinceLastAssignment(...($params['daysSinceLastAssignment'] ?? []))
->filterByAverageCompletion($params['averageCompletion'][0] ?? null)
->filterByUserIds($params['reviewerIds'] ?? null)
->limit($params['count'] ?? null)
->offset($params['offset'] ?? null);
$usersCollection = $collector->getMany();
$items = [];
$map = Repo::user()->getSchemaMap();
foreach ($usersCollection as $user) {
$items[] = $map->summarizeReviewer($user);
}
return $response->withJson([
'itemsMax' => $collector->limit(null)->offset(null)->getCount(),
'items' => $items,
], 200);
}
/**
* Convert the query params passed to the end point. Exclude unsupported
* params and coerce the type of those passed.
*
* @param array $params Key/value of request params
* @param array $allowedKeys The param keys which should be processed and returned
*
* @return array
*/
private function _processAllowedParams($params, $allowedKeys)
{
// Merge query params over default params
$defaultParams = [
'count' => 20,
'offset' => 0,
];
$requestParams = array_merge($defaultParams, $params);
// Process query params to format incoming data as needed
$returnParams = [];
foreach ($requestParams as $param => $val) {
if (!in_array($param, $allowedKeys)) {
continue;
}
switch ($param) {
case 'orderBy':
if (in_array($val, ['id', 'familyName', 'givenName'])) {
$returnParams[$param] = $val;
}
break;
case 'orderDirection':
$returnParams[$param] = $val === 'ASC' ? $val : 'DESC';
break;
case 'status':
if (in_array($val, ['all', 'active', 'disabled'])) {
$returnParams[$param] = $val;
}
break;
// Always convert roleIds to array
case 'reviewerIds':
case 'roleIds':
if (is_string($val)) {
$val = explode(',', $val);
} elseif (!is_array($val)) {
$val = [$val];
}
$returnParams[$param] = array_map('intval', $val);
break;
case 'assignedToCategory':
case 'assignedToSection':
case 'assignedToSubmissionStage':
case 'assignedToSubmission':
case 'reviewerRating':
case 'reviewStage':
case 'offset':
case 'searchPhrase':
$returnParams[$param] = trim($val);
break;
case 'reviewsCompleted':
case 'reviewsActive':
case 'daysSinceLastAssignment':
case 'averageCompletion':
if (is_array($val)) {
$val = array_map('intval', $val);
} elseif (strpos($val, '-') !== false) {
$val = array_map('intval', explode('-', $val));
} else {
$val = [(int) $val];
}
$returnParams[$param] = $val;
break;
// Enforce a maximum count per request
case 'count':
$returnParams[$param] = min(100, (int) $val);
break;
}
}
return $returnParams;
}
/**
* Retrieve the user report
*/
public function getReport(SlimRequest $slimRequest, APIResponse $response, array $args): ?APIResponse
{
$request = $this->getRequest();
$context = $request->getContext();
if (!$context) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$params = ['contextIds' => [$context->getId()]];
foreach ($slimRequest->getQueryParams() as $param => $value) {
switch ($param) {
case 'userGroupIds':
if (is_string($value) && str_contains($value, ',')) {
$value = explode(',', $value);
} elseif (!is_array($value)) {
$value = [$value];
}
$params[$param] = array_map('intval', $value);
break;
case 'mappings':
if (is_string($value) && str_contains($value, ',')) {
$value = explode(',', $value);
} elseif (!is_array($value)) {
$value = [$value];
}
$params[$param] = $value;
break;
}
}
Hook::call('API::users::user::report::params', [&$params, $slimRequest]);
$this->getApp()->getContainer()->get('settings')->replace(['outputBuffering' => false]);
$report = Repo::user()->getReport($params);
header('content-type: text/comma-separated-values');
header('content-disposition: attachment; filename="user-report-' . date('Y-m-d') . '.csv"');
$report->serialize(fopen('php://output', 'w+'));
exit;
}
}
+133
View File
@@ -0,0 +1,133 @@
<?php
/**
* @file api/v1/vocabs/PKPVocabHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPVocabHandler
*
* @ingroup api_v1_vocab
*
* @brief Handle API requests for controlled vocab operations.
*
*/
namespace PKP\API\v1\vocabs;
use APP\core\Application;
use PKP\core\APIResponse;
use PKP\core\PKPString;
use PKP\db\DAORegistry;
use PKP\facades\Locale;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\submission\SubmissionAgencyDAO;
use PKP\submission\SubmissionDisciplineDAO;
use PKP\submission\SubmissionKeywordDAO;
use PKP\submission\SubmissionLanguageDAO;
use PKP\submission\SubmissionSubjectDAO;
use Slim\Http\Request;
use Stringy\Stringy;
class PKPVocabHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'vocabs';
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_AUTHOR],
],
],
];
parent::__construct();
}
//
// Implement methods from PKPHandler
//
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get the controlled vocab entries available in this context
*/
public function getMany(Request $slimRequest, APIResponse $response, array $args): APIResponse
{
$request = Application::get()->getRequest();
$context = $request->getContext();
if (!$context) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$requestParams = $slimRequest->getQueryParams();
$vocab = $requestParams['vocab'] ?? '';
$locale = $requestParams['locale'] ?? Locale::getLocale();
$term = $requestParams['term'] ?? null;
if (!in_array($locale, $context->getData('supportedSubmissionLocales'))) {
return $response->withStatus(400)->withJsonError('api.vocabs.400.localeNotSupported', ['locale' => $locale]);
}
switch ($vocab) {
case SubmissionKeywordDAO::CONTROLLED_VOCAB_SUBMISSION_KEYWORD:
$submissionKeywordEntryDao = DAORegistry::getDAO('SubmissionKeywordEntryDAO'); /** @var \PKP\submission\SubmissionKeywordEntryDAO $submissionKeywordEntryDao */
$entries = $submissionKeywordEntryDao->getByContextId($vocab, $context->getId(), $locale, $term)->toArray();
break;
case SubmissionSubjectDAO::CONTROLLED_VOCAB_SUBMISSION_SUBJECT:
$submissionSubjectEntryDao = DAORegistry::getDAO('SubmissionSubjectEntryDAO'); /** @var \PKP\submission\SubmissionSubjectEntryDAO $submissionSubjectEntryDao */
$entries = $submissionSubjectEntryDao->getByContextId($vocab, $context->getId(), $locale, $term)->toArray();
break;
case SubmissionDisciplineDAO::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE:
$submissionDisciplineEntryDao = DAORegistry::getDAO('SubmissionDisciplineEntryDAO'); /** @var \PKP\submission\SubmissionDisciplineEntryDAO $submissionDisciplineEntryDao */
$entries = $submissionDisciplineEntryDao->getByContextId($vocab, $context->getId(), $locale, $term)->toArray();
break;
case SubmissionLanguageDAO::CONTROLLED_VOCAB_SUBMISSION_LANGUAGE:
$words = array_filter(PKPString::regexp_split('/\s+/', $term), 'strlen');
$languageNames = [];
foreach (Locale::getLanguages() as $language) {
if ($language->getAlpha2() && $language->getType() === 'L' && $language->getScope() === 'I' && Stringy::create($language->getLocalName())->containsAny($words, false)) {
$languageNames[] = $language->getLocalName();
}
}
asort($languageNames);
return $response->withJson($languageNames, 200);
case SubmissionAgencyDAO::CONTROLLED_VOCAB_SUBMISSION_AGENCY:
$submissionAgencyEntryDao = DAORegistry::getDAO('SubmissionAgencyEntryDAO'); /** @var \PKP\submission\SubmissionAgencyEntryDAO $submissionAgencyEntryDao */
$entries = $submissionAgencyEntryDao->getByContextId($vocab, $context->getId(), $locale, $term)->toArray();
break;
default:
$entries = [];
Hook::call('API::vocabs::getMany', [$vocab, &$entries, $slimRequest, $response, $request]);
}
$data = [];
foreach ($entries as $entry) {
$data[] = $entry->getData($vocab, $locale);
}
$data = array_values(array_unique($data));
return $response->withJson($data, 200);
}
}