<?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);
}
}
|