Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions api/v1/citations/PKPCitationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Route;
use PKP\citation\pid\Arxiv;
use PKP\citation\pid\Doi;
use PKP\citation\pid\Handle;
use PKP\core\PKPBaseController;
use PKP\core\PKPRequest;
use PKP\plugins\Hook;
Expand Down Expand Up @@ -173,6 +176,19 @@ public function edit(Request $illuminateRequest): JsonResponse

$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_CITATION, $illuminateRequest->input());

$arxiv = Arxiv::extractFromString($params['arxiv']);
if(!empty($arxiv)){
$params['arxiv'] = $arxiv;
}
$doi = Doi::extractFromString($params['doi']);
if(!empty($doi)){
$params['doi'] = $doi;
}
$handle = Handle::extractFromString($params['handle']);
if(!empty($handle)){
$params['handle'] = $handle;
}

$readOnlyErrors = $this->getWriteDisabledErrors(PKPSchemaService::SCHEMA_CITATION, $params);
if (!empty($readOnlyErrors)) {
return response()->json($readOnlyErrors, Response::HTTP_BAD_REQUEST);
Expand Down
83 changes: 37 additions & 46 deletions api/v1/submissions/PKPSubmissionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ class PKPSubmissionController extends PKPBaseController
'changeVersion',
'getNextAvailableVersion',
'importAdditionalCitations',
'editCitationsMetadataLookup',
'deleteCitationsByPublicationId',
'reprocessCitationsByPublicationId'
];

/** @var array Handlers that must be authorized to write to a publication */
Expand Down Expand Up @@ -371,17 +371,17 @@ public function getGroupRoutes(): void
Role::ROLE_ID_AUTHOR,
]),
])->group(function () {
Route::put('{submissionId}/publications/{publicationId}/citations/metadataLookup', $this->editCitationsMetadataLookup(...))
->name('submission.citations.import')
->whereNumber(['submissionId', 'publicationId']);

Route::post('{submissionId}/publications/{publicationId}/citations/importAdditionalCitations', $this->importAdditionalCitations(...))
->name('submission.citations.import')
->whereNumber(['submissionId', 'publicationId']);

Route::delete('{submissionId}/publications/{publicationId}/citations/deleteCitationsByPublicationId', $this->deleteCitationsByPublicationId(...))
->name('submission.citations.delete')
->whereNumber(['submissionId', 'publicationId']);

Route::post('{submissionId}/publications/{publicationId}/citations/reprocessCitationsByPublicationId', $this->reprocessCitationsByPublicationId(...))
->name('submission.citations.reprocess')
->whereNumber(['submissionId', 'publicationId']);
});
}

Expand Down Expand Up @@ -2335,9 +2335,9 @@ protected function validateVersionIsMinor(Request $illuminateRequest): ?bool
}

/**
* Update citationsMetadataLookup of a publication, reprocess citations if enabled.
* Import / add citations from a raw citation string of a publication.
*/
protected function editCitationsMetadataLookup(Request $illuminateRequest): JsonResponse
protected function importAdditionalCitations(Request $illuminateRequest): JsonResponse
{
$publication = Repo::publication()->get((int)$illuminateRequest->route('publicationId'));

Expand Down Expand Up @@ -2365,39 +2365,17 @@ protected function editCitationsMetadataLookup(Request $illuminateRequest): Json
], Response::HTTP_FORBIDDEN);
}

$citationsMetadataLookup = (bool)$illuminateRequest->input('citationsMetadataLookup');

$processCitation = false;
if ($citationsMetadataLookup && !$publication->getData('citationsMetadataLookup')) {
$processCitation = true;
}

/** @var Citation $citation */
foreach ($publication->getData('citations') as &$citation) {
$citation->setIsProcessed(!$processCitation);
if ($processCitation) {
Repo::citation()->reprocessCitation($citation);
}
}
unset($citation);

