491 lines
17 KiB
PHP
491 lines
17 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @file plugins/generic/crossref/CrossrefExportPlugin.php
|
|
*
|
|
* Copyright (c) 2014-2022 Simon Fraser University
|
|
* Copyright (c) 2003-2022 John Willinsky
|
|
* Distributed under The MIT License. For full terms see the file LICENSE.
|
|
*
|
|
* @class CrossrefExportPlugin
|
|
*
|
|
* @brief Crossref/MEDLINE XML metadata export plugin
|
|
*/
|
|
|
|
namespace APP\plugins\generic\crossref;
|
|
|
|
use APP\core\Application;
|
|
use PKP\config\Config;
|
|
use APP\facades\Repo;
|
|
use APP\issue\Issue;
|
|
use APP\journal\Journal;
|
|
use APP\plugins\DOIPubIdExportPlugin;
|
|
use APP\plugins\IDoiRegistrationAgency;
|
|
use APP\submission\Submission;
|
|
use Exception;
|
|
use GuzzleHttp\Exception\GuzzleException;
|
|
use GuzzleHttp\Exception\RequestException;
|
|
use PKP\core\DataObject;
|
|
use PKP\doi\Doi;
|
|
use PKP\file\FileManager;
|
|
use PKP\file\TemporaryFileManager;
|
|
use PKP\plugins\Hook;
|
|
use PKP\plugins\Plugin;
|
|
|
|
class CrossrefExportPlugin extends DOIPubIdExportPlugin
|
|
{
|
|
// The status of the Crossref DOI.
|
|
// any, notDeposited, and markedRegistered are reserved
|
|
public const CROSSREF_STATUS_FAILED = 'failed';
|
|
public const CROSSREF_API_DEPOSIT_OK = 200;
|
|
public const CROSSREF_API_DEPOSIT_ERROR_FROM_CROSSREF = 403;
|
|
public const CROSSREF_API_URL = 'https://api.crossref.org/v2/deposits';
|
|
//TESTING
|
|
public const CROSSREF_API_URL_DEV = 'https://test.crossref.org/v2/deposits';
|
|
public const CROSSREF_API_STATUS_URL = 'https://doi.crossref.org/servlet/submissionDownload';
|
|
//TESTING
|
|
public const CROSSREF_API_STATUS_URL_DEV = 'https://test.crossref.org/servlet/submissionDownload';
|
|
// The name of the setting used to save the registered DOI and the URL with the deposit status.
|
|
public const CROSSREF_DEPOSIT_STATUS = 'depositStatus';
|
|
|
|
public function __construct(protected IDoiRegistrationAgency|Plugin $agencyPlugin)
|
|
{
|
|
parent::__construct();
|
|
}
|
|
|
|
public function register($category, $path, $mainContextId = null)
|
|
{
|
|
$success = parent::register($category, $path, $mainContextId);
|
|
if ($success) {
|
|
// register hooks. This will prevent DB access attempts before the
|
|
// schema is installed.
|
|
if (Application::isUnderMaintenance()) {
|
|
return true;
|
|
}
|
|
}
|
|
return $success;
|
|
}
|
|
|
|
/**
|
|
* @copydoc Plugin::getName()
|
|
*/
|
|
public function getName()
|
|
{
|
|
return 'CrossrefExportPlugin';
|
|
}
|
|
|
|
/**
|
|
* @copydoc Plugin::getDisplayName()
|
|
*/
|
|
public function getDisplayName()
|
|
{
|
|
return __('plugins.importexport.crossref.displayName');
|
|
}
|
|
|
|
/**
|
|
* @copydoc Plugin::getDescription()
|
|
*/
|
|
public function getDescription()
|
|
{
|
|
return __('plugins.importexport.crossref.description');
|
|
}
|
|
|
|
/**
|
|
* @copydoc PubObjectsExportPlugin::getSubmissionFilter()
|
|
*/
|
|
public function getSubmissionFilter()
|
|
{
|
|
return 'article=>crossref-xml';
|
|
}
|
|
|
|
/**
|
|
* @copydoc PubObjectsExportPlugin::getIssueFilter()
|
|
*/
|
|
public function getIssueFilter()
|
|
{
|
|
return 'issue=>crossref-xml';
|
|
}
|
|
|
|
/** Proxy to main plugin class's `getSetting` method */
|
|
public function getSetting($contextId, $name)
|
|
{
|
|
return $this->agencyPlugin->getSetting($contextId, $name);
|
|
}
|
|
|
|
/**
|
|
* @copydoc PubObjectsExportPlugin::getStatusMessage()
|
|
*/
|
|
public function getStatusMessage($request)
|
|
{
|
|
// Application is set to sandbox mode and will not run the features of plugin
|
|
if (Config::getVar('general', 'sandbox', false)) {
|
|
error_log('Application is set to sandbox mode and will not have any interaction with crossref external service');
|
|
return __('common.sandbox');
|
|
}
|
|
|
|
// if the failure occurred on request and the message was saved
|
|
// return that message
|
|
$articleId = $request->getUserVar('articleId');
|
|
$article = Repo::submission()->get((int)$articleId);
|
|
$failedMsg = $article->getData('doiObject')->getData($this->getFailedMsgSettingName());
|
|
if (!empty($failedMsg)) {
|
|
return $failedMsg;
|
|
}
|
|
|
|
$context = $request->getContext();
|
|
|
|
$httpClient = Application::get()->getHttpClient();
|
|
try {
|
|
$response = $httpClient->request(
|
|
'POST',
|
|
$this->isTestMode($context) ? static::CROSSREF_API_STATUS_URL_DEV : static::CROSSREF_API_STATUS_URL,
|
|
[
|
|
'form_params' => [
|
|
'doi_batch_id' => $request->getUserVar('batchId'),
|
|
'type' => 'result',
|
|
'usr' => $this->getSetting($context->getId(), 'username'),
|
|
'pwd' => $this->getSetting($context->getId(), 'password'),
|
|
]
|
|
]
|
|
);
|
|
} catch (RequestException $e) {
|
|
$returnMessage = $e->getMessage();
|
|
if ($e->hasResponse()) {
|
|
$returnMessage = $e->getResponse()->getBody() . ' (' . $e->getResponse()->getStatusCode() . ' ' . $e->getResponse()->getReasonPhrase() . ')';
|
|
}
|
|
return __('plugins.importexport.common.register.error.mdsError', ['param' => $returnMessage]);
|
|
}
|
|
|
|
return (string) $response->getBody();
|
|
}
|
|
|
|
/**
|
|
* Get a list of additional setting names that should be stored with the objects.
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function _getObjectAdditionalSettings()
|
|
{
|
|
return array_merge(parent::_getObjectAdditionalSettings(), [
|
|
$this->getDepositBatchIdSettingName(),
|
|
$this->getFailedMsgSettingName(),
|
|
$this->getSuccessMsgSettingName(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @copydoc ImportExportPlugin::getPluginSettingsPrefix()
|
|
*/
|
|
public function getPluginSettingsPrefix()
|
|
{
|
|
return 'crossrefplugin';
|
|
}
|
|
|
|
/**
|
|
* @copydoc PubObjectsExportPlugin::getSettingsFormClassName()
|
|
*/
|
|
public function getSettingsFormClassName()
|
|
{
|
|
throw new Exception('DOI settings no longer managed via plugin settings form.');
|
|
}
|
|
|
|
/**
|
|
* @copydoc PubObjectsExportPlugin::getExportDeploymentClassName()
|
|
*/
|
|
public function getExportDeploymentClassName()
|
|
{
|
|
return (string) \APP\plugins\generic\crossref\CrossrefExportDeployment::class;
|
|
}
|
|
|
|
public function exportAndDeposit($context, $objects, $filter, string &$responseMessage, $noValidation = null): bool
|
|
{
|
|
$fileManager = new FileManager();
|
|
$resultErrors = [];
|
|
|
|
assert($filter != null);
|
|
// Errors occurred will be accessible via the status link
|
|
// thus do not display all errors notifications (for every article),
|
|
// just one general.
|
|
// Warnings occurred when the registration was successful will however be
|
|
// displayed for each article.
|
|
$errorsOccurred = false;
|
|
// The new Crossref deposit API expects one request per object.
|
|
// On contrary the export supports bulk/batch object export, thus
|
|
// also the filter expects an array of objects.
|
|
// Thus the foreach loop, but every object will be in an one item array for
|
|
// the export and filter to work.
|
|
foreach ($objects as $object) {
|
|
// Get the XML
|
|
// Supply an exportErrors array because otherwise exportXML() will echo out export errors
|
|
$exportErrors = [];
|
|
$exportXml = $this->exportXML([$object], $filter, $context, $noValidation, $exportErrors);
|
|
// Write the XML to a file.
|
|
// export file name example: crossref-20160723-160036-articles-1-1.xml
|
|
$objectFileNamePart = $this->_getObjectFileNamePart($object);
|
|
$exportFileName = $this->getExportFileName($this->getExportPath(), $objectFileNamePart, $context, '.xml');
|
|
$fileManager->writeFile($exportFileName, $exportXml);
|
|
// Deposit the XML file.
|
|
$result = $this->depositXML($object, $context, $exportFileName);
|
|
if (!$result) {
|
|
$errorsOccurred = true;
|
|
}
|
|
if (is_array($result)) {
|
|
$resultErrors[] = $result;
|
|
}
|
|
// Remove all temporary files.
|
|
$fileManager->deleteByPath($exportFileName);
|
|
}
|
|
// Prepare response message and return status
|
|
if (empty($resultErrors)) {
|
|
if ($errorsOccurred) {
|
|
$responseMessage = 'plugins.importexport.crossref.register.error.mdsError';
|
|
return false;
|
|
} else {
|
|
$responseMessage = $this->getDepositSuccessNotificationMessageKey();
|
|
return true;
|
|
}
|
|
} else {
|
|
$responseMessage = 'api.dois.400.depositFailed';
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exports and stores XML as a TemporaryFile
|
|
*
|
|
*
|
|
* @throws Exception
|
|
*/
|
|
public function exportAsDownload(\PKP\context\Context $context, array $objects, string $filter, string $objectsFileNamePart, ?bool $noValidation = null, ?array &$exportErrors = null): ?int
|
|
{
|
|
$fileManager = new TemporaryFileManager();
|
|
|
|
$exportErrors = [];
|
|
$exportXml = $this->exportXML($objects, $filter, $context, $noValidation, $exportErrors);
|
|
|
|
$exportFileName = $this->getExportFileName($this->getExportPath(), $objectsFileNamePart, $context, '.xml');
|
|
|
|
$fileManager->writeFile($exportFileName, $exportXml);
|
|
|
|
$user = Application::get()->getRequest()->getUser();
|
|
|
|
return $fileManager->createTempFileFromExisting($exportFileName, $user->getId());
|
|
}
|
|
|
|
/**
|
|
* @param Submission $objects
|
|
* @param Journal $context
|
|
* @param string $filename Export XML filename
|
|
*
|
|
* @throws GuzzleException
|
|
*
|
|
* @see PubObjectsExportPlugin::depositXML()
|
|
*
|
|
*/
|
|
public function depositXML($objects, $context, $filename)
|
|
{
|
|
// Application is set to sandbox mode and will not run the features of plugin
|
|
if (Config::getVar('general', 'sandbox', false)) {
|
|
error_log('Application is set to sandbox mode and will not have any interaction with crossref external service');
|
|
return false;
|
|
}
|
|
|
|
$status = null;
|
|
$msgSave = null;
|
|
|
|
$httpClient = Application::get()->getHttpClient();
|
|
assert(is_readable($filename));
|
|
|
|
try {
|
|
$response = $httpClient->request(
|
|
'POST',
|
|
$this->isTestMode($context) ? static::CROSSREF_API_URL_DEV : static::CROSSREF_API_URL,
|
|
[
|
|
'multipart' => [
|
|
[
|
|
'name' => 'usr',
|
|
'contents' => $this->getSetting($context->getId(), 'username'),
|
|
],
|
|
[
|
|
'name' => 'pwd',
|
|
'contents' => $this->getSetting($context->getId(), 'password'),
|
|
],
|
|
[
|
|
'name' => 'operation',
|
|
'contents' => 'doMDUpload',
|
|
],
|
|
[
|
|
'name' => 'mdFile',
|
|
'contents' => fopen($filename, 'r'),
|
|
],
|
|
]
|
|
]
|
|
);
|
|
} catch (RequestException $e) {
|
|
$returnMessage = $e->getMessage();
|
|
if ($e->hasResponse()) {
|
|
$eResponseBody = $e->getResponse()->getBody();
|
|
$eStatusCode = $e->getResponse()->getStatusCode();
|
|
if ($eStatusCode == static::CROSSREF_API_DEPOSIT_ERROR_FROM_CROSSREF) {
|
|
$xmlDoc = new \DOMDocument('1.0', 'utf-8');
|
|
$xmlDoc->loadXML($eResponseBody);
|
|
$batchIdNode = $xmlDoc->getElementsByTagName('batch_id')->item(0);
|
|
$msg = $xmlDoc->getElementsByTagName('msg')->item(0)->nodeValue;
|
|
$msgSave = $msg . PHP_EOL . $eResponseBody;
|
|
$status = Doi::STATUS_ERROR;
|
|
$this->updateDepositStatus($context, $objects, $status, $batchIdNode->nodeValue, $msgSave);
|
|
$returnMessage = $msg . ' (' . $eStatusCode . ' ' . $e->getResponse()->getReasonPhrase() . ')';
|
|
} else {
|
|
$returnMessage = $eResponseBody . ' (' . $eStatusCode . ' ' . $e->getResponse()->getReasonPhrase() . ')';
|
|
$this->updateDepositStatus($context, $objects, Doi::STATUS_ERROR, null, $returnMessage);
|
|
}
|
|
}
|
|
return [['plugins.importexport.common.register.error.mdsError', $returnMessage]];
|
|
}
|
|
|
|
// Get DOMDocument from the response XML string
|
|
$xmlDoc = new \DOMDocument('1.0', 'utf-8');
|
|
$xmlDoc->loadXML($response->getBody());
|
|
$batchIdNode = $xmlDoc->getElementsByTagName('batch_id')->item(0);
|
|
$submissionIdNode = $xmlDoc->getElementsByTagName('submission_id')->item(0);
|
|
$successMessage = __('plugins.generic.crossref.successMessage', ['submissionId' => $submissionIdNode->nodeValue]);
|
|
|
|
// Get the DOI deposit status
|
|
// If the deposit failed
|
|
$failureCountNode = $xmlDoc->getElementsByTagName('failure_count')->item(0);
|
|
$failureCount = (int) $failureCountNode->nodeValue;
|
|
if ($failureCount > 0) {
|
|
$status = Doi::STATUS_ERROR;
|
|
$result = false;
|
|
} else {
|
|
// Deposit was received
|
|
$status = Doi::STATUS_REGISTERED;
|
|
$result = true;
|
|
|
|
// If there were some warnings, display them
|
|
$warningCountNode = $xmlDoc->getElementsByTagName('warning_count')->item(0);
|
|
$warningCount = (int) $warningCountNode->nodeValue;
|
|
if ($warningCount > 0) {
|
|
$result = [['plugins.importexport.crossref.register.success.warning', htmlspecialchars($response->getBody())]];
|
|
}
|
|
// A possibility for other plugins (e.g. reference linking) to work with the response
|
|
Hook::run('crossrefexportplugin::deposited', [[$this, $response->getBody(), $objects]]);
|
|
}
|
|
|
|
// Update the status
|
|
if ($status) {
|
|
$this->updateDepositStatus($context, $objects, $status, $batchIdNode->nodeValue, $msgSave, $successMessage);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Check the Crossref APIs, if deposits and registration have been successful
|
|
*
|
|
* @param Journal $context
|
|
* @param DataObject $object The object getting deposited
|
|
* @param int $status
|
|
* @param string $batchId
|
|
* @param string $failedMsg (optional)
|
|
* @param null|mixed $successMsg
|
|
*/
|
|
public function updateDepositStatus($context, $object, $status, $batchId = null, $failedMsg = null, $successMsg = null)
|
|
{
|
|
assert($object instanceof Submission || $object instanceof Issue);
|
|
if ($object instanceof Submission) {
|
|
$doiIds = Repo::doi()->getDoisForSubmission($object->getId());
|
|
} else {
|
|
$doiIds = Repo::doi()->getDoisForIssue($object->getId(), true);
|
|
}
|
|
|
|
foreach ($doiIds as $doiId) {
|
|
$doi = Repo::doi()->get($doiId);
|
|
|
|
$editParams = [
|
|
'status' => $status,
|
|
// Sets new failedMsg or resets to null for removal of previous message
|
|
$this->getFailedMsgSettingName() => $failedMsg,
|
|
$this->getDepositBatchIdSettingName() => $batchId,
|
|
$this->getSuccessMsgSettingName() => $successMsg,
|
|
];
|
|
|
|
if ($status === Doi::STATUS_REGISTERED) {
|
|
$editParams['registrationAgency'] = $this->getName();
|
|
}
|
|
|
|
Repo::doi()->edit($doi, $editParams);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @copydoc DOIPubIdExportPlugin::markRegistered()
|
|
*/
|
|
public function markRegistered($context, $objects)
|
|
{
|
|
foreach ($objects as $object) {
|
|
// Get all DOIs for each object
|
|
// Check if submission or issue
|
|
if ($object instanceof Submission) {
|
|
$doiIds = Repo::doi()->getDoisForSubmission($object->getId());
|
|
} else {
|
|
$doiIds = Repo::doi()->getDoisForIssue($object->getId, true);
|
|
}
|
|
|
|
foreach ($doiIds as $doiId) {
|
|
Repo::doi()->markRegistered($doiId);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get request failed message setting name.
|
|
* NB: Changed as of 3.4
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getFailedMsgSettingName()
|
|
{
|
|
return $this->getPluginSettingsPrefix() . '_failedMsg';
|
|
}
|
|
|
|
/**
|
|
* Get deposit batch ID setting name.
|
|
* NB Changed as of 3.4
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getDepositBatchIdSettingName()
|
|
{
|
|
return $this->getPluginSettingsPrefix() . '_batchId';
|
|
}
|
|
|
|
public function getSuccessMsgSettingName(): string
|
|
{
|
|
return $this->getPluginSettingsPrefix() . '_successMsg';
|
|
}
|
|
|
|
/**
|
|
* @copydoc PubObjectsExportPlugin::getDepositSuccessNotificationMessageKey()
|
|
*/
|
|
public function getDepositSuccessNotificationMessageKey()
|
|
{
|
|
return 'plugins.importexport.common.register.success';
|
|
}
|
|
|
|
/**
|
|
* @param Submission|Issue $object
|
|
*
|
|
*/
|
|
private function _getObjectFileNamePart(DataObject $object): string
|
|
{
|
|
if ($object instanceof Submission) {
|
|
return 'articles-' . $object->getId();
|
|
} elseif ($object instanceof Issue) {
|
|
return 'issues-' . $object->getId();
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
}
|