359 lines
13 KiB
PHP
359 lines
13 KiB
PHP
<?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);
|
|
}
|
|
}
|