$publication->setData('citationsMetadataLookup', $citationsMetadataLookup);
Repo::publication()->edit($publication, []);
$rawCitations = (string)$illuminateRequest->input('rawCitations');

$publication = Repo::publication()->get($publication->getId());
$result = Repo::citation()->importAdditionalCitations($publication->getId(), $rawCitations);

return response()->json(
[
'citationsMetadataLookup' => $publication->getData('citationsMetadataLookup')
],
Response::HTTP_OK
);
return response()->json($result, Response::HTTP_OK);
}

/**
* Import / add citations from a raw citation string of a publication.
* Delete a publication's citations.
*/
protected function importAdditionalCitations(Request $illuminateRequest): JsonResponse
protected function deleteCitationsByPublicationId(Request $illuminateRequest): JsonResponse
{
$publication = Repo::publication()->get((int)$illuminateRequest->route('publicationId'));

Expand Down Expand Up @@ -2425,17 +2403,24 @@ protected function importAdditionalCitations(Request $illuminateRequest): JsonRe
], Response::HTTP_FORBIDDEN);
}

$rawCitations = (string)$illuminateRequest->input('rawCitations');
$existingCitations = [];
/** @var Citation $citation */
foreach ($publication->getData('citations') as $citation) {
$existingCitations[] = Repo::citation()->getSchemaMap()->map($citation);
}

$result = Repo::citation()->importAdditionalCitations($publication->getId(), $rawCitations);
Repo::citation()->deleteByPublicationId($publication->getId());

return response()->json($result, Response::HTTP_OK);
return response()->json([
'itemsMax' => count($existingCitations),
'items' => $existingCitations
], Response::HTTP_OK);
}

/**
* Delete a publication's citations.
* Reprocess a publication's citations.
*/
protected function deleteCitationsByPublicationId(Request $illuminateRequest): JsonResponse
protected function reprocessCitationsByPublicationId(Request $illuminateRequest): JsonResponse
{
$publication = Repo::publication()->get((int)$illuminateRequest->route('publicationId'));

Expand Down Expand Up @@ -2463,17 +2448,23 @@ protected function deleteCitationsByPublicationId(Request $illuminateRequest): J
], Response::HTTP_FORBIDDEN);
}

$existingCitations = [];
/** @var Citation $citation */
foreach ($publication->getData('citations') as $citation) {
$existingCitations[] = Repo::citation()->getSchemaMap()->map($citation);
// Set all citations as not processed
$citations = $publication->getData('citations');
foreach ($citations as &$citation) {
$citation->setIsProcessed(false);
Repo::citation()->edit($citation, []);
}
unset($citation);

Repo::citation()->deleteByPublicationId($publication->getId());
foreach ($citations as &$citation) {
Repo::citation()->reprocessCitation($citation);
$citation = Repo::citation()->getSchemaMap()->map($citation);
}
unset($citation);

return response()->json([
'itemsMax' => count($existingCitations),
'items' => $existingCitations
'itemsMax' => count($citations),
'items' => $citations
], Response::HTTP_OK);
}
}
8 changes: 2 additions & 6 deletions classes/citation/Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,6 @@ public function importCitations(int $publicationId, ?string $rawCitationList): v
$importedCitations = [];
$this->deleteByPublicationId($publicationId);
if (is_array($citationStrings) && !empty($citationStrings)) {
$publication = Repo::publication()->get($publicationId);
foreach ($citationStrings as $seq => $rawCitationString) {
if (!empty($rawCitationString)) {
$citation = new Citation();
Expand All @@ -231,8 +230,7 @@ public function importCitations(int $publicationId, ?string $rawCitationList): v
$citation->setIsProcessed(false);
$newCitationId = $this->dao->insert($citation);
$citation->setId($newCitationId);
if ($publication->getData('citationsMetadataLookup') ||
(is_null($publication->getData('citationsMetadataLookup')) && $this->request->getContext()->getData('citationsMetadataLookup'))) {
if ($this->request->getContext()->getData('citationsMetadataLookup')) {
$this->reprocessCitation($citation);
}
$importedCitations[] = $citation;
Expand All @@ -256,7 +254,6 @@ public function importAdditionalCitations(int $publicationId, ?string $rawCitati

$rejectedCitations = [];
if (is_array($citationStrings) && !empty($citationStrings)) {
$publication = Repo::publication()->get($publicationId);
foreach ($citationStrings as $rawCitationString) {
if (!empty($rawCitationString)) {
if (!$this->existsRawCitation($publicationId, $rawCitationString)) {
Expand All @@ -268,8 +265,7 @@ public function importAdditionalCitations(int $publicationId, ?string $rawCitati
$citation->setIsProcessed(false);
$newCitationId = $this->dao->insert($citation);
$citation->setId($newCitationId);
if ($publication->getData('citationsMetadataLookup') ||
(is_null($publication->getData('citationsMetadataLookup')) && $this->request->getContext()->getData('citationsMetadataLookup'))) {
if ($this->request->getContext()->getData('citationsMetadataLookup')) {
$this->reprocessCitation($citation);
}
} else {
Expand Down
28 changes: 28 additions & 0 deletions classes/citation/enum/CitationSourceType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

/**
* @file classes/citation/enum/CitationSourceType.php
*
* Copyright (c) 2025 Simon Fraser University
* Copyright (c) 2025 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class CitationSourceType
*
* @ingroup citation
*
* @brief Enumeration for citation source types.
*/

namespace PKP\citation\enum;

enum CitationSourceType: string
{
case BOOK_SERIES = 'book series';
case CONFERENCE = 'conference';
case EBOOK_PLATFORM = 'ebook platform';
case JOURNAL = 'journal';
case METADATA = 'metadata';
case OTHER = 'other';
case REPOSITORY = 'repository';
}
60 changes: 60 additions & 0 deletions classes/citation/enum/CitationType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

/**
* @file classes/citation/enum/CitationType.php
*
* Copyright (c) 2025 Simon Fraser University
* Copyright (c) 2025 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class CitationType
*
* @ingroup citation
*
* @brief Enumeration for citation types.
*/

namespace PKP\citation\enum;

enum CitationType: string
{
case BOOK = 'book';
case BOOK_CHAPTER = 'book-chapter';
case BOOK_PART = 'book-part';
case BOOK_SECTION = 'book-section';
case BOOK_SERIES = 'book-series';
case BOOK_SET = 'book-set';
case BOOK_TRACK = 'book-track';
case COMPONENT = 'component';
case DATABASE = 'database';
case DATASET = 'dataset';
case DISSERTATION = 'dissertation';
case EDITED_BOOK = 'edited-book';
case EDITORIAL = 'editorial';
case ERRATUM = 'erratum';
case GRANT = 'grant';
case JOURNAL = 'journal';
case JOURNAL_ARTICLE = 'journal-article';
case JOURNAL_ISSUE = 'journal-issue';
case JOURNAL_VOLUME = 'journal-volume';
case LETTER = 'letter';
case LIBGUIDES = 'libguides';
case MONOGRAPH = 'monograph';
case OTHER = 'other';
case PARATEXT = 'paratext';
case PEER_REVIEW = 'peer-review';
case POSTED_CONTENT = 'posted-content';
case PREPRINT = 'preprint';
case PROCEEDINGS = 'proceedings';
case PROCEEDINGS_ARTICLE = 'proceedings-article';
case PROCEEDINGS_SERIES = 'proceedings-series';
case REFERENCE_BOOK = 'reference-book';
case REFERENCE_ENTRY = 'reference-entry';
case REPORT = 'report';
case REPORT_COMPONENT = 'report-component';
case REPORT_SERIES = 'report-series';
case RETRACTION = 'retraction';
case REVIEW = 'review';
case STANDARD = 'standard';
case SUPPLEMENTARY_MATERIALS = 'supplementary-materials';
}
3 changes: 3 additions & 0 deletions classes/citation/externalServices/openAlex/Inbound.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ public function getWork(Citation $citation): ?Citation
$newValue[] = $this->getAuthor($authorship);
}
break;
case 'type':
$newValue = !empty($response[$mappedKey]) ? $response[$mappedKey] : $response['type'];
break;
default:
if (is_array($mappedKey)) {
$newValue = ExternalServicesHelper::getValueFromArrayPath($response, $mappedKey);
Expand Down
2 changes: 1 addition & 1 deletion classes/citation/externalServices/openAlex/Mapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public static function getWork(): array
return [
'title' => 'title',
'date' => 'publication_date',
'type' => 'type_crossref',
'type' => 'type_crossref', // use 'type' if empty
'volume' => ['biblio', 'volume'],
'issue' => ['biblio', 'issue'],
'firstPage' => ['biblio', 'first_page'],
Expand Down
37 changes: 12 additions & 25 deletions classes/citation/pid/Arxiv.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,20 @@
class Arxiv extends BasePid
{
/** @copydoc AbstractPid::regex */
public const regex = '%\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))%s';

/** @copydoc AbstractPid::prefix */
public const prefix = 'https://arxiv.org/abs';

/** @copydoc AbstractPid::prefixInCorrect */
public const prefixInCorrect = [
'arxiv:'
public const regexes = [
'/arxiv:\s*\d{4}\.\d{4,5}(v\d+)?/i', // arxiv:2025.12345v2
'/https?:\/\/arxiv.org\/(?:abs|pdf)\/(?:\d+.\d+|[a-z-]+.\d+)/i' // https://arxiv.org/abs/2025.12345
Copy link
Contributor Author

@GaziYucel GaziYucel Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bozana

In Wikidata I found this:
^https?://arxiv.org/(?:pdf|abs)/((?:\d{4}.\d{4,5}|[a-z-]+(?:.[A-Z]{2})?/\d{7})(?:v\d+)?)
For URLs.

It seems there were old identifier, that could contain letters and then 7 digits, s. https://info.arxiv.org/help/arxiv_identifier.html#old

Also there could be pdf instead of abs in the URL -- if someone wants to access/cite PDF directly

Maybe not need to consider www

@GaziYucel

This doesn't match for example this: https://arxiv.org/pdf/cond-mat.02534

I changed the regex a bit, this would work:
/https?://(?:www.)?arxiv.org/(?:abs|pdf)/(?:\d+.\d+|[a-z-]+.\d+)/i

Try with https://regex101.com/

arxiv:cond-mat.02534
arxiv:1902.02534
https://arxiv.org/abs/1902.02534
https://arxiv.org/pdf/cond-mat.02534
doi: 10.1111/j.1365-277X.2011.01184.x.
doi: 10.1002/tox.20155.
doi: 10.7748/phc.2016.e1162.
https://doi.org/10.3390/s23218689.
https://doi.org/10.7717/peerj.1990
https://doi.org/10.1037/arc0000014.
hdl:11366/1249
handle:11366/1249
http://hdl.handle.net/11366/1249
https://www.bildung-forschung.digital/files/BAnz%20AT%2017.06.2020%20B3-1.pdf

@bozana

shell we ignore www ?

];

/** @copydoc AbstractPid::extractFromString() */
public static function extractFromString(?string $string): string
{
$string = parent::extractFromString($string);

$class = get_called_class();

// check if prefix found in extracted string
$prefixes = $class::prefixInCorrect;
$prefixes[] = $class::prefix;
/** @copydoc AbstractPid::prefix */
public const prefix = 'arxiv:';

foreach ($prefixes as $prefix) {
if (str_contains($string, $prefix)) {
return $string;
}
}
/** @copydoc AbstractPid::urlPrefix */
public const urlPrefix = 'https://arxiv.org/abs/';

return '';
}
/** @copydoc AbstractPid::alternatePrefixes */
public const alternatePrefixes = [
'arxiv',
'https://arxiv.org/pdf/'
];
}
Loading