| Server IP : 127.0.1.1 / Your IP : 216.73.216.83 Web Server : Apache/2.4.58 (Ubuntu) System : Linux nepub 6.8.0-88-generic #89-Ubuntu SMP PREEMPT_DYNAMIC Sat Oct 11 01:02:46 UTC 2025 x86_64 User : root ( 0) PHP Version : 8.2.30 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/html/public_html/lib/pkp/classes/statistics/ |
Upload File : |
<?php
/**
* @defgroup lib_pkp_classes_statistics
*/
/**
* @file lib/pkp/classes/statistics/PKPMetricsDAO.inc.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 PKPMetricsDAO
* @ingroup lib_pkp_classes_statistics
*
* @brief Class with basic operations for retrieving and adding statistics data.
*/
import('classes.statistics.StatisticsHelper'); //STATISTICS_DIMENSION_
class PKPMetricsDAO extends DAO {
/**
* Retrieve a range of aggregate, filtered, ordered metric values, i.e.
* a statistics report.
*
* @see <https://pkp.sfu.ca/wiki/index.php/OJSdeStatisticsConcept#Input_and_Output_Formats_.28Aggregation.2C_Filters.2C_Metrics_Data.29>
* for a full specification of the input and output format of this method.
*
* @param $metricType string|array metrics selection
* @param $columns string|array column (aggregation level) selection
* @param $filters array report-level filter selection
* @param $orderBy array order criteria
* @param $range null|DBResultRange paging specification
* @param $nonAdditive boolean (optional) Whether the metric type dimension
* will be additive or not. This must be used with care, different metric types
* should not be additive because they may diverge in ways of counting usage events.
*
* @return null|array The selected data as a simple tabular result set or
* null if metrics are not supported, the specified report
* is invalid or cannot be produced or another error occurred.
*/
function &getMetrics($metricType, $columns = array(), $filters = array(), $orderBy = array(), $range = null, $nonAdditive = true) {
// Return by reference.
$nullVar = null;
// Canonicalize and validate parameter format.
if (is_scalar($metricType)) $metricType = array($metricType);
if (is_scalar($columns)) $columns = array($columns);
if (!(is_array($filters) && is_array($orderBy))) return $nullVar;
// Validate parameter content.
foreach ($metricType as $metricTypeElement) {
if (!is_string($metricTypeElement)) return $nullVar;
}
$validColumns = array(
STATISTICS_DIMENSION_CONTEXT_ID,
STATISTICS_DIMENSION_PKP_SECTION_ID,
STATISTICS_DIMENSION_ASSOC_OBJECT_ID,
STATISTICS_DIMENSION_ASSOC_OBJECT_TYPE,
STATISTICS_DIMENSION_SUBMISSION_ID,
STATISTICS_DIMENSION_REPRESENTATION_ID,
STATISTICS_DIMENSION_FILE_TYPE,
STATISTICS_DIMENSION_ASSOC_TYPE,
STATISTICS_DIMENSION_ASSOC_ID,
STATISTICS_DIMENSION_COUNTRY,
STATISTICS_DIMENSION_REGION,
STATISTICS_DIMENSION_CITY,
STATISTICS_DIMENSION_MONTH,
STATISTICS_DIMENSION_DAY,
STATISTICS_DIMENSION_METRIC_TYPE
);
// If the metric column was defined, remove it. We've already
// add that below.
$metricKey = array_search(STATISTICS_METRIC, $columns);
if ($metricKey !== false) unset($columns[$metricKey]);
if (count(array_diff($columns, $validColumns)) > 0) return $nullVar;
$validColumns[] = STATISTICS_METRIC;
foreach ($filters as $filterColumn => $value) {
if (!in_array($filterColumn, $validColumns)) return $nullVar;
}
$validDirections = array(STATISTICS_ORDER_ASC, STATISTICS_ORDER_DESC);
foreach ($orderBy as $orderColumn => $direction) {
if (!in_array($orderColumn, $validColumns)) return $nullVar;
if (!in_array($direction, $validDirections)) return $nullVar;
}
// Validate correct use of the (non-additive) metric type dimension. We
// either require a filter on a single metric type or the metric type
// must be present as a column.
if (empty($metricType)) return $nullVar;
if (count($metricType) !== 1 && $nonAdditive) {
if (!in_array(STATISTICS_DIMENSION_METRIC_TYPE, $columns)) {
array_push($columns, STATISTICS_DIMENSION_METRIC_TYPE);
}
}
// Add the metric type as filter.
$filters[STATISTICS_DIMENSION_METRIC_TYPE] = $metricType;
// Build the select and group by clauses.
if (empty($columns)) {
$selectClause = 'SELECT SUM(metric) AS metric';
$groupByClause = '';
} else {
$selectedColumns = implode(', ', $columns);
$selectClause = "SELECT $selectedColumns, SUM(metric) AS metric";
$groupByClause = "GROUP BY $selectedColumns";
}
// Build the where and having clauses.
$params = array();
$whereClause = '';
$havingClause = '';
$isFirst = true;
foreach ($filters as $column => $values) {
// The filter array contains STATISTICS_* constants for the filtered
// hierarchy aggregation level as keys.
if ($column === STATISTICS_METRIC) {
$havingClause = 'HAVING ';
$currentClause =& $havingClause; // Reference required.
} else {
if ($isFirst && $column) {
$whereClause = 'WHERE ';
$isFirst = false;
} else {
$whereClause .= ' AND ';
}
$currentClause =& $whereClause; // Reference required.
}
if (is_object($values)) {
$values = (array) $values;
}
if (is_array($values) && isset($values['from'])) {
// Range filter: The value is a hashed array with from/to entries.
if (!isset($values['to'])) return $nullVar;
$currentClause .= "($column BETWEEN ? AND ?)";
$params[] = $values['from'];
$params[] = $values['to'];
} else {
// Element selection filter: The value is a scalar or an
// unordered array of one or more hierarchy element IDs.
if (is_array($values) && count($values) === 1) {
$values = array_pop($values);
}
if (is_scalar($values)) {
$currentClause .= "$column = ?";
$params[] = $values;
} elseif (count($values)) {
$placeholders = array_pad(array(), count($values), '?');
$placeholders = implode(', ', $placeholders);
$currentClause .= "$column IN ($placeholders)";
foreach ($values as $value) {
$params[] = $value;
}
} else {
// count($values) == 0: No matches should be returned.
$currentClause .= '1=0';
}
}
unset($currentClause);
}
// Replace the current time constant by time values
// inside the parameters array.
$currentTime = array(
STATISTICS_YESTERDAY => date('Ymd', strtotime('-1 day', time())),
STATISTICS_CURRENT_MONTH => date('Ym', time()));
foreach ($currentTime as $constant => $time) {
$currentTimeKeys = array_keys($params, $constant);
foreach ($currentTimeKeys as $key) {
$params[$key] = $time;
}
}
// Build the order-by clause.
$orderByClause = '';
if (count($orderBy) > 0) {
$isFirst = true;
foreach ($orderBy as $orderColumn => $direction) {
if ($isFirst) {
$orderByClause = 'ORDER BY ';
} else {
$orderByClause .= ', ';
}
$orderByClause .= "$orderColumn $direction";
$isFirst = false;
}
}
// Build the report.
$sql = "$selectClause FROM metrics $whereClause $groupByClause $havingClause $orderByClause";
if (is_a($range, 'DBResultRange')) $result = $this->retrieveRange($sql, $params, $range);
else $result = $this->retrieve($sql, $params);
// Return the report.
$returner = [];
foreach ($result as $row) {
$returner[] = (array) $row;
}
return $returner;
}
/**
* Get all load ids that are associated
* with records filtered by the passed
* arguments.
* @param $assocType int
* @param $assocId int
* @param $metricType string
* @return array
*/
function getLoadId($assocType, $assocId, $metricType) {
$result = $this->retrieve('SELECT load_id FROM metrics WHERE assoc_type = ? AND assoc_id = ? AND metric_type = ? GROUP BY load_id', [$assocType, $assocId, $metricType]);
$loadIds = [];
foreach ($result as $row) {
$loadIds[] = $row->load_id;
}
return $loadIds;
}
/**
* Check for the presence of any record
* that has the passed metric type.
* @param $metricType string
* @return boolean
*/
function hasRecord($metricType) {
$result = $this->retrieve('SELECT load_id FROM metrics WHERE metric_type = ? LIMIT 1', [$metricType]);
return (boolean) $result->current();
}
/**
* Purge a load batch.
*
* @param $loadId string
*/
function purgeLoadBatch($loadId) {
$this->update('DELETE FROM metrics WHERE load_id = ?', [$loadId]); // Not a number.
}
/**
* Purge all records associated with the passed metric type
* until the passed date.
* @param $metricType string
* @param $toDate string
*/
function purgeRecords($metricType, $toDate) {
$this->update('DELETE FROM metrics WHERE metric_type = ? AND day IS NOT NULL AND day <= ?', [$metricType, $toDate]);
}
/**
* Insert an entry into metrics table.
*
* @param $record array
* @param $errorMsg string
*/
function insertRecord($record) {
$recordToStore = array();
// Required dimensions.
$requiredDimensions = ['load_id', 'assoc_type', 'assoc_id', 'metric_type'];
foreach ($requiredDimensions as $requiredDimension) {
if (!isset($record[$requiredDimension])) {
throw new Exception('Cannot load record: missing dimension "' . $requiredDimension . '".');
}
$recordToStore[$requiredDimension] = $record[$requiredDimension];
}
$assocType = $recordToStore['assoc_type'] = (int)$recordToStore['assoc_type'];
$assocId = $recordToStore['assoc_id'] = (int)$recordToStore['assoc_id'];
list($contextId, $pkpSectionId, $assocObjType,
$assocObjId, $submissionId, $representationId) = $this->foreignKeyLookup($assocType, $assocId);
$recordToStore['context_id'] = $contextId;
$recordToStore['pkp_section_id'] = $pkpSectionId;
$recordToStore['assoc_object_type'] = $assocObjType;
$recordToStore['assoc_object_id'] = $assocObjId;
$recordToStore['submission_id'] = $submissionId;
$recordToStore['representation_id'] = $representationId;
// File type is optional.
if (isset($record['file_type']) && $record['file_type']) $recordToStore['file_type'] = (int)$record['file_type'];
// We require either month or day in the time dimension.
if (isset($record['day'])) {
if (!PKPString::regexp_match('/[0-9]{8}/', $record['day'])) {
throw new Exception('Cannot load record: invalid date.');
}
$recordToStore['day'] = $record['day'];
$recordToStore['month'] = substr($record['day'], 0, 6);
if (isset($record['month']) && $recordToStore['month'] != $record['month']) {
throw new Exception('Cannot load record: invalid month.');
}
} elseif (isset($record['month'])) {
if (!PKPString::regexp_match('/[0-9]{6}/', $record['month'])) {
throw new Exception('Cannot load record: invalid month.');
}
$recordToStore['month'] = $record['month'];
} else {
throw new Exception('Cannot load record: Missing time dimension.');
}
// Geolocation is optional.
if (isset($record['country_id'])) $recordToStore['country_id'] = (string)$record['country_id'];
if (isset($record['region'])) $recordToStore['region'] = (string)$record['region'];
if (isset($record['city'])) $recordToStore['city'] = (string)$record['city'];
// The metric must be set. If it is 0 we ignore the record.
if (!isset($record['metric'])) {
throw new Exception('Cannot load record: metric is missing.');
}
if (!is_numeric($record['metric'])) {
throw new Exception('Cannot load record: invalid metric.');
}
$recordToStore['metric'] = (int) $record['metric'];
// Save the record to the database.
$fields = implode(', ', array_keys($recordToStore));
$placeholders = implode(', ', array_pad(array(), count($recordToStore), '?'));
$params = array_values($recordToStore);
return $this->update("INSERT INTO metrics ($fields) VALUES ($placeholders)", $params);
}
//
// Protected methods.
//
/**
* Foreign key lookup for the published object dimension.
* @param $assocType int
* @param $assocId int
* @return array Values must be foreign keys relative to the
* context, pkp section, associated object (type and id), submission
* and representation.
*/
protected function foreignKeyLookup($assocType, $assocId) {
$contextId = $sectionId = $submissionId = $assocObjectType = $assocObjectId = $representationId = null;
$isFile = false;
$isRepresentation = false;
switch($assocType) {
case ASSOC_TYPE_SUBMISSION_FILE:
case ASSOC_TYPE_SUBMISSION_FILE_COUNTER_OTHER:
$submissionFile = Services::get('submissionFile')->get($assocId);
if ($submissionFile) {
$isFile = true;
$submissionId = $submissionFile->getData('submissionId');
if ($submissionFile->getData('assocType') == ASSOC_TYPE_REPRESENTATION) {
$representationId = $submissionFile->getData('assocId');
} else {
throw new Exception('Cannot load record: submission file is not associated with a representation object.');
}
} else {
throw new Exception('Cannot load record: invalid submission file id.');
}
// Don't break but go on to retrieve the representation.
case ASSOC_TYPE_REPRESENTATION:
if (!$isFile) $representationId = $assocId;
$representationDao = Application::getRepresentationDAO(); /* @var $representationDao RepresentationDAO */
$representation = $representationDao->getById($representationId); /* @var $representation Representation */
if ($representation) {
if (!$isFile) $isRepresentation = true;
$contextId = $representation->getContextId();
$publication = Services::get('publication')->get($representation->getData('publicationId'));
$submissionId = $publication->getData('submissionId');
} else {
throw new Exception('Cannot load record: invalid representation id.');
}
// Don't break but go on to retrieve the submission.
case ASSOC_TYPE_SUBMISSION:
if (!$isFile && !$isRepresentation) $submissionId = $assocId;
$submissionDao = DAORegistry::getDAO('SubmissionDAO'); /* @var $submissionDao SubmissionDAO */
$submission = $submissionDao->getById($submissionId);
if ($submission) {
$contextId = $submission->getContextId();
$submissionId = $submission->getId();
$sectionId = $submission->getSectionId();
} else {
throw new Exception('Cannot load record: invalid submission id.');
}
list($assocObjectType, $assocObjectId) = $this->getAssocObjectInfo($submissionId, $contextId);
break;
case ASSOC_TYPE_SECTION:
$sectionDao = Application::getSectionDAO();
$section = $sectionDao->getById($assocId); /* @var $section PKPSection */
if ($section) {
$sectionId = $section->getId();
$contextId = $section->getContextId();
} else {
throw new Exception('Cannot load record: invalid section id.');
}
break;
case Application::getContextAssocType():
$contextDao = Application::getContextDAO(); /* @var $contextDao ContextDAO */
$context = $contextDao->getById($assocId);
if (!$context) {
throw new Exception('Cannot load record: invalid context id.');
}
$contextId = $assocId;
break;
}
return array($contextId, $sectionId, $assocObjectType, $assocObjectId, $submissionId, $representationId);
}
/**
* Get the id and type of the object that
* the passed submission info is associated with.
* Default implementation returns null, subclasses
* have to implement it.
* @param $submissionId Submission id.
* @param $contextId The submission context id.
* @return array Assoc type and id of the object.
*/
protected function getAssocObjectInfo($submissionId, $contextId) {
return array(null, null);
}
}