| Server IP : 89.117.53.150 / Your IP : 216.73.216.11 Web Server : Apache System : Linux server2.hostofiraq.site 5.15.0-186-generic #196-Ubuntu SMP Sat Jun 20 16:09:34 UTC 2026 x86_64 User : iscmbt-kufa-atu ( 1014) PHP Version : 8.2.32 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /proc/self/cwd/iscmbt/plugins/generic/crossrefReferenceLinking/ |
Upload File : |
<?php
/**
* @file CrossrefReferenceLinkingPlugin.inc.php
*
* Copyright (c) 2013-2023 Simon Fraser University
* Copyright (c) 2003-2023 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file LICENSE.
*
* @class CrossrefReferenceLinkingPlugin
* @brief Reference Linking plugin class
*/
namespace APP\plugins\generic\crossrefReferenceLinking;
use APP\core\Application;
use APP\facades\Repo;
use APP\notification\NotificationManager;
use APP\notification\Notification;
use APP\publication\Publication;
use APP\submission\Submission;
use DOMDocument;
use PKP\citation\CitationDAO;
use PKP\context\Context;
use PKP\core\JSONMessage;
use PKP\db\DAORegistry;
use PKP\linkAction\LinkAction;
use PKP\linkAction\request\AjaxModal;
use PKP\plugins\GenericPlugin;
use PKP\plugins\Hook;
use PKP\plugins\PluginRegistry;
class CrossrefReferenceLinkingPlugin extends GenericPlugin
{
public const CROSSREF_API_REFS_URL = 'https://doi.crossref.org/getResolvedRefs';
public const CROSSREF_API_REFS_URL_DEV = 'https://test.crossref.org/getResolvedRefs';
/**
* @copydoc Plugin::register()
*/
public function register($category, $path, $mainContextId = null): bool
{
$registered = parent::register($category, $path, $mainContextId);
if (!$registered) {
return false;
}
if (Application::isUnderMaintenance()) {
return true;
}
// Additional fields added
Hook::add('Schema::get::submission', [$this, 'addSubmissionSchema']);
Hook::add('citationdao::getAdditionalFieldNames', [$this, 'getAdditionalCitationFieldNames']);
if (!$this->getEnabled($mainContextId)) {
return true;
}
if (!isset($mainContextId)) {
$mainContextId = $this->getCurrentContextId();
}
if (!$this->hasCrossrefCredentials($mainContextId) || !$this->citationsEnabled($mainContextId)) {
return true;
}
// Register scheduled task
Hook::add('AcronPlugin::parseCronTab', [$this, 'callbackParseCronTab']);
// Crossref export plugin hooks
Hook::add('articlecrossrefxmlfilter::execute', [$this, 'addCrossrefCitationsElements']);
Hook::add('crossrefexportplugin::deposited', [$this, 'getCitationsDiagnosticId']);
// Citation changed hook
Hook::add('CitationDAO::afterImportCitations', [$this, 'citationsChanged']);
// Article page hooks
Hook::add('Templates::Article::Details::Reference', [$this, 'displayReferenceDOI']);
return true;
}
/**
* Are Crossref username and password set in Crossref plugin
*/
public function hasCrossrefCredentials(int $contextId): bool
{
// If crossref plugin is set i.e. the crossref credentials exist we can assume that DOI plugin is set correctly
$crossrefPlugin = PluginRegistry::getPlugin('generic', 'crossrefplugin');
return $crossrefPlugin && strlen($crossrefPlugin->getSetting($contextId, 'username')) > 0 && strlen($crossrefPlugin->getSetting($contextId, 'password')) > 0;
}
/**
* Are citations submission metadata enabled in this journal
*/
public function citationsEnabled(int $contextId): bool
{
$contextDao = Application::getContextDAO();
$context = $contextDao->getById($contextId);
return !empty($context->getSetting('citations'));
}
/**
* @copydoc Plugin::getDisplayName()
*/
public function getDisplayName(): string
{
return __('plugins.generic.crossrefReferenceLinking.displayName');
}
/**
* @copydoc Plugin::getDescription()
*/
public function getDescription(): string
{
return __('plugins.generic.crossrefReferenceLinking.description');
}
/**
* @see Plugin::getActions()
*/
public function getActions($request, $actionArgs): array
{
$actions = parent::getActions($request, $actionArgs);
if (!$this->getEnabled()) {
return $actions;
}
$router = $request->getRouter();
$linkAction = new LinkAction(
id: 'settings',
actionRequest: new AjaxModal(
url: $router->url(
request: $request,
op: 'manage',
params: [
'verb' => 'settings',
'plugin' => $this->getName(),
'category' => 'generic'
]
),
title: $this->getDisplayName()
),
title: __('manager.plugins.settings')
);
array_unshift($actions, $linkAction);
return $actions;
}
/**
* @copydoc Plugin::manage()
*/
public function manage($args, $request): JSONMessage
{
$context = $request->getContext();
switch ($request->getUserVar('verb')) {
case 'settings':
$form = new CrossrefReferenceLinkingSettingsForm($this, $context->getId());
$form->initData();
return new JSONMessage(true, $form->fetch($request));
case 'save':
$form = new CrossrefReferenceLinkingSettingsForm($this, $context->getId());
$form->readInputData();
if ($form->validate()) {
$form->execute($request);
$notificationManager = new NotificationManager();
$notificationManager->createTrivialNotification(
$request->getUser()->getId(),
Notification::NOTIFICATION_TYPE_SUCCESS,
['contents' => __('plugins.generic.crossrefReferenceLinking.settings.form.saved')]
);
return new JSONMessage(true);
}
return new JSONMessage(true, $form->fetch($request));
}
return parent::manage($args, $request);
}
/**
* @see AcronPlugin::_parseCrontab()
*
* @param $hookName string 'AcronPlugin::parseCronTab'
* @param $args array [
* @option array Task files paths
* ]
*/
public function callbackParseCronTab(string $hookName, array $args): bool
{
if ($this->getEnabled() || !Application::isUnderMaintenance()) {
/** @var array $taskFilesPath */
$taskFilesPath =& $args[0]; // Reference needed.
$taskFilesPath[] = $this->getPluginPath() . '/scheduledTasks.xml';
}
return Hook::CONTINUE;
}
/**
* Add references data to the Crossref XML export
*
* @param $hookName string 'articlecrossrefxmlfilter::execute'
* @param $params array [
* @option DOMDocument Crossref filter output
* ]
*/
public function addCrossrefCitationsElements(string $hookName, array $params): bool
{
/** @var DOMDocument $preliminaryOutput */
$preliminaryOutput =& $params[0];
$request = Application::get()->getRequest();
$context = $request->getContext();
// Crossref export cannot be executed via CLI any more, thus there will always be a context
$contextId = $context->getId();
$citationDao = DAORegistry::getDAO('CitationDAO'); /** @var CitationDAO $citationDao */
$rfNamespace = 'http://www.crossref.org/schema/5.3.1';
$articleNodes = $preliminaryOutput->getElementsByTagName('journal_article');
foreach ($articleNodes as $articleNode) {
$doiDataNode = $articleNode->getElementsByTagName('doi_data')->item(0);
$doiNode = $doiDataNode->getElementsByTagName('doi')->item(0);
$doiValue = $doiNode->nodeValue;
// There should be only one DOI
/** @var Doi $doi */
$doi = Repo::doi()->getCollector()->filterByContextIds([$contextId])->filterByIdentifier($doiValue)->getMany()->first();
if (!$doi) {
return false;
}
$publications = Repo::publication()->getCollector()->filterByDoiIds([$doi->getId()])->getMany();
if ($publications->count() < 1) {
return false;
}
$submission = Repo::submission()->get($publications->first()->getData('submissionId'));
$articleCitations = $citationDao->getByPublicationId($submission->getCurrentPublication()->getId());
$articleCitationsArray = $articleCitations->toArray();
if (!empty($articleCitationsArray)) {
$citationListNode = $preliminaryOutput->createElementNS($rfNamespace, 'citation_list');
foreach ($articleCitationsArray as $citation) {
$rawCitation = $citation->getRawCitation();
if (!empty($rawCitation)) {
$citationNode = $preliminaryOutput->createElementNS($rfNamespace, 'citation');
$citationNode->setAttribute('key', $citation->getId());
// if Crossref DOI already exists for this citation, include it
// else include unstructred raw citation
if ($citation->getData($this->getCitationDoiSettingName())) {
$node = $preliminaryOutput->createElementNS($rfNamespace, 'doi');
$node->appendChild($preliminaryOutput->createTextNode($citation->getData($this->getCitationDoiSettingName())));
} else {
$node = $preliminaryOutput->createElementNS($rfNamespace, 'unstructured_citation');
$node->appendChild($preliminaryOutput->createTextNode($rawCitation));
}
$citationNode->appendChild($node);
$citationListNode->appendChild($citationNode);
}
}
$doiDataNode->parentNode->insertBefore($citationListNode, $doiDataNode->nextSibling);
}
}
return Hook::CONTINUE;
}
/**
* During the article DOI registration with Crossref, get the citations diagnostic ID from the Crossref response.
*
* @param $hookName string Hook name 'crossrefexportplugin::deposited'
* @param array [
* @option CrossrefExportPlugin
* @option string XML reposonse from Crossref deposit
* @option Submission
* ]
*/
public function getCitationsDiagnosticId(string $hookName, array $params): bool
{
/** @var string $response */
$response = & $params[1];
/** @var Submission $submission */
$submission = & $params[2];
// Get DOMDocument from the response XML string
$xmlDoc = new DOMDocument();
$xmlDoc->loadXML($response);
if ($xmlDoc->getElementsByTagName('citations_diagnostic')->length > 0) {
$citationsDiagnosticNode = $xmlDoc->getElementsByTagName('citations_diagnostic')->item(0); /** @var DOMNodeList $citationsDiagnosticNode */
$citationsDiagnosticCode = $citationsDiagnosticNode->getAttribute('deferred') ;
//set the citations diagnostic code and the setting for the automatic check
$submission->setData($this->getCitationsDiagnosticIdSettingName(), $citationsDiagnosticCode);
$submission->setData($this->getAutoCheckSettingName(), true);
$submission = Repo::submission()->edit($submission, [], Application::get()->getRequest());
}
return Hook::CONTINUE;
}
/**
* Add properties to the submission entity (SchemaDAO-based)
*
* @param $hookName string `Schema::get::submission`
* @param array $args [
* @option stdClass $schema
* ]
*/
public function addSubmissionSchema(string $hookName, array $args): bool
{
$schema = $args[0];
$schema->properties->{$this->getCitationsDiagnosticIdSettingName()} = (object) [
'type' => 'string',
'apiSummary' => true,
'validation' => ['nullable']
];
$schema->properties->{$this->getAutoCheckSettingName()} = (object) [
'type' => 'boolean',
'apiSummary' => true,
'validation' => ['nullable']
];
return Hook::CONTINUE;
}
/**
* Consider the additional citation setting name 'crossref::doi'.
*/
public function getAdditionalCitationFieldNames(string $hookName, CitationDAO $citationDao, array &$additionalFields): bool
{
$additionalFields[] = $this->getCitationDoiSettingName();
return Hook::CONTINUE;
}
/**
* Resets the submission data related to Reference Linking Plugin.
* Used every time the citations for a certain publication are imported.
*
* @param $hookName string 'CitationDAO::afterImportCitations'
* @param $params array [
* @option integer The publication ID for which the citations are imported
* ]
*/
public function citationsChanged(string $hookName, array $params): bool
{
/** @var int $publicationId */
$publicationId = $params[0];
$publication = Repo::publication()->get($publicationId);
$submission = Repo::submission()->get($publication->getData('submissionId'));
if ($submission->getData($this->getCitationsDiagnosticIdSettingName())) {
$submission->setData($this->getCitationsDiagnosticIdSettingName(), null);
$submission->setData($this->getAutoCheckSettingName(), null);
$submission = Repo::submission()->edit($submission, [], Application::get()->getRequest());
}
return Hook::CONTINUE;
}
/**
* Get found Crossref references DOIs for the given publication DOI.
*/
public function considerFoundCrossrefReferencesDOIs(Publication $publication): void
{
$doi = urlencode($publication->getDoi());
if (empty($doi)) {
return;
}
$citationDao = DAORegistry::getDAO('CitationDAO'); /** @var CitationDAO $citationDao */
$citations = $citationDao->getByPublicationId($publication->getId())->toAssociativeArray();
$citationsToCheck = [];
foreach ($citations as $citation) { /** @var Citation $citation */
if (!$citation->getData($this->getCitationDoiSettingName())) {
$citationsToCheck[$citation->getId()] = $citation;
}
}
if (empty($citationsToCheck)) {
return;
}
$citationsToCheckKeys = array_keys($citationsToCheck);
$submission = Repo::submission()->get($publication->getData('submissionId'));
$matchedReferences = $this->getResolvedRefs($doi, $submission->getData('contextId'));
if ($matchedReferences) {
$filteredMatchedReferences = array_filter(
$matchedReferences,
fn ($value) => in_array($value['key'], $citationsToCheckKeys)
);
foreach ($filteredMatchedReferences as $matchedReference) {
$citation = $citationsToCheck[$matchedReference['key']];
$citation->setData($this->getCitationDoiSettingName(), $matchedReference['doi']);
$citationDao->updateObject($citation);
}
// remove auto check setting
$submission->setData($this->getAutoCheckSettingName(), null);
$submission = Repo::submission()->edit($submission, [], Application::get()->getRequest());
}
}
/**
* Insert reference DOI on the citations and article view page.
*
* @param $hookName string Hook name
* @param $params array [
* @option Citation
* @option Smarty
* @option string Rendered smarty template
* ]
*/
public function displayReferenceDOI(string $hookName, array $params): bool
{
/** @var Citation $citation */
$citation = $params[0]['citation'];
/** @var \Smarty $smarty */
$smarty = &$params[1];
/** @var string $output */
$output = &$params[2];
if ($citation->getData($this->getCitationDoiSettingName())) {
$crossrefFullUrl = 'https://doi.org/' . $citation->getData($this->getCitationDoiSettingName());
$smarty->assign('crossrefFullUrl', $crossrefFullUrl);
$output .= $smarty->fetch($this->getTemplateResource('displayDOI.tpl'));
}
return Hook::CONTINUE;
}
/**
* Get citations diagnostic ID setting name.
*/
public function getCitationsDiagnosticIdSettingName(): string
{
return 'crossref::citationsDiagnosticId';
}
/**
* Get citation crossref DOI setting name.
*/
public function getCitationDoiSettingName(): string
{
return 'crossref::doi';
}
/**
* Get setting name, that defines if the scheduled task for the automatic check
* of the found Crossref citations DOIs should be run, if set up so in the plugin settings.
*/
public function getAutoCheckSettingName(): string
{
return 'crossref::checkCitationsDOIs';
}
/**
* Retrieve all submissions that should be automatically checked for the found Crossref citations DOIs.
*
* @return Submission[]
*/
public function getSubmissionsToCheck(Context $context): array
{
// Retrieve all published articles with their DOIs depositted together with the references.
// i.e. with the citations diagnostic ID setting
$submissionIds = Repo::submission()->getIdsBySetting($this->getAutoCheckSettingName(), true, $context->getId())->toArray();
$submissions = array_map(function ($submissionId) {
return Repo::submission()->get($submissionId);
}, $submissionIds);
return array_filter($submissions, function ($submission) {
return $submission->getData('status') === Submission::STATUS_PUBLISHED;
});
}
/**
* Use Crossref API to get the references DOIs for the the given article DOI.
*/
protected function getResolvedRefs(string $doi, int $contextId): ?array
{
$matchedReferences = null;
PluginRegistry::loadCategory('generic');
$crossrefPlugin = PluginRegistry::getPlugin('generic', 'crossrefplugin');
$username = $crossrefPlugin->getSetting($contextId, 'username');
$password = $crossrefPlugin->getSetting($contextId, 'password');
// Use a different endpoint for testing and production.
$isTestMode = $crossrefPlugin->getSetting($contextId, 'testMode') == 1;
$endpoint = ($isTestMode ? self::CROSSREF_API_REFS_URL_DEV : self::CROSSREF_API_REFS_URL);
$url = $endpoint . '?doi=' . $doi . '&usr=' . $username . '&pwd=' . $password;
$httpClient = Application::get()->getHttpClient();
try {
$response = $httpClient->request('POST', $url);
} catch (\GuzzleHttp\Exception\RequestException $e) {
return null;
}
if ($response?->getStatusCode() == 200) {
$response = json_decode($response->getBody(), true);
$matchedReferences = $response['matched-references'];
}
return $matchedReferences;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\APP\plugins\generic\crossrefReferenceLinking\CrossrefReferenceLinkingPlugin', '\CrossrefReferenceLinkingPlugin');
}