[ Mini Kiebo ]
Server: Windows NT DESKTOP-5B8S0D4 6.2 build 9200 (Windows 8 Professional Edition) i586
Path:
D:
/
Backup
/
05122024
/
htdocs
/
jurnal-kesmas
/
api
/
v1
/
stats
/
issues
/
[
Home
]
File: StatsIssueHandler.php
<?php /** * @file api/v1/stats/StatsIssueHandler.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 StatsIssueHandler * * @ingroup api_v1_stats * * @brief Handle API requests for issue statistics. * */ namespace APP\API\v1\stats\issues; use APP\core\Application; use APP\core\Services; use APP\facades\Repo; use APP\security\authorization\OjsIssueRequiredPolicy; use APP\statistics\StatisticsHelper; 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 as SlimHttpRequest; class StatsIssueHandler extends APIHandler { /** * Constructor */ public function __construct() { $this->_handlerPath = 'stats/issues'; $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() . '/{issueId:\d+}', 'handler' => [$this, 'get'], 'roles' => $roles ], [ 'pattern' => $this->getEndpointPattern() . '/{issueId:\d+}/timeline', 'handler' => [$this, 'getTimeline'], 'roles' => $roles ], ], ]; parent::__construct(); } /** * @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', 'getGalley', 'getToc'])) { $this->addPolicy(new OjsIssueRequiredPolicy($request, $args)); } return parent::authorize($request, $args, $roleAssignments); } /** * Get usage stats for a set of issues * * Returns total views by toc and all galleys. */ public function getMany(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse { $request = $this->getRequest(); $responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false; $defaultParams = [ 'count' => 30, 'offset' => 0, 'orderDirection' => StatisticsHelper::STATISTICS_ORDER_DESC, ]; $requestParams = array_merge($defaultParams, $slimRequest->getQueryParams()); $allowedParams = $this->_processAllowedParams($requestParams, [ 'dateStart', 'dateEnd', 'count', 'offset', 'orderDirection', 'searchPhrase', 'issueIds', ]); Hook::call('API::stats::issues::params', [&$allowedParams, $slimRequest]); $allowedParams['contextIds'] = [$request->getContext()->getId()]; $result = $this->_validateStatDates($allowedParams); if ($result !== true) { return $response->withStatus(400)->withJsonError($result); } if (!in_array($allowedParams['orderDirection'], [StatisticsHelper::STATISTICS_ORDER_ASC, StatisticsHelper::STATISTICS_ORDER_DESC])) { return $response->withStatus(400)->withJsonError('api.stats.400.invalidOrderDirection'); } // Identify issues which should be included in the results when a searchPhrase is passed if (!empty($allowedParams['searchPhrase'])) { $allowedIssueIds = empty($allowedParams['issueIds']) ? [] : $allowedParams['issueIds']; $allowedParams['issueIds'] = $this->_processSearchPhrase($allowedParams['searchPhrase'], $allowedIssueIds); if (empty($allowedParams['issueIds'])) { $csvColumnNames = $this->_getIssueReportColumnNames(); if ($responseCSV) { return $response->withCSV([], $csvColumnNames, 0); } else { return $response->withJson([ 'items' => [], 'itemsMax' => 0, ], 200); } } } // Get a list (count number) of top issues by total (toc + galley) views $statsService = Services::get('issueStats'); $totalMetrics = $statsService->getTotals($allowedParams); // Get the stats for each issue $items = []; foreach ($totalMetrics as $totalMetric) { $issueId = $totalMetric->issue_id; $dateStart = array_key_exists('dateStart', $allowedParams) ? $allowedParams['dateStart'] : null; $dateEnd = array_key_exists('dateEnd', $allowedParams) ? $allowedParams['dateEnd'] : null; $metricsByType = $statsService->getTotalsByType($issueId, $this->getRequest()->getContext()->getId(), $dateStart, $dateEnd); if ($responseCSV) { $items[] = $this->getItemForCSV($issueId, $metricsByType['toc'], $metricsByType['galley']); } else { $items[] = $this->getItemForJSON($issueId, $metricsByType['toc'], $metricsByType['galley']); } } $itemsMax = $statsService->getCount($allowedParams); $csvColumnNames = $this->_getIssueReportColumnNames(); if ($responseCSV) { return $response->withCSV($items, $csvColumnNames, $itemsMax); } else { return $response->withJson([ 'items' => $items, 'itemsMax' => $itemsMax, ], 200); } } /** * Get the total TOC or issue galley views for a set of issues * 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; $request = $this->getRequest(); $defaultParams = [ 'timelineInterval' => StatisticsHelper::STATISTICS_DIMENSION_MONTH, ]; $requestParams = array_merge($defaultParams, $slimRequest->getQueryParams()); $allowedParams = $this->_processAllowedParams($requestParams, [ 'dateStart', 'dateEnd', 'timelineInterval', 'searchPhrase', 'type' ]); Hook::call('API::stats::issues::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); } $allowedParams['contextIds'] = [$request->getContext()->getId()]; $allowedParams['assocTypes'] = [Application::ASSOC_TYPE_ISSUE]; if (array_key_exists('type', $allowedParams) && $allowedParams['type'] == 'files') { $allowedParams['assocTypes'] = [Application::ASSOC_TYPE_ISSUE_GALLEY]; }; // Identify issues which should be included in the results when a searchPhrase is passed if (!empty($allowedParams['searchPhrase'])) { $allowedIssueIds = empty($allowedParams['issueIds']) ? [] : $allowedParams['issueIds']; $allowedParams['issueIds'] = $this->_processSearchPhrase($allowedParams['searchPhrase'], $allowedIssueIds); if (empty($allowedParams['issueIds'])) { $dateStart = empty($allowedParams['dateStart']) ? StatisticsHelper::STATISTICS_EARLIEST_DATE : $allowedParams['dateStart']; $dateEnd = empty($allowedParams['dateEnd']) ? date('Ymd', strtotime('yesterday')) : $allowedParams['dateEnd']; $emptyTimeline = Services::get('issueStats')->getEmptyTimelineIntervals($dateStart, $dateEnd, $allowedParams['timelineInterval']); if ($responseCSV) { $csvColumnNames = Services::get('issueStats')->getTimelineReportColumnNames(); return $response->withCSV($emptyTimeline, $csvColumnNames, 0); } return $response->withJson($emptyTimeline, 200); } } $data = Services::get('issueStats')->getTimeline($allowedParams['timelineInterval'], $allowedParams); if ($responseCSV) { $csvColumnNames = Services::get('issueStats')->getTimelineReportColumnNames(); return $response->withCSV($data, $csvColumnNames, count($data)); } return $response->withJson($data, 200); } /** * Get a single issue's usage statistics */ public function get(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse { $request = $this->getRequest(); $issue = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ISSUE); $allowedParams = $this->_processAllowedParams($slimRequest->getQueryParams(), [ 'dateStart', 'dateEnd', ]); Hook::call('API::stats::issue::params', [&$allowedParams, $slimRequest]); $result = $this->_validateStatDates($allowedParams); if ($result !== true) { return $response->withStatus(400)->withJsonError($result); } $statsService = Services::get('issueStats'); $dateStart = array_key_exists('dateStart', $allowedParams) ? $allowedParams['dateStart'] : null; $dateEnd = array_key_exists('dateEnd', $allowedParams) ? $allowedParams['dateEnd'] : null; $metricsByType = $statsService->getTotalsByType($issue->getId(), $request->getContext()->getId(), $dateStart, $dateEnd); return $response->withJson([ 'tocViews' => $metricsByType['toc'], 'issueGalleyViews' => $metricsByType['galley'], 'issue' => Repo::issue()->getSchemaMap()->mapToStats($issue), ], 200); } /** * Get the total TOC or issue galley views for an issue broken down by * month or day */ public function getTimeline(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse { $request = $this->getRequest(); $issue = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ISSUE); $defaultParams = [ 'timelineInterval' => StatisticsHelper::STATISTICS_DIMENSION_MONTH, ]; $requestParams = array_merge($defaultParams, $slimRequest->getQueryParams()); $allowedParams = $this->_processAllowedParams($requestParams, [ 'dateStart', 'dateEnd', 'timelineInterval', 'type' ]); Hook::call('API::stats::issue::timeline::params', [&$allowedParams, $slimRequest]); $allowedParams['contextIds'] = [$request->getContext()->getId()]; $allowedParams['issueIds'] = [$issue->getId()]; $allowedParams['assocTypes'] = [Application::ASSOC_TYPE_ISSUE]; if (array_key_exists('type', $allowedParams) && $allowedParams['type'] == 'files') { $allowedParams['assocTypes'] = [Application::ASSOC_TYPE_ISSUE_GALLEY]; }; 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('issueStats'); $data = $statsService->getTimeline($allowedParams['timelineInterval'], $allowedParams); 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': 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 'issueIds': 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 issueIds param when a searchPhase * param is also passed. * * If the searchPhrase and issueIds params were both passed in the * request, then we only return IDs that match both conditions. */ protected function _processSearchPhrase(string $searchPhrase, array $issueIds = []): array { $searchPhraseIssueIds = Repo::issue() ->getCollector() ->filterByContextIds([Application::get()->getRequest()->getContext()->getId()]) ->filterByPublished(true) ->searchPhrase($searchPhrase) ->getIds() ->toArray(); if (!empty($issueIds)) { return array_intersect($issueIds, $searchPhraseIssueIds); } return $searchPhraseIssueIds; } /** * Get column names for the issue CSV report */ protected function _getIssueReportColumnNames(): array { return [ __('common.id'), __('editor.issues.issueIdentification'), __('stats.total'), __('stats.views'), __('stats.downloads') ]; } /** * Get CSV row with issue metrics */ protected function getItemForCSV(int $issueId, int $tocViews, int $issueGalleyViews): array { $totalViews = $tocViews + $issueGalleyViews; // Get issue identification for display $issue = Repo::issue()->get($issueId); $identification = $issue->getIssueIdentification(); return [ $issueId, $identification, $totalViews, $tocViews, $issueGalleyViews ]; } /** * Get JSON data with issue metrics */ protected function getItemForJSON(int $issueId, int $tocViews, int $issueGalleyViews): array { $totalViews = $tocViews + $issueGalleyViews; // Get basic issue details for display $issue = Repo::issue()->get($issueId); $issueProps = Repo::issue()->getSchemaMap()->mapToStats($issue); return [ 'totalViews' => $totalViews, 'tocViews' => $tocViews, 'issueGalleyViews' => $issueGalleyViews, 'issue' => $issueProps, ]; } /** * 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 ]); } }