464 lines
16 KiB
PHP
464 lines
16 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @file plugins/generic/datacite/DataciteExportPlugin.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 DataciteExportPlugin
|
|
*
|
|
* @brief DataCite export/registration plugin.
|
|
*/
|
|
|
|
namespace APP\plugins\generic\datacite;
|
|
|
|
use APP\core\Application;
|
|
use APP\facades\Repo;
|
|
use APP\issue\Issue;
|
|
use APP\plugins\DOIPubIdExportPlugin;
|
|
use APP\plugins\IDoiRegistrationAgency;
|
|
use APP\submission\Submission;
|
|
use Exception;
|
|
use PKP\config\Config;
|
|
use PKP\context\Context;
|
|
use PKP\core\DataObject;
|
|
use PKP\core\PKPApplication;
|
|
use PKP\core\PKPString;
|
|
use PKP\doi\Doi;
|
|
use PKP\file\FileManager;
|
|
use PKP\file\TemporaryFileManager;
|
|
use PKP\galley\Galley;
|
|
use PKP\plugins\Plugin;
|
|
use PKP\submission\Representation;
|
|
|
|
// DataCite API
|
|
define('DATACITE_API_RESPONSE_OK', 201);
|
|
define('DATACITE_API_URL', 'https://mds.datacite.org/');
|
|
define('DATACITE_API_URL_TEST', 'https://mds.test.datacite.org/');
|
|
|
|
// Export file types.
|
|
define('DATACITE_EXPORT_FILE_XML', 0x01);
|
|
define('DATACITE_EXPORT_FILE_TAR', 0x02);
|
|
|
|
class DataciteExportPlugin extends DOIPubIdExportPlugin
|
|
{
|
|
protected IDoiRegistrationAgency|Plugin $agencyPlugin;
|
|
|
|
public function __construct(IDoiRegistrationAgency $agencyPlugin)
|
|
{
|
|
parent::__construct();
|
|
|
|
$this->agencyPlugin = $agencyPlugin;
|
|
}
|
|
|
|
/**
|
|
* @see Plugin::getName()
|
|
*/
|
|
public function getName()
|
|
{
|
|
return 'DataciteExportPlugin';
|
|
}
|
|
|
|
/**
|
|
* @see Plugin::getDisplayName()
|
|
*/
|
|
public function getDisplayName()
|
|
{
|
|
return __('plugins.importexport.datacite.displayName');
|
|
}
|
|
|
|
/**
|
|
* @see Plugin::getDescription()
|
|
*/
|
|
public function getDescription()
|
|
{
|
|
return __('plugins.importexport.datacite.description');
|
|
}
|
|
|
|
/**
|
|
* @copydoc PubObjectsExportPlugin::getSubmissionFilter()
|
|
*/
|
|
public function getSubmissionFilter()
|
|
{
|
|
return 'article=>datacite-xml';
|
|
}
|
|
|
|
/**
|
|
* @copydoc PubObjectsExportPlugin::getIssueFilter()
|
|
*/
|
|
public function getIssueFilter()
|
|
{
|
|
return 'issue=>datacite-xml';
|
|
}
|
|
|
|
/**
|
|
* @copydoc PubObjectsExportPlugin::getRepresentationFilter()
|
|
*/
|
|
public function getRepresentationFilter()
|
|
{
|
|
return 'galley=>datacite-xml';
|
|
}
|
|
|
|
/**
|
|
* @copydoc ImportExportPlugin::getPluginSettingsPrefix()
|
|
*/
|
|
public function getPluginSettingsPrefix()
|
|
{
|
|
return 'datacite';
|
|
}
|
|
|
|
/**
|
|
* @copydoc DOIPubIdExportPlugin::getSettingsFormClassName()
|
|
*/
|
|
public function getSettingsFormClassName()
|
|
{
|
|
throw new Exception('DOI settings no longer managed via plugin settings form.');
|
|
}
|
|
|
|
/**
|
|
* @copydoc PubObjectsExportPlugin::getExportDeploymentClassName()
|
|
*/
|
|
public function getExportDeploymentClassName()
|
|
{
|
|
return '\APP\plugins\generic\datacite\DataciteExportDeployment';
|
|
}
|
|
|
|
/** Proxy to main plugin class's `getSetting` method */
|
|
public function getSetting($contextId, $name)
|
|
{
|
|
return $this->agencyPlugin->getSetting($contextId, $name);
|
|
}
|
|
|
|
/**
|
|
* @param DataObject[] $objects
|
|
*
|
|
*/
|
|
public function exportAndDeposit(
|
|
Context $context,
|
|
array $objects,
|
|
string &$responseMessage,
|
|
?bool $noValidation = null
|
|
): bool {
|
|
$fileManager = new FileManager();
|
|
|
|
$errorsOccurred = false;
|
|
foreach ($objects as $object) {
|
|
// Get the XML
|
|
$exportErrors = [];
|
|
$filter = $this->_getFilterFromObject($object);
|
|
$exportXml = $this->exportXML($object, $filter, $context, $noValidation, $exportErrors);
|
|
// Write the XML to a file.
|
|
// export file name example: datacite-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.generic.datacite.deposit.unsuccessful';
|
|
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(Context $context, array $objects, ?bool $noValidation = null, ?array &$outputErrors = null): ?int
|
|
{
|
|
$fileManager = new TemporaryFileManager();
|
|
|
|
// Export
|
|
$result = $this->_checkForTar();
|
|
if ($result === true) {
|
|
$exportedFiles = [];
|
|
foreach ($objects as $object) {
|
|
$filter = $this->_getFilterFromObject($object);
|
|
// Get the XML
|
|
$exportXml = $this->exportXML($object, $filter, $context, $noValidation, $outputErrors);
|
|
// Write the XML to a file.
|
|
// export file name example: datacite-20160723-160036-articles-1-1.xml
|
|
$objectFileNamePart = $this->_getObjectFileNamePart($object);
|
|
$exportFileName = $this->getExportFileName(
|
|
$this->getExportPath(),
|
|
$objectFileNamePart,
|
|
$context,
|
|
'.xml'
|
|
);
|
|
$fileManager->writeFile($exportFileName, $exportXml);
|
|
$exportedFiles[] = $exportFileName;
|
|
}
|
|
// If we have more than one export file we package the files
|
|
// up as a single tar before going on.
|
|
assert(count($exportedFiles) >= 1);
|
|
if (count($exportedFiles) > 1) {
|
|
// tar file name: e.g. datacite-20160723-160036-articles-1.tar.gz
|
|
$finalExportFileName = $this->getExportFileName(
|
|
$this->getExportPath(),
|
|
$objectFileNamePart,
|
|
$context,
|
|
'.tar.gz'
|
|
);
|
|
$this->_tarFiles($this->getExportPath(), $finalExportFileName, $exportedFiles);
|
|
// remove files
|
|
foreach ($exportedFiles as $exportedFile) {
|
|
$fileManager->deleteByPath($exportedFile);
|
|
}
|
|
} else {
|
|
$finalExportFileName = array_shift($exportedFiles);
|
|
}
|
|
$user = Application::get()->getRequest()->getUser();
|
|
|
|
return $fileManager->createTempFileFromExisting($finalExportFileName, $user->getId());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @copydoc PubObjectsExportPlugin::depositXML()
|
|
*/
|
|
public function depositXML($object, $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 datacite will not do any deposition');
|
|
return false;
|
|
}
|
|
|
|
$request = Application::get()->getRequest();
|
|
// Get the DOI and the URL for the object.
|
|
$doi = $object->getStoredPubId('doi');
|
|
assert(!empty($doi));
|
|
$testDOIPrefix = null;
|
|
if ($this->isTestMode($context)) {
|
|
$testDOIPrefix = $this->getSetting($context->getId(), 'testDOIPrefix');
|
|
assert(!empty($testDOIPrefix));
|
|
$doi = PKPString::regexp_replace('#^[^/]+/#', $testDOIPrefix . '/', $doi);
|
|
}
|
|
$url = $this->_getObjectUrl($request, $context, $object);
|
|
assert(!empty($url));
|
|
|
|
$dataCiteAPIUrl = DATACITE_API_URL;
|
|
$username = $this->getSetting($context->getId(), 'username');
|
|
$password = $this->getSetting($context->getId(), 'password');
|
|
if ($this->isTestMode($context)) {
|
|
$dataCiteAPIUrl = DATACITE_API_URL_TEST;
|
|
$username = $this->getSetting($context->getId(), 'testUsername');
|
|
$password = $this->getSetting($context->getId(), 'testPassword');
|
|
}
|
|
|
|
// Prepare HTTP session.
|
|
assert(is_readable($filename));
|
|
$httpClient = Application::get()->getHttpClient();
|
|
try {
|
|
$response = $httpClient->request('POST', $dataCiteAPIUrl . 'metadata', [
|
|
'auth' => [$username, $password],
|
|
'body' => fopen($filename, 'r'),
|
|
'headers' => [
|
|
'Content-Type' => 'application/xml;charset=UTF-8',
|
|
],
|
|
]);
|
|
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
|
$returnMessage = $e->getMessage();
|
|
if ($e->hasResponse()) {
|
|
$returnMessage = $e->getResponse()->getBody() . ' (' . $e->getResponse()->getStatusCode() . ' ' . $e->getResponse()->getReasonPhrase() . ')';
|
|
}
|
|
$this->updateDepositStatus($object, Doi::STATUS_ERROR);
|
|
return [['plugins.importexport.common.register.error.mdsError', "Registering DOI {$doi}: {$returnMessage}"]];
|
|
}
|
|
|
|
// Mint a DOI.
|
|
$httpClient = Application::get()->getHttpClient();
|
|
try {
|
|
$response = $httpClient->request('POST', $dataCiteAPIUrl . 'doi', [
|
|
'auth' => [$username, $password],
|
|
'headers' => [
|
|
'Content-Type' => 'text/plain;charset=UTF-8',
|
|
],
|
|
'body' => "doi={$doi}\nurl={$url}",
|
|
]);
|
|
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
|
$returnMessage = $e->getMessage();
|
|
if ($e->hasResponse()) {
|
|
$returnMessage = $e->getResponse()->getBody() . ' (' . $e->getResponse()->getStatusCode() . ' ' . $e->getResponse()->getReasonPhrase() . ')';
|
|
}
|
|
$this->updateDepositStatus($object, Doi::STATUS_ERROR);
|
|
return [['plugins.importexport.common.register.error.mdsError', "Registering DOI {$doi}: {$returnMessage}"]];
|
|
}
|
|
// Test mode submits entirely different DOI and URL so the status of that should not be stored in the database
|
|
// for the real DOI
|
|
if (!$this->isTestMode($context)) {
|
|
$this->updateDepositStatus($object, Doi::STATUS_REGISTERED);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Update stored DOI status based on if deposits and registration have been successful
|
|
*
|
|
* @param Submission|Issue|Representation $object
|
|
*/
|
|
public function updateDepositStatus(DataObject $object, string $status)
|
|
{
|
|
assert($object instanceof Submission || $object instanceof Issue || $object instanceof Representation);
|
|
if ($object instanceof Submission) {
|
|
$object = $object->getCurrentPublication();
|
|
}
|
|
$doiObject = $object->getData('doiObject');
|
|
$editParams = [
|
|
'status' => $status
|
|
];
|
|
if ($status == Doi::STATUS_REGISTERED) {
|
|
$editParams['registrationAgency'] = $this->getName();
|
|
}
|
|
Repo::doi()->edit($doiObject, $editParams);
|
|
}
|
|
|
|
/**
|
|
* Test whether the tar binary is available.
|
|
*
|
|
* @return bool|array Boolean true if available otherwise
|
|
* an array with an error message.
|
|
*/
|
|
public function _checkForTar()
|
|
{
|
|
$tarBinary = Config::getVar('cli', 'tar');
|
|
if (empty($tarBinary) || !is_executable($tarBinary)) {
|
|
$result = [
|
|
['manager.plugins.tarCommandNotFound']
|
|
];
|
|
} else {
|
|
$result = true;
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Create a tar archive.
|
|
*
|
|
* @param string $targetPath
|
|
* @param string $targetFile
|
|
* @param array $sourceFiles
|
|
*/
|
|
public function _tarFiles($targetPath, $targetFile, $sourceFiles)
|
|
{
|
|
assert((bool) $this->_checkForTar());
|
|
// GZip compressed result file.
|
|
$tarCommand = Config::getVar('cli', 'tar') . ' -czf ' . escapeshellarg($targetFile);
|
|
// Do not reveal our internal export path by exporting only relative filenames.
|
|
$tarCommand .= ' -C ' . escapeshellarg($targetPath);
|
|
// Do not reveal our webserver user by forcing root as owner.
|
|
$tarCommand .= ' --owner 0 --group 0 --';
|
|
// Add each file individually so that other files in the directory
|
|
// will not be included.
|
|
foreach ($sourceFiles as $sourceFile) {
|
|
assert(dirname($sourceFile) . '/' === $targetPath);
|
|
if (dirname($sourceFile) . '/' !== $targetPath) {
|
|
continue;
|
|
}
|
|
$tarCommand .= ' ' . escapeshellarg(basename($sourceFile));
|
|
}
|
|
// Execute the command.
|
|
exec($tarCommand);
|
|
}
|
|
|
|
/**
|
|
* Get the canonical URL of an object.
|
|
*
|
|
* @param \APP\core\Request $request
|
|
* @param \PKP\context\Context $context
|
|
* @param \APP\issue\Issue|\APP\submission\Submission|\PKP\galley\Galley $object
|
|
*/
|
|
public function _getObjectUrl($request, $context, $object)
|
|
{
|
|
//Dispatcher needed when called from CLI
|
|
$dispatcher = $request->getDispatcher();
|
|
// Retrieve the article of article files.
|
|
if ($object instanceof Galley) {
|
|
$publication = Repo::publication()->get($object->getData('publicationId'));
|
|
$articleId = $publication->getData('submissionId');
|
|
$cache = $this->getCache();
|
|
if ($cache->isCached('articles', $articleId)) {
|
|
$article = $cache->get('articles', $articleId);
|
|
} else {
|
|
$article = Repo::submission()->get($articleId);
|
|
}
|
|
assert($article instanceof Submission);
|
|
}
|
|
$url = null;
|
|
switch (true) {
|
|
case $object instanceof Issue:
|
|
$url = $dispatcher->url($request, PKPApplication::ROUTE_PAGE, $context->getPath(), 'issue', 'view', $object->getBestIssueId(), null, null, true);
|
|
break;
|
|
case $object instanceof Submission:
|
|
$url = $dispatcher->url($request, PKPApplication::ROUTE_PAGE, $context->getPath(), 'article', 'view', $object->getBestId(), null, null, true);
|
|
break;
|
|
case $object instanceof Galley:
|
|
$url = $dispatcher->url($request, PKPApplication::ROUTE_PAGE, $context->getPath(), 'article', 'view', [$article->getBestId(), $object->getBestGalleyId()], null, null, true);
|
|
break;
|
|
}
|
|
if ($this->isTestMode($context)) {
|
|
// Change server domain for testing.
|
|
$url = PKPString::regexp_replace('#://[^\s]+/index.php#', '://example.com/index.php', $url);
|
|
}
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* @param Submission|Issue|Representation $object
|
|
*
|
|
*/
|
|
private function _getFilterFromObject(DataObject $object): string
|
|
{
|
|
if ($object instanceof Submission) {
|
|
return $this->getSubmissionFilter();
|
|
} elseif ($object instanceof Issue) {
|
|
return $this->getIssueFilter();
|
|
} elseif ($object instanceof Representation) {
|
|
return $this->getRepresentationFilter();
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param Submission|Issue|Representation $object
|
|
*
|
|
*/
|
|
private function _getObjectFileNamePart(DataObject $object): string
|
|
{
|
|
if ($object instanceof Submission) {
|
|
return 'articles-' . $object->getId();
|
|
} elseif ($object instanceof Issue) {
|
|
return 'issues-' . $object->getId();
|
|
} elseif ($object instanceof Representation) {
|
|
return 'galleys-' . $object->getId();
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
}
|