389 lines
12 KiB
PHP
389 lines
12 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @file classes/doi/Repository.php
|
|
*
|
|
* Copyright (c) 2014-2020 Simon Fraser University
|
|
* Copyright (c) 2000-2020 John Willinsky
|
|
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
|
*
|
|
* @class Repository
|
|
*
|
|
* @brief A repository to find and manage DOIs.
|
|
*/
|
|
|
|
namespace PKP\doi;
|
|
|
|
use APP\core\Request;
|
|
use APP\core\Services;
|
|
use APP\facades\Repo;
|
|
use Exception;
|
|
use Illuminate\Support\Facades\App;
|
|
use PKP\context\Context;
|
|
use PKP\core\PKPString;
|
|
use PKP\doi\exceptions\DoiException;
|
|
use PKP\jobs\doi\DepositSubmission;
|
|
use PKP\plugins\Hook;
|
|
use PKP\services\PKPSchemaService;
|
|
use PKP\validation\ValidatorFactory;
|
|
|
|
abstract class Repository
|
|
{
|
|
public const TYPE_PUBLICATION = 'publication';
|
|
public const TYPE_REPRESENTATION = 'representation';
|
|
|
|
public const SUFFIX_DEFAULT = 'default';
|
|
public const SUFFIX_CUSTOM_PATTERN = 'customPattern';
|
|
public const SUFFIX_MANUAL = 'customId';
|
|
|
|
public const CUSTOM_PUBLICATION_PATTERN = 'doiPublicationSuffixPattern';
|
|
public const CUSTOM_REPRESENTATION_PATTERN = 'doiRepresentationSuffixPattern';
|
|
|
|
public const CREATION_TIME_COPYEDIT = 'copyEditCreationTime';
|
|
public const CREATION_TIME_PUBLICATION = 'publicationCreationTime';
|
|
public const CREATION_TIME_NEVER = 'neverCreationTime';
|
|
|
|
/** @var DAO $dao */
|
|
public $dao;
|
|
|
|
/** @var string $schemaMap The name of the class to map this entity to its schema */
|
|
public $schemaMap = maps\Schema::class;
|
|
|
|
/** @var Request $request */
|
|
protected $request;
|
|
|
|
/** @var PKPSchemaService<Doi> $schemaService */
|
|
protected $schemaService;
|
|
|
|
public function __construct(DAO $dao, Request $request, PKPSchemaService $schemaService)
|
|
{
|
|
$this->dao = $dao;
|
|
$this->request = $request;
|
|
$this->schemaService = $schemaService;
|
|
}
|
|
|
|
/** @copydoc DAO::newDataObject() */
|
|
public function newDataObject(array $params = []): Doi
|
|
{
|
|
$doi = $this->dao->newDataObject();
|
|
if (!empty($params)) {
|
|
$doi->setAllData($params);
|
|
}
|
|
return $doi;
|
|
}
|
|
|
|
/** @copydoc DAO::get() */
|
|
public function get(int $id, int $contextId = null): ?Doi
|
|
{
|
|
return $this->dao->get($id, $contextId);
|
|
}
|
|
|
|
/** @copydoc DAO::exists() */
|
|
public function exists(int $id, int $contextId = null): bool
|
|
{
|
|
return $this->dao->exists($id, $contextId);
|
|
}
|
|
|
|
/** @copydoc DAO::getCollector */
|
|
public function getCollector(): Collector
|
|
{
|
|
return App::make(Collector::class);
|
|
}
|
|
|
|
/**
|
|
* Get an instance of the map class for mapping
|
|
* DOIs to their schema
|
|
*/
|
|
public function getSchemaMap(): maps\Schema
|
|
{
|
|
return app('maps')->withExtensions($this->schemaMap);
|
|
}
|
|
|
|
/**
|
|
* Check if duplicate of this DOI has already been recorded across all contexts.
|
|
*/
|
|
public function isDuplicate(string $doi, ?int $excludeDoiId = null): bool
|
|
{
|
|
$collector = $this->getCollector()->filterByIdentifier($doi);
|
|
$ids = $collector->getIds();
|
|
|
|
if ($ids->count() == 0) {
|
|
return false;
|
|
}
|
|
|
|
if ($excludeDoiId === null && $ids->count() > 0) {
|
|
return true;
|
|
}
|
|
|
|
if ($ids->has($excludeDoiId) && $ids->count() < 2) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Validate properties for a Doi
|
|
*
|
|
* Perform validation checks on data used to add or edit a Doi
|
|
*
|
|
* @param array $props A key/value array with the new data to validate
|
|
*
|
|
* @return array A key/value array with validation errors. Empty if no errors
|
|
*/
|
|
public function validate(?Doi $object, array $props): array
|
|
{
|
|
$errors = [];
|
|
|
|
$validator = ValidatorFactory::make(
|
|
$props,
|
|
$this->schemaService->getValidationRules($this->dao->schema, []),
|
|
);
|
|
|
|
// Check required fields
|
|
ValidatorFactory::required(
|
|
$validator,
|
|
$object,
|
|
$this->schemaService->getRequiredProps($this->dao->schema),
|
|
$this->schemaService->getMultilingualProps($this->dao->schema),
|
|
[],
|
|
''
|
|
);
|
|
|
|
// The contextId must match an existing context
|
|
$validator->after(function ($validator) use ($object, $props) {
|
|
if (isset($props['contextId']) && !$validator->errors()->get('contextId')) {
|
|
if (!Services::get('context')->exists($props['contextId'])) {
|
|
$validator->errors()->add('contextId', __('api.contexts.404.contextNotFound'));
|
|
}
|
|
}
|
|
|
|
// Check for duplicates across all contexts
|
|
$doiId = $object ? $object->getData('id') : null;
|
|
$doi = $props['doi'] ?? null;
|
|
if ($doi !== null && $this->isDuplicate($doi, $doiId)) {
|
|
$validator->errors()->add('doi', __('doi.editor.doiSuffixCustomIdentifierNotUnique'));
|
|
}
|
|
});
|
|
|
|
$validator->after(function ($validator) use ($object, $props) {
|
|
$doi = $props['doi'] ?? null;
|
|
if ($doi !== null && !$validator->errors()->get('doi')) {
|
|
$validRegexPattern = '/[^-._;()\/A-Za-z0-9]/';
|
|
|
|
Hook::call('Doi::suffixValidation', [&$validRegexPattern]);
|
|
|
|
$hasInvalidCharacters = PKPString::regexp_match($validRegexPattern, $doi);
|
|
if ($hasInvalidCharacters) {
|
|
$validator->errors()->add('doi', __('doi.editor.doiSuffixInvalidCharacters'));
|
|
}
|
|
}
|
|
});
|
|
|
|
if ($validator->fails()) {
|
|
$errors = $this->schemaService->formatValidationErrors($validator->errors());
|
|
}
|
|
|
|
Hook::call('Doi::validate', [&$errors, $object, $props]);
|
|
|
|
return $errors;
|
|
}
|
|
|
|
/** @copydoc DAO::insert() */
|
|
public function add(Doi $doi): int
|
|
{
|
|
$id = $this->dao->insert($doi);
|
|
Hook::call('Doi::add', [$doi]);
|
|
|
|
return $id;
|
|
}
|
|
|
|
/** @copydoc DAO:update() */
|
|
public function edit(Doi $doi, array $params)
|
|
{
|
|
$newDoi = clone $doi;
|
|
$newDoi->setAllData(array_merge($newDoi->_data, $params));
|
|
|
|
Hook::call('Doi::edit', [$newDoi, $doi, $params]);
|
|
|
|
$this->dao->update($newDoi);
|
|
}
|
|
|
|
/** @copydoc DAO::delete() */
|
|
public function delete(Doi $doi)
|
|
{
|
|
Hook::call('Doi::delete::before', [$doi]);
|
|
$this->dao->delete($doi);
|
|
Hook::call('Doi::delete', [$doi]);
|
|
}
|
|
|
|
/**
|
|
* Delete a collection of DOIs
|
|
*/
|
|
public function deleteMany(Collector $collector)
|
|
{
|
|
$dois = $collector->getMany();
|
|
foreach ($dois as $doi) {
|
|
$this->delete($doi);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set DOIs status to Doi::STATUS_STALE, indicating the metadata has change and needs
|
|
* to be updated with the registration agency.
|
|
*/
|
|
public function markStale(array $doiIds)
|
|
{
|
|
$this->dao->markStale($doiIds);
|
|
}
|
|
|
|
/**
|
|
* Sets DOI status to Doi::STATUS_SUBMITTED, indicating the DOI has been queued to be
|
|
* deposited with a registration agency, but the actual deposit has not yet been made.
|
|
*/
|
|
public function markSubmitted(array $doiIds)
|
|
{
|
|
$this->dao->markSubmitted($doiIds);
|
|
}
|
|
|
|
/**
|
|
* Manually sets DOI status to Doi::STATUS_REGISTERED. This is used in cases where the
|
|
* DOI registration process has been complete elsewhere and needs to be recorded as
|
|
* registered locally.
|
|
*/
|
|
public function markRegistered(int $doiId)
|
|
{
|
|
$doi = $this->get($doiId);
|
|
$editParams = [
|
|
'status' => Doi::STATUS_REGISTERED,
|
|
'registrationAgency' => null
|
|
];
|
|
|
|
Hook::call('Doi::markRegistered', [&$editParams]);
|
|
$this->edit($doi, $editParams);
|
|
}
|
|
|
|
/**
|
|
* Manually sets DOI status to Doi::STATUS_UNREGISTERED.
|
|
*/
|
|
public function markUnregistered(int $doiId)
|
|
{
|
|
$doi = $this->get($doiId);
|
|
$editParams = [
|
|
'status' => Doi::STATUS_UNREGISTERED,
|
|
];
|
|
|
|
$this->edit($doi, $editParams);
|
|
}
|
|
|
|
/**
|
|
* Schedules DOI deposits with the active registration agency for all valid and
|
|
* unregistered/stale publication items. Items are added as a queued job to be
|
|
* completed asynchronously.
|
|
*/
|
|
public function depositAll(Context $context)
|
|
{
|
|
$enabledDoiTypes = $context->getData(Context::SETTING_ENABLED_DOI_TYPES) ?? [];
|
|
if ($this->_checkIfSubmissionValidForDeposit($enabledDoiTypes)) {
|
|
// If there is no configured registration agency, nothing can be deposited.
|
|
$agency = $context->getConfiguredDoiAgency();
|
|
if (!$agency) {
|
|
return;
|
|
}
|
|
|
|
$submissionsCollection = $this->dao->getAllDepositableSubmissionIds($context);
|
|
$submissionData = $submissionsCollection->reduce(function ($carry, $item) {
|
|
if ($item->submission_id) {
|
|
$carry['submissionIds'][] = $item->submission_id;
|
|
}
|
|
$carry['doiIds'][] = $item->doi_id;
|
|
|
|
return $carry;
|
|
}, ['submissionIds' => [], 'doiIds' => []]);
|
|
|
|
// Schedule/queue jobs for submissions
|
|
foreach ($submissionData['submissionIds'] as $submissionId) {
|
|
dispatch(new DepositSubmission($submissionId, $context, $agency));
|
|
}
|
|
|
|
// Mark submission DOIs as submitted
|
|
Repo::doi()->markSubmitted($submissionData['doiIds']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks whether a DOI object is referenced by ID on any pub objects for a given pub object type.
|
|
*
|
|
* @param string $pubObjectType One of Repo::doi()::TYPE_* constants
|
|
*/
|
|
public function isAssigned(int $doiId, string $pubObjectType): bool
|
|
{
|
|
return match ($pubObjectType) {
|
|
Repo::doi()::TYPE_PUBLICATION => Repo::publication()
|
|
->getCollector()
|
|
->filterByDoiIds([$doiId])
|
|
->getIds()
|
|
->count(),
|
|
default => false,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates an eight character DOI suffix
|
|
*
|
|
*/
|
|
protected function generateDefaultSuffix(): string
|
|
{
|
|
return DoiGenerator::encodeSuffix();
|
|
}
|
|
|
|
/**
|
|
* Loops over valid submission DOI types to see if any are enabled
|
|
*/
|
|
private function _checkIfSubmissionValidForDeposit(array $enabledDoiTypes): bool
|
|
{
|
|
foreach ($this->getValidSubmissionDoiTypes() as $validSubmissionDoiType) {
|
|
if (in_array($validSubmissionDoiType, $enabledDoiTypes)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get app-specific DOI type constants to check when scheduling deposit for submissions
|
|
*/
|
|
abstract protected function getValidSubmissionDoiTypes(): array;
|
|
|
|
/**
|
|
* Gets all DOI IDs related to a submission
|
|
*
|
|
* @return array<int> DOI IDs
|
|
*/
|
|
abstract public function getDoisForSubmission(int $submissionId): array;
|
|
|
|
/**
|
|
* Compose final DOI and save to database
|
|
*
|
|
* @throws Exception
|
|
*/
|
|
protected function mintAndStoreDoi(Context $context, string $doiSuffix): int
|
|
{
|
|
$doiPrefix = $context->getData(Context::SETTING_DOI_PREFIX);
|
|
if (empty($doiPrefix)) {
|
|
throw new DoiException('doi.exceptions.missingPrefix');
|
|
}
|
|
|
|
$completedDoi = $doiPrefix . '/' . $doiSuffix;
|
|
|
|
$doiDataParams = [
|
|
'doi' => $completedDoi,
|
|
'contextId' => $context->getId()
|
|
];
|
|
|
|
$doi = $this->newDataObject($doiDataParams);
|
|
return $this->add($doi);
|
|
}
|
|
}
|