1715 lines
66 KiB
PHP
1715 lines
66 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @file OrcidProfilePlugin.php
|
|
*
|
|
* Copyright (c) 2015-2022 University of Pittsburgh
|
|
* Copyright (c) 2014-2022 Simon Fraser University
|
|
* Copyright (c) 2003-2022 John Willinsky
|
|
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
|
*
|
|
* @class OrcidProfilePlugin
|
|
*
|
|
* @ingroup plugins_generic_orcidProfile
|
|
*
|
|
* @brief ORCID Profile plugin class
|
|
*/
|
|
|
|
namespace APP\plugins\generic\orcidProfile;
|
|
use APP\issue\Issue;
|
|
use APP\journal\Journal;
|
|
use APP\plugins\generic\citationStyleLanguage\CitationStyleLanguagePlugin;
|
|
use APP\publication\Publication;
|
|
use APP\author\Author;
|
|
use APP\controllers\grid\users\author\form\AuthorForm;
|
|
use APP\core\Application;
|
|
use APP\core\Request;
|
|
use APP\core\Services;
|
|
use APP\decision\Decision;
|
|
use APP\facades\Repo;
|
|
use APP\plugins\generic\orcidProfile\classes\form\OrcidProfileSettingsForm;
|
|
use APP\plugins\generic\orcidProfile\classes\form\OrcidProfileStatusForm;
|
|
use APP\plugins\generic\orcidProfile\classes\OrcidValidator;
|
|
use APP\plugins\generic\orcidProfile\mailables\OrcidCollectAuthorId;
|
|
use APP\plugins\generic\orcidProfile\mailables\OrcidRequestAuthorAuthorization;
|
|
use APP\submission\Submission;
|
|
use APP\template\TemplateManager;
|
|
use Carbon\Carbon;
|
|
use Exception;
|
|
use GuzzleHttp\Exception\ClientException;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use PKP\components\forms\FieldOptions;
|
|
use PKP\components\forms\FieldText;
|
|
use PKP\components\forms\publication\ContributorForm;
|
|
use PKP\config\Config;
|
|
use PKP\core\Core;
|
|
use PKP\core\JSONMessage;
|
|
use PKP\core\PKPApplication;
|
|
use PKP\db\DAORegistry;
|
|
use PKP\facades\Locale;
|
|
use PKP\form\Form;
|
|
use PKP\install\Installer;
|
|
use PKP\linkAction\LinkAction;
|
|
use PKP\linkAction\request\AjaxModal;
|
|
use PKP\plugins\GenericPlugin;
|
|
use PKP\plugins\Hook;
|
|
use PKP\plugins\PluginRegistry;
|
|
use PKP\services\PKPSchemaService;
|
|
use PKP\submission\PKPSubmission;
|
|
use PKP\submission\reviewAssignment\ReviewAssignment;
|
|
use PKP\submission\reviewAssignment\ReviewAssignmentDAO;
|
|
use Sokil\IsoCodes\Database\Countries\Country;
|
|
|
|
define('ORCID_URL', 'https://orcid.org/');
|
|
define('ORCID_URL_SANDBOX', 'https://sandbox.orcid.org/');
|
|
define('ORCID_API_URL_PUBLIC', 'https://pub.orcid.org/');
|
|
define('ORCID_API_URL_PUBLIC_SANDBOX', 'https://pub.sandbox.orcid.org/');
|
|
define('ORCID_API_URL_MEMBER', 'https://api.orcid.org/');
|
|
define('ORCID_API_URL_MEMBER_SANDBOX', 'https://api.sandbox.orcid.org/');
|
|
define('ORCID_API_VERSION_URL', 'v3.0/');
|
|
define('ORCID_API_SCOPE_PUBLIC', '/authenticate');
|
|
define('ORCID_API_SCOPE_MEMBER', '/activities/update');
|
|
define('OAUTH_TOKEN_URL', 'oauth/token');
|
|
define('ORCID_EMPLOYMENTS_URL', 'employments');
|
|
define('ORCID_PROFILE_URL', 'person');
|
|
define('ORCID_EMAIL_URL', 'email');
|
|
define('ORCID_WORK_URL', 'work');
|
|
define('ORCID_REVIEW_URL', 'peer-review');
|
|
|
|
class OrcidProfilePlugin extends GenericPlugin
|
|
{
|
|
public const PUBID_TO_ORCID_EXT_ID = ['doi' => 'doi', 'other::urn' => 'urn'];
|
|
public const USER_GROUP_TO_ORCID_ROLE = ['Author' => 'AUTHOR', 'Translator' => 'CHAIR_OR_TRANSLATOR', 'Journal manager' => 'AUTHOR'];
|
|
|
|
private $currentContextId;
|
|
|
|
/**
|
|
* @copydoc Plugin::register()
|
|
*
|
|
* @param null|mixed $mainContextId
|
|
*/
|
|
public function register($category, $path, $mainContextId = null)
|
|
{
|
|
$success = parent::register($category, $path, $mainContextId);
|
|
if (Application::isUnderMaintenance()) {
|
|
return true;
|
|
}
|
|
if ($success && $this->getEnabled($mainContextId)) {
|
|
$contextId = ($mainContextId === null) ? $this->getCurrentContextId() : $mainContextId;
|
|
|
|
$validator = new OrcidValidator($this);
|
|
|
|
$clientId = $this->getSetting($contextId, 'orcidClientId');
|
|
$clientSecret = $this->getSetting($contextId, 'orcidClientSecret');
|
|
|
|
if (!$validator->validateClientSecret($clientSecret) || !$validator->validateClientId($clientId)) {
|
|
error_log(new Exception('The ORCID plugin is enabled, but its settings are invalid. In order to fix, access the plugin settings and try to save the form'));
|
|
return $success;
|
|
}
|
|
|
|
Hook::add('ArticleHandler::view', [&$this, 'submissionView']);
|
|
Hook::add('PreprintHandler::view', [&$this, 'submissionView']);
|
|
|
|
// Insert the OrcidProfileHandler to handle ORCID redirects
|
|
Hook::add('LoadHandler', [$this, 'setupCallbackHandler']);
|
|
|
|
// Register callback for Smarty filters; add CSS
|
|
Hook::add('TemplateManager::display', [$this, 'handleTemplateDisplay']);
|
|
|
|
// Add "Connect ORCID" button to PublicProfileForm
|
|
Hook::add('User::PublicProfile::AdditionalItems', [$this, 'handleUserPublicProfileDisplay']);
|
|
|
|
// Display additional ORCID access information and checkbox to send e-mail to authors in the AuthorForm
|
|
Hook::add('authorform::display', [$this, 'handleFormDisplay']);
|
|
|
|
// Send email to author, if the added checkbox was ticked
|
|
Hook::add('authorform::execute', [$this, 'handleAuthorFormExecute']);
|
|
|
|
// Handle ORCID on user registration
|
|
Hook::add('registrationform::execute', [$this, 'collectUserOrcidId']);
|
|
|
|
// Send emails to authors without ORCID id upon submission
|
|
//TODO Hook::add('submissionsubmitstep3form::execute', [$this, 'handleSubmissionSubmitStep3FormExecute']);
|
|
|
|
// Send emails to authors without authorised ORCID access on promoting a submission to copy editing. Not included in OPS.
|
|
if ($this->getSetting($contextId, 'sendMailToAuthorsOnPublication')) {
|
|
Hook::add('EditorAction::recordDecision', [$this, 'handleEditorAction']);
|
|
}
|
|
|
|
Hook::add('Publication::publish', [$this, 'handlePublicationStatusChange']);
|
|
|
|
Hook::add('ThankReviewerForm::thankReviewer', [$this, 'handleThankReviewer']);
|
|
|
|
// Add more ORCiD fields to author Schema
|
|
Hook::add('Schema::get::author', function ($hookName, $args) {
|
|
$schema = &$args[0];
|
|
|
|
$schema->properties->orcidSandbox = (object)[
|
|
'type' => 'string',
|
|
'apiSummary' => true,
|
|
'validation' => ['nullable']
|
|
];
|
|
$schema->properties->orcidAccessToken = (object)[
|
|
'type' => 'string',
|
|
'apiSummary' => true,
|
|
'validation' => ['nullable']
|
|
];
|
|
$schema->properties->orcidAccessScope = (object)[
|
|
'type' => 'string',
|
|
'apiSummary' => true,
|
|
'validation' => ['nullable']
|
|
];
|
|
$schema->properties->orcidRefreshToken = (object)[
|
|
'type' => 'string',
|
|
'apiSummary' => true,
|
|
'validation' => ['nullable']
|
|
];
|
|
$schema->properties->orcidAccessExpiresOn = (object)[
|
|
'type' => 'string',
|
|
'apiSummary' => true,
|
|
'validation' => ['nullable']
|
|
];
|
|
$schema->properties->orcidAccessDenied = (object)[
|
|
'type' => 'string',
|
|
'apiSummary' => true,
|
|
'validation' => ['nullable']
|
|
];
|
|
$schema->properties->orcidEmailToken = (object)[
|
|
'type' => 'string',
|
|
'apiSummary' => true,
|
|
'validation' => ['nullable']
|
|
];
|
|
$schema->properties->orcidWorkPutCode = (object)[
|
|
'type' => 'string',
|
|
'apiSummary' => true,
|
|
'validation' => ['nullable']
|
|
];
|
|
});
|
|
|
|
// Add more ORCiD fields to user Schema
|
|
Hook::add('Schema::get::user', function ($hookName, $args) {
|
|
$schema = &$args[0];
|
|
|
|
$schema->properties->orcidAccessToken = (object)[
|
|
'type' => 'string',
|
|
'apiSummary' => true,
|
|
'validation' => ['nullable']
|
|
];
|
|
$schema->properties->orcidAccessScope = (object)[
|
|
'type' => 'string',
|
|
'apiSummary' => true,
|
|
'validation' => ['nullable']
|
|
];
|
|
$schema->properties->orcidRefreshToken = (object)[
|
|
'type' => 'string',
|
|
'apiSummary' => true,
|
|
'validation' => ['nullable']
|
|
];
|
|
$schema->properties->orcidAccessExpiresOn = (object)[
|
|
'type' => 'string',
|
|
'apiSummary' => true,
|
|
'validation' => ['nullable']
|
|
];
|
|
$schema->properties->orcidAccessDenied = (object)[
|
|
'type' => 'string',
|
|
'apiSummary' => true,
|
|
'validation' => ['nullable']
|
|
];
|
|
$schema->properties->orcidReviewPutCode = (object)[
|
|
'type' => 'string',
|
|
'apiSummary' => true,
|
|
'validation' => ['nullable']
|
|
];
|
|
});
|
|
Services::get('schema')->get(PKPSchemaService::SCHEMA_USER, true);
|
|
|
|
Hook::add('Mailer::Mailables', [$this, 'addMailable']);
|
|
|
|
Hook::add('Author::edit', [$this, 'handleAuthorFormExecute']);
|
|
|
|
Hook::add('Form::config::before', [$this, 'addOrcidFormFields']);
|
|
|
|
|
|
Hook::add('Installer::postInstall', [$this, 'updateSchema']);
|
|
|
|
Hook::add('Publication::validatePublish', [$this, 'validate']);
|
|
}
|
|
|
|
return $success;
|
|
}
|
|
|
|
/**
|
|
* Load a setting for a specific journal or load it from the config.inc.php if it is specified there.
|
|
*
|
|
* @param int $contextId The id of the journal from which the plugin settings should be loaded.
|
|
* @param string $name Name of the setting.
|
|
*
|
|
* @return mixed The setting value, either from the database for this context
|
|
* or from the global configuration file.
|
|
*/
|
|
public function getSetting($contextId, $name)
|
|
{
|
|
switch ($name) {
|
|
case 'orcidProfileAPIPath':
|
|
$config_value = Config::getVar('orcid', 'api_url');
|
|
break;
|
|
case 'orcidClientId':
|
|
$config_value = Config::getVar('orcid', 'client_id');
|
|
break;
|
|
case 'orcidClientSecret':
|
|
$config_value = Config::getVar('orcid', 'client_secret');
|
|
break;
|
|
case 'country':
|
|
$config_value = Config::getVar('orcid', 'country');
|
|
break;
|
|
case 'city':
|
|
$config_value = Config::getVar('orcid', 'city');
|
|
break;
|
|
default:
|
|
return parent::getSetting($contextId, $name);
|
|
}
|
|
|
|
$config_value = $config_value ?? parent::getSetting($contextId, $name);
|
|
if ($name == 'orcidProfileAPIPath') {
|
|
if ($config_value == 'https://orcid.org/') {
|
|
$config_value = ORCID_API_URL_PUBLIC;
|
|
} elseif ($config_value == 'https://sandbox.orcid.org/') {
|
|
$config_value = ORCID_API_URL_PUBLIC_SANDBOX;
|
|
}
|
|
}
|
|
return $config_value;
|
|
}
|
|
|
|
/**
|
|
* adds orcid form fields.
|
|
*
|
|
* @param string $hookName
|
|
* @param Form $form
|
|
*/
|
|
public function addOrcidFormFields($hookName, $form): bool
|
|
{
|
|
if (!$form instanceof ContributorForm) {
|
|
return Hook::CONTINUE;
|
|
}
|
|
|
|
$form->removeField('orcid');
|
|
$form->addField(new FieldText('orcid', [
|
|
'label' => __('user.orcid'),
|
|
'optIntoEdit' => true,
|
|
'optIntoEditLabel' => __('common.override'),
|
|
'tooltip' => __('plugins.generic.orcidProfile.about.orcidExplanation'),
|
|
|
|
]), [FIELD_POSITION_AFTER, 'url']);
|
|
|
|
$form->addField(new FieldOptions('requestOrcidAuthorization', [
|
|
'label' => __('plugins.generic.orcidProfile.verify.title'),
|
|
'options' => [
|
|
[
|
|
'label' => __('plugins.generic.orcidProfile.author.requestAuthorization'),
|
|
'value' > false,
|
|
]
|
|
]
|
|
]), [FIELD_POSITION_AFTER, 'orcid']);
|
|
|
|
$form->addField(
|
|
new FieldOptions('deleteORCID', [
|
|
'label' => __('plugins.generic.orcidProfile.displayName'),
|
|
'options' => [
|
|
[
|
|
'label' => __('plugins.generic.orcidProfile.author.deleteORCID'),
|
|
'value' > false,
|
|
]
|
|
],
|
|
'showWhen' => 'orcid',
|
|
]),
|
|
[FIELD_POSITION_AFTER, 'orcid']
|
|
);
|
|
|
|
return Hook::CONTINUE;
|
|
}
|
|
|
|
/**
|
|
* @param string $hookName
|
|
* @param array $args
|
|
*/
|
|
public function handleThankReviewer($hookName, $args)
|
|
{
|
|
$request = PKPApplication::get()->getRequest();
|
|
$context = $request->getContext();
|
|
$newPublication = & $args[0];
|
|
if ($this->isMemberApiEnabled($this->currentContextId)) {
|
|
if ($this->getSetting($context->getId(), 'country') && $this->getSetting($context->getId(), 'city')) {
|
|
$this->publishReviewerWorkToOrcid($newPublication, $request);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return bool True if the ORCID Member API has been selected in this context.
|
|
*/
|
|
public function isMemberApiEnabled($contextId)
|
|
{
|
|
$apiUrl = $this->getSetting($contextId, 'orcidProfileAPIPath');
|
|
if ($apiUrl === ORCID_API_URL_MEMBER || $apiUrl === ORCID_API_URL_MEMBER_SANDBOX) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return JSONMessage|null
|
|
*/
|
|
public function publishReviewerWorkToOrcid(Submission $submission, Request $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 orcid service');
|
|
return new JSONMessage(false, __('common.sandbox'));
|
|
}
|
|
|
|
$context = $request->getContext();
|
|
$requestVars = $request->getUserVars();
|
|
/** @var ReviewAssignmentDAO */
|
|
$reviewAssignmentDao = DAORegistry::getDAO('ReviewAssignmentDAO');
|
|
$reviewAssignmentId = $requestVars['reviewAssignmentId'];
|
|
if (isset($reviewAssignmentId)) {
|
|
$review = $reviewAssignmentDao->getById($reviewAssignmentId);
|
|
$reviewer = Repo::user()->get($review->getData('reviewerId'));
|
|
|
|
if ($reviewer->getOrcid() && $reviewer->getData('orcidAccessToken')) {
|
|
$orcidAccessExpiresOn = Carbon::parse($reviewer->getData('orcidAccessExpiresOn'));
|
|
if ($orcidAccessExpiresOn->isFuture()) {
|
|
# Extract only the ORCID from the stored ORCID uri
|
|
$orcid = basename(parse_url($reviewer->getOrcid(), PHP_URL_PATH));
|
|
$orcidReview = $this->buildOrcidReview($submission, $review, $request);
|
|
$uri = $this->getSetting($context->getId(), 'orcidProfileAPIPath') . ORCID_API_VERSION_URL . $orcid . '/' . ORCID_REVIEW_URL;
|
|
$method = 'POST';
|
|
if ($putCode = $reviewer->getData('orcidReviewPutCode')) {
|
|
$uri .= '/' . $putCode;
|
|
$method = 'PUT';
|
|
$orcidReview['put-code'] = $putCode;
|
|
}
|
|
$headers = [
|
|
'Content-Type' => ' application/vnd.orcid+json; qs=4',
|
|
'Accept' => 'application/json',
|
|
'Authorization' => 'Bearer ' . $reviewer->getData('orcidAccessToken')
|
|
];
|
|
$httpClient = Application::get()->getHttpClient();
|
|
|
|
try {
|
|
$response = $httpClient->request(
|
|
$method,
|
|
$uri,
|
|
[
|
|
'headers' => $headers,
|
|
'json' => $orcidReview,
|
|
'allow_redirects' => ['strict' => true],
|
|
]
|
|
);
|
|
} catch (ClientException $exception) {
|
|
$reason = $exception->getResponse()->getBody();
|
|
$this->logInfo("Publication fail: {$reason}");
|
|
return new JSONMessage(false);
|
|
}
|
|
$httpStatus = $response->getStatusCode();
|
|
$this->logInfo("Response status: {$httpStatus}");
|
|
$responseHeaders = $response->getHeaders();
|
|
switch ($httpStatus) {
|
|
case 200:
|
|
$this->logInfo("Review updated in profile, putCode: {$putCode}");
|
|
break;
|
|
case 201:
|
|
$location = $responseHeaders['Location'][0];
|
|
// Extract the ORCID work put code for updates/deletion.
|
|
$putCode = basename(parse_url($location, PHP_URL_PATH));
|
|
$reviewer->setData('orcidReviewPutCode', $putCode);
|
|
Repo::user()->edit($reviewer, ['orcidReviewPutCode']);
|
|
$this->logInfo("Review added to profile, putCode: {$putCode}");
|
|
break;
|
|
default:
|
|
$this->logError("Unexpected status {$httpStatus} response, body: {$responseHeaders}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function buildOrcidReview($submission, $review, $request, $issue = null)
|
|
{
|
|
$publicationUrl = $request->getDispatcher()->url($request, PKPApplication::ROUTE_PAGE, null, 'article', 'view', $submission->getId());
|
|
$context = $request->getContext();
|
|
$publicationLocale = ($submission->getData('locale')) ? $submission->getData('locale') : 'en';
|
|
$pubIdPlugins = PluginRegistry::loadCategory('pubIds', true, $context->getId()); // DO not remove
|
|
$supportedSubmissionLocales = $context->getSupportedSubmissionLocales();
|
|
|
|
if (!empty($review->getData('dateCompleted')) && $context->getData('onlineIssn')) {
|
|
$reviewCompletionDate = Carbon::parse($review->getData('dateCompleted'));
|
|
|
|
$orcidReview = [
|
|
'reviewer-role' => 'reviewer',
|
|
'review-type' => 'review',
|
|
'review-completion-date' => [
|
|
'year' => [
|
|
'value' => $reviewCompletionDate->format('Y')
|
|
],
|
|
'month' => [
|
|
'value' => $reviewCompletionDate->format('m')
|
|
],
|
|
'day' => [
|
|
'value' => $reviewCompletionDate->format('d')
|
|
]
|
|
],
|
|
'review-group-id' => 'issn:' . $context->getData('onlineIssn'),
|
|
|
|
'convening-organization' => [
|
|
'name' => $context->getData('publisherInstitution'),
|
|
'address' => [
|
|
'city' => $this->getSetting($context->getId(), 'city'),
|
|
'country' => $this->getSetting($context->getId(), 'country')
|
|
|
|
]
|
|
],
|
|
'review-identifiers' => ['external-id' => [
|
|
[
|
|
'external-id-type' => 'source-work-id',
|
|
'external-id-value' => $review->getData('reviewRoundId'),
|
|
'external-id-relationship' => 'part-of']
|
|
]]
|
|
];
|
|
if ($review->getReviewMethod() == ReviewAssignment::SUBMISSION_REVIEW_METHOD_OPEN) {
|
|
$orcidReview['subject-url'] = ['value' => $publicationUrl];
|
|
$orcidReview['review-url'] = ['value' => $publicationUrl];
|
|
$orcidReview['subject-type'] = 'journal-article';
|
|
$orcidReview['subject-name'] = [
|
|
'title' => ['value' => $submission->getCurrentPublication()->getLocalizedData('title') ?? '']
|
|
];
|
|
|
|
|
|
if (!empty($submission->getData('pub-id::doi'))) {
|
|
$externalIds = [
|
|
|
|
'external-id-type' => 'doi',
|
|
'external-id-value' => $submission->getData('pub-id::doi'),
|
|
'external-id-url' => [
|
|
'value' => 'https://doi.org/' . $submission->getData('pub-id::doi')
|
|
],
|
|
'external-id-relationship' => 'self'
|
|
|
|
];
|
|
$orcidReview['subject-external-identifier'] = $externalIds;
|
|
}
|
|
}
|
|
|
|
$translatedTitleAvailable = false;
|
|
foreach ($supportedSubmissionLocales as $defaultLanguage) {
|
|
if ($defaultLanguage !== $publicationLocale) {
|
|
$iso2LanguageCode = substr($defaultLanguage, 0, 2);
|
|
$defaultTitle = $submission->getLocalizedData($iso2LanguageCode);
|
|
if (strlen($defaultTitle) > 0 && !$translatedTitleAvailable) {
|
|
$orcidReview['subject-name']['translated-title'] = ['value' => $defaultTitle, 'language-code' => $iso2LanguageCode];
|
|
$translatedTitleAvailable = true;
|
|
}
|
|
}
|
|
}
|
|
return $orcidReview;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write info message to log.
|
|
*
|
|
* @param string $message Message to write
|
|
*/
|
|
public function logInfo($message)
|
|
{
|
|
if ($this->getSetting($this->currentContextId, 'logLevel') === 'ERROR') {
|
|
return;
|
|
}
|
|
self::writeLog($message, 'INFO');
|
|
}
|
|
|
|
/**
|
|
* Write error message to log.
|
|
*
|
|
* @param string $message Message to write
|
|
*/
|
|
public function logError($message)
|
|
{
|
|
if ($this->getSetting($this->currentContextId, 'logLevel') === 'ERROR') {
|
|
return;
|
|
}
|
|
self::writeLog($message, 'ERROR');
|
|
}
|
|
|
|
/**
|
|
* Write a message with specified level to log
|
|
*
|
|
* @param string $message Message to write
|
|
* @param string $level Error level to add to message
|
|
*/
|
|
private static function writeLog($message, $level)
|
|
{
|
|
$fineStamp = date('Y-m-d H:i:s') . substr(microtime(), 1, 4);
|
|
error_log("{$fineStamp} {$level} {$message}\n", 3, self::logFilePath());
|
|
}
|
|
|
|
/**
|
|
* @return string Path to a custom ORCID log file.
|
|
*/
|
|
public static function logFilePath()
|
|
{
|
|
return Config::getVar('files', 'files_dir') . '/orcid.log';
|
|
}
|
|
|
|
/**
|
|
* Hook callback: register pages for each sushi-lite method
|
|
* This URL is of the form: orcidapi/{$orcidrequest}
|
|
*
|
|
* @see PKPPageRouter::route()
|
|
*/
|
|
public function setupCallbackHandler($hookName, $params)
|
|
{
|
|
$page = $params[0];
|
|
if ($this->getEnabled() && $page == 'orcidapi') {
|
|
define('HANDLER_CLASS', OrcidProfileHandler::class);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if there exist a valid orcid configuration section in the global config.inc.php of OJS.
|
|
*
|
|
* @return boolean True, if the config file has api_url, client_id and client_secret set in an [orcid] section
|
|
*/
|
|
public function isGloballyConfigured()
|
|
{
|
|
$apiUrl = Config::getVar('orcid', 'api_url');
|
|
$clientId = Config::getVar('orcid', 'client_id');
|
|
$clientSecret = Config::getVar('orcid', 'client_secret');
|
|
return isset($apiUrl) && trim($apiUrl) && isset($clientId) && trim($clientId) &&
|
|
isset($clientSecret) && trim($clientSecret);
|
|
}
|
|
|
|
/**
|
|
* Hook callback to handle form display.
|
|
* Registers output filter for public user profile and author form.
|
|
*
|
|
* @param string $hookName
|
|
* @param Form[] $args
|
|
*
|
|
* @return bool
|
|
*
|
|
* @see Form::display()
|
|
*
|
|
*/
|
|
public function handleFormDisplay($hookName, $args)
|
|
{
|
|
//TODO
|
|
$request = Application::get()->getRequest();
|
|
$templateMgr = TemplateManager::getManager($request);
|
|
switch ($hookName) {
|
|
case 'authorform::display':
|
|
/** @var AuthorForm */
|
|
$authorForm = &$args[0];
|
|
$author = $authorForm->getAuthor();
|
|
if ($author) {
|
|
$authenticated = !empty($author->getData('orcidAccessToken'));
|
|
$templateMgr->assign(
|
|
[
|
|
'orcidAccessToken' => $author->getData('orcidAccessToken'),
|
|
'orcidAccessScope' => $author->getData('orcidAccessScope'),
|
|
'orcidAccessExpiresOn' => $author->getData('orcidAccessExpiresOn'),
|
|
'orcidAccessDenied' => $author->getData('orcidAccessDenied'),
|
|
'orcidAuthenticated' => $authenticated
|
|
]
|
|
);
|
|
}
|
|
|
|
$templateMgr->registerFilter('output', [$this, 'authorFormFilter']);
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Output filter adds ORCiD interaction to contributors metadata add/edit form.
|
|
*
|
|
* @param $output string
|
|
* @param $templateMgr TemplateManager
|
|
* @return string
|
|
*/
|
|
function authorFormFilter($output, $templateMgr) {
|
|
if (preg_match('/<input[^>]+name="submissionId"[^>]*>/', $output, $matches, PREG_OFFSET_CAPTURE)) {
|
|
$match = $matches[0][0];
|
|
$offset = $matches[0][1];
|
|
$templateMgr->assign('orcidIcon', $this->getIcon());
|
|
$newOutput = substr($output, 0, $offset + strlen($match));
|
|
$newOutput .= $templateMgr->fetch($this->getTemplateResource('authorFormOrcid.tpl'));
|
|
$newOutput .= substr($output, $offset + strlen($match));
|
|
$output = $newOutput;
|
|
$templateMgr->unregisterFilter('output', [$this, 'authorFormFilter']);
|
|
}
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* Hook callback: register output filter for user registration and article display.
|
|
*
|
|
* @param string $hookName
|
|
* @param array $args
|
|
*
|
|
* @return bool
|
|
*
|
|
* @see TemplateManager::display()
|
|
*
|
|
*/
|
|
public function handleTemplateDisplay($hookName, $args)
|
|
{
|
|
//TODO orcid
|
|
$templateMgr = &$args[0];
|
|
$template = &$args[1];
|
|
$request = Application::get()->getRequest();
|
|
|
|
// Assign our private stylesheet, for front and back ends.
|
|
$templateMgr->addStyleSheet(
|
|
'orcidProfile',
|
|
$request->getBaseUrl() . '/' . $this->getStyleSheet(),
|
|
[
|
|
'contexts' => ['frontend', 'backend']
|
|
]
|
|
);
|
|
|
|
switch ($template) {
|
|
case 'frontend/pages/userRegister.tpl':
|
|
$templateMgr->registerFilter('output', [$this, 'registrationFilter']);
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return the location of the plugin's CSS file
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getStyleSheet()
|
|
{
|
|
return $this->getPluginPath() . '/css/orcidProfile.css';
|
|
}
|
|
|
|
public function isSandbox()
|
|
{
|
|
$apiUrl = $this->getSetting($this->getCurrentContextId(), 'orcidProfileAPIPath');
|
|
return ($apiUrl == ORCID_API_URL_MEMBER_SANDBOX);
|
|
}
|
|
|
|
/**
|
|
* Output filter adds ORCiD interaction to registration form.
|
|
*
|
|
* @param string $output
|
|
* @param TemplateManager $templateMgr
|
|
*
|
|
* @return string
|
|
*/
|
|
public function registrationFilter($output, $templateMgr)
|
|
{
|
|
if (preg_match('/<form[^>]+id="register"[^>]+>/', $output, $matches, PREG_OFFSET_CAPTURE)) {
|
|
$match = $matches[0][0];
|
|
$offset = $matches[0][1];
|
|
$targetOp = 'register';
|
|
$templateMgr->assign([
|
|
'targetOp' => $targetOp,
|
|
'orcidUrl' => $this->getOrcidUrl(),
|
|
'orcidOAuthUrl' => $this->buildOAuthUrl('orcidAuthorize', ['targetOp' => $targetOp]),
|
|
'orcidIcon' => $this->getIcon(),
|
|
]);
|
|
|
|
$newOutput = substr($output, 0, $offset + strlen($match));
|
|
$newOutput .= $templateMgr->fetch($this->getTemplateResource('orcidProfile.tpl'));
|
|
$newOutput .= substr($output, $offset + strlen($match));
|
|
$output = $newOutput;
|
|
$templateMgr->unregisterFilter('output', [$this, 'registrationFilter']);
|
|
}
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* Return the ORCID website url (prod or sandbox) based on the current API configuration
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getOrcidUrl()
|
|
{
|
|
$request = Application::get()->getRequest();
|
|
$context = $request->getContext();
|
|
$contextId = ($context == null) ? 0 : $context->getId();
|
|
|
|
$apiPath = $this->getSetting($contextId, 'orcidProfileAPIPath');
|
|
return in_array($apiPath, [ORCID_API_URL_PUBLIC, ORCID_API_URL_MEMBER]) ? ORCID_URL : ORCID_URL_SANDBOX;
|
|
}
|
|
|
|
/**
|
|
* Return an ORCID OAuth authorization link with
|
|
*
|
|
* @param string $handlerMethod containting a valid method of the OrcidProfileHandler
|
|
* @param array $redirectParams associative array with additional request parameters for the redirect URL
|
|
*/
|
|
public function buildOAuthUrl($handlerMethod, $redirectParams)
|
|
{
|
|
$request = Application::get()->getRequest();
|
|
$context = $request->getContext();
|
|
// This should only ever happen within a context, never site-wide.
|
|
assert($context != null);
|
|
$contextId = $context->getId();
|
|
|
|
if ($this->isMemberApiEnabled($contextId)) {
|
|
$scope = ORCID_API_SCOPE_MEMBER;
|
|
} else {
|
|
$scope = ORCID_API_SCOPE_PUBLIC;
|
|
}
|
|
// We need to construct a page url, but the request is using the component router.
|
|
// Use the Dispatcher to construct the url and set the page router.
|
|
$redirectUrl = $request->getDispatcher()->url(
|
|
$request,
|
|
Application::ROUTE_PAGE,
|
|
null,
|
|
'orcidapi',
|
|
$handlerMethod,
|
|
null,
|
|
$redirectParams
|
|
);
|
|
|
|
return $this->getOauthPath() . 'authorize?' . http_build_query(
|
|
[
|
|
'client_id' => $this->getSetting($contextId, 'orcidClientId'),
|
|
'response_type' => 'code',
|
|
'scope' => $scope,
|
|
'redirect_uri' => $redirectUrl]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Return the OAUTH path (prod or sandbox) based on the current API configuration
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getOauthPath()
|
|
{
|
|
return $this->getOrcidUrl() . 'oauth/';
|
|
}
|
|
|
|
/**
|
|
* Return a string of the ORCiD SVG icon
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getIcon()
|
|
{
|
|
$path = Core::getBaseDir() . '/' . $this->getPluginPath() . '/templates/images/orcid.svg';
|
|
return file_exists($path) ? file_get_contents($path) : '';
|
|
}
|
|
|
|
/**
|
|
* Renders additional content for the PublicProfileForm.
|
|
*
|
|
* Called by @param string $output
|
|
*
|
|
*
|
|
* @return bool
|
|
*
|
|
* @see lib/pkp/templates/user/publicProfileForm.tpl
|
|
*
|
|
*/
|
|
public function handleUserPublicProfileDisplay($hookName, $params)
|
|
{
|
|
$templateMgr = &$params[1];
|
|
$output = &$params[2];
|
|
$request = Application::get()->getRequest();
|
|
$context = $request->getContext();
|
|
$userId = $request->getUser()->getId();
|
|
$user = Repo::user()->get($userId);
|
|
$contextId = ($context == null) ? 0 : $context->getId();
|
|
$targetOp = 'profile';
|
|
$templateMgr->assign(
|
|
[
|
|
'targetOp' => $targetOp,
|
|
'orcidUrl' => $this->getOrcidUrl(),
|
|
'orcidOAuthUrl' => $this->buildOAuthUrl('orcidAuthorize', ['targetOp' => $targetOp]),
|
|
'orcidClientId' => $this->getSetting($contextId, 'orcidClientId'),
|
|
'orcidIcon' => $this->getIcon(),
|
|
'orcidAuthenticated' => !empty($user->getData('orcidAccessToken')),
|
|
]
|
|
);
|
|
|
|
$output = $templateMgr->fetch($this->getTemplateResource('orcidProfile.tpl'));
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* handleAuthorFormExecute sends an e-mail to the author if a specific checkbox was ticked in the author form.
|
|
*
|
|
* @param string $hookname
|
|
* @param AuthorForm[] $args
|
|
*
|
|
* @see AuthorForm::execute() The function calling the hook.
|
|
*
|
|
*/
|
|
public function handleAuthorFormExecute($hookname, $args)
|
|
{
|
|
if (count($args) == 3) {
|
|
/** @var Author */
|
|
$author = &$args[0];
|
|
$values = $args[2];
|
|
|
|
if ($author && $values['requestOrcidAuthorization']) {
|
|
$this->sendAuthorMail($author);
|
|
}
|
|
|
|
if ($author && $values['deleteORCID']) {
|
|
$author->setOrcid(null);
|
|
$this->removeOrcidAccessToken($author, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send mail with ORCID authorization link to the e-mail address of the supplied Author object.
|
|
*
|
|
* @param Author $author
|
|
* @param bool $updateAuthor If true update the author fields in the database.
|
|
* Use this only if not called from a function, which does this anyway.
|
|
*/
|
|
public function sendAuthorMail($author, $updateAuthor = false)
|
|
{
|
|
$request = Application::get()->getRequest();
|
|
$context = $request->getContext();
|
|
|
|
// This should only ever happen within a context, never site-wide.
|
|
if ($context != null) {
|
|
$contextId = $context->getId();
|
|
$publicationId = $author->getData('publicationId');
|
|
$publication = Repo::publication()->get($publicationId);
|
|
$submission = Repo::submission()->get($publication->getData('submissionId'));
|
|
|
|
$emailToken = md5(microtime() . $author->getEmail());
|
|
$author->setData('orcidEmailToken', $emailToken);
|
|
$oauthUrl = $this->buildOAuthUrl('orcidVerify', ['token' => $emailToken, 'state' => $publicationId]);
|
|
|
|
if ($this->isMemberApiEnabled($contextId)) {
|
|
$mailable = new OrcidRequestAuthorAuthorization($context, $submission, $oauthUrl);
|
|
} else {
|
|
$mailable = new OrcidCollectAuthorId($context, $submission, $oauthUrl);
|
|
}
|
|
|
|
// Set From to primary journal contact
|
|
$mailable->from($context->getData('contactEmail'), $context->getData('contactName'));
|
|
|
|
// Send to author
|
|
$mailable->recipients([$author]);
|
|
$emailTemplateKey = $mailable::getEmailTemplateKey();
|
|
$emailTemplate = Repo::emailTemplate()->getByKey($contextId, $emailTemplateKey);
|
|
$mailable->body($emailTemplate->getLocalizedData('body'))
|
|
->subject($emailTemplate->getLocalizedData('subject'));
|
|
Mail::send($mailable);
|
|
|
|
if ($updateAuthor) {
|
|
Repo::author()->dao->update($author);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove all data fields, which belong to an ORCID access token from the
|
|
* given Author object. Also updates fields in the db.
|
|
*
|
|
* @param Author $author object with ORCID access token
|
|
*/
|
|
public function removeOrcidAccessToken($author, $saveAuthor = true)
|
|
{
|
|
$author->setData('orcidAccessToken', null);
|
|
$author->setData('orcidAccessScope', null);
|
|
$author->setData('orcidRefreshToken', null);
|
|
$author->setData('orcidAccessExpiresOn', null);
|
|
$author->setData('orcidSandbox', null);
|
|
|
|
if ($saveAuthor) {
|
|
Repo::author()->dao->update($author);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collect the ORCID when registering a user.
|
|
*
|
|
* @param string $hookName
|
|
* @param array $params
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function collectUserOrcidId($hookName, $params)
|
|
{
|
|
$form = $params[0];
|
|
$user = $form->user;
|
|
|
|
$form->readUserVars(['orcid']);
|
|
$user->setOrcid($form->getData('orcid'));
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Output filter adds ORCiD interaction to the 3rd step submission form.
|
|
*
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function handleSubmissionSubmitStep3FormExecute($hookName, $params)
|
|
{
|
|
$form = $params[0];
|
|
// Have to use global Request access because request is not passed to hook
|
|
$publication = Repo::publication()->get($form->submission->getData('currentPublicationId'));
|
|
$authors = $publication->getData('authors');
|
|
|
|
$request = Application::get()->getRequest();
|
|
$user = $request->getUser();
|
|
$author = $authors->first();
|
|
//error_log("OrcidProfilePlugin: authors[0] = " . var_export($authors[0], true));
|
|
//error_log("OrcidProfilePlugin: user = " . var_export($user, true));
|
|
if ($author?->getOrcid() === $user->getOrcid()) {
|
|
// if the author and user share the same ORCID id
|
|
// copy the access token from the user
|
|
//error_log("OrcidProfilePlugin: user->orcidAccessToken = " . $user->getData('orcidAccessToken'));
|
|
$author->setData('orcidAccessToken', $user->getData('orcidAccessToken'));
|
|
$author->setData('orcidAccessScope', $user->getData('orcidAccessScope'));
|
|
$author->setData('orcidRefreshToken', $user->getData('orcidRefreshToken'));
|
|
$author->setData('orcidAccessExpiresOn', $user->getData('orcidAccessExpiresOn'));
|
|
$author->setData('orcidSandbox', $user->getData('orcidSandbox'));
|
|
|
|
Repo::author()->dao->update($author);
|
|
|
|
//error_log("OrcidProfilePlugin: author = " . var_export($authors[0], true));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Add additional ORCID specific fields to the Author and User objects
|
|
*
|
|
* @param string $hookName
|
|
* @param array $params
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function handleAdditionalFieldNames($hookName, $params)
|
|
{
|
|
$fields = &$params[1];
|
|
$fields[] = 'orcidSandbox';
|
|
$fields[] = 'orcidAccessToken';
|
|
$fields[] = 'orcidAccessScope';
|
|
$fields[] = 'orcidRefreshToken';
|
|
$fields[] = 'orcidAccessExpiresOn';
|
|
$fields[] = 'orcidAccessDenied';
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @copydoc Plugin::getDescription()
|
|
*/
|
|
public function getDescription()
|
|
{
|
|
return __('plugins.generic.orcidProfile.description');
|
|
}
|
|
|
|
/**
|
|
* @see PKPPlugin::getInstallEmailTemplatesFile()
|
|
*/
|
|
public function getInstallEmailTemplatesFile()
|
|
{
|
|
return ($this->getPluginPath() . '/emailTemplates.xml');
|
|
}
|
|
|
|
/**
|
|
* Extend the {url ...} smarty to support this plugin.
|
|
*/
|
|
public function smartyPluginUrl($params, $smarty)
|
|
{
|
|
$path = [$this->getCategory(), $this->getName()];
|
|
if (is_array($params['path'])) {
|
|
$params['path'] = array_merge($path, $params['path']);
|
|
} elseif (!empty($params['path'])) {
|
|
$params['path'] = array_merge($path, [$params['path']]);
|
|
} else {
|
|
$params['path'] = $path;
|
|
}
|
|
|
|
if (!empty($params['id'])) {
|
|
$params['path'] = array_merge($params['path'], [$params['id']]);
|
|
unset($params['id']);
|
|
}
|
|
return $smarty->smartyUrl($params, $smarty);
|
|
}
|
|
|
|
public function submissionView($hookName, $args)
|
|
{
|
|
$request = $args[0];
|
|
$templateMgr = TemplateManager::getManager($request);
|
|
$templateMgr->assign(['orcidIcon' => $this->getIcon()]);
|
|
}
|
|
|
|
/**
|
|
* @see Plugin::getActions()
|
|
*/
|
|
public function getActions($request, $actionArgs)
|
|
{
|
|
$router = $request->getRouter();
|
|
return array_merge(
|
|
$this->getEnabled() ? [
|
|
new LinkAction(
|
|
'settings',
|
|
new AjaxModal(
|
|
$router->url(
|
|
$request,
|
|
null,
|
|
null,
|
|
'manage',
|
|
null,
|
|
[
|
|
'verb' => 'settings',
|
|
'plugin' => $this->getName(),
|
|
'category' => 'generic'
|
|
]
|
|
),
|
|
$this->getDisplayName()
|
|
),
|
|
__('manager.plugins.settings'),
|
|
null
|
|
),
|
|
new LinkAction(
|
|
'status',
|
|
new AjaxModal($router->url($request, null, null, 'manage', null, ['verb' => 'status', 'plugin' => $this->getName(), 'category' => 'generic']), $this->getDisplayName()),
|
|
__('common.status'),
|
|
null
|
|
)
|
|
] : [],
|
|
parent::getActions($request, $actionArgs)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @see Plugin::manage()
|
|
*/
|
|
public function getDisplayName()
|
|
{
|
|
return __('plugins.generic.orcidProfile.displayName');
|
|
}
|
|
|
|
public function manage($args, $request)
|
|
{
|
|
$context = $request->getContext();
|
|
$contextId = ($context == null) ? 0 : $context->getId();
|
|
|
|
switch ($request->getUserVar('verb')) {
|
|
case 'settings':
|
|
|
|
$templateMgr = TemplateManager::getManager($request);
|
|
$templateMgr->registerPlugin('function', 'plugin_url', [$this, 'smartyPluginUrl']);
|
|
|
|
$templateMgr->assign('orcidApiUrls', [
|
|
ORCID_API_URL_PUBLIC => 'plugins.generic.orcidProfile.manager.settings.orcidProfileAPIPath.public',
|
|
ORCID_API_URL_PUBLIC_SANDBOX => 'plugins.generic.orcidProfile.manager.settings.orcidProfileAPIPath.publicSandbox',
|
|
ORCID_API_URL_MEMBER => 'plugins.generic.orcidProfile.manager.settings.orcidProfileAPIPath.member',
|
|
ORCID_API_URL_MEMBER_SANDBOX => 'plugins.generic.orcidProfile.manager.settings.orcidProfileAPIPath.memberSandbox'
|
|
]);
|
|
|
|
$countries = collect(Locale::getCountries())
|
|
->mapWithKeys(fn (Country $country) => [$country->getAlpha2() => $country->getLocalName()])
|
|
->sort(fn (string $a, string $b) => strcoll($a, $b))
|
|
->toArray();
|
|
|
|
$templateMgr->assign('countries', $countries);
|
|
$templateMgr->assign('logLevelOptions', [
|
|
'ERROR' => 'plugins.generic.orcidProfile.manager.settings.logLevel.error',
|
|
'ALL' => 'plugins.generic.orcidProfile.manager.settings.logLevel.all'
|
|
]);
|
|
|
|
$form = new OrcidProfileSettingsForm($this, $contextId);
|
|
if ($request->getUserVar('save')) {
|
|
$form->readInputData();
|
|
if ($form->validate()) {
|
|
$form->execute();
|
|
return new JSONMessage(true);
|
|
}
|
|
} else {
|
|
$form->initData();
|
|
}
|
|
return new JSONMessage(true, $form->fetch($request));
|
|
case 'status':
|
|
$form = new OrcidProfileStatusForm($this, $contextId);
|
|
$form->initData();
|
|
return new JSONMessage(true, $form->fetch($request));
|
|
}
|
|
return parent::manage($args, $request);
|
|
}
|
|
|
|
/**
|
|
* handlePublishIssue sends all submissions for which the authors hava an ORCID and access token
|
|
* to ORCID. This hook will be called on publication of a new issue.
|
|
*
|
|
* @param string $hookName
|
|
* @param Issue[] $args Issue object that will be published
|
|
*
|
|
**@see
|
|
*
|
|
*/
|
|
public function handlePublicationStatusChange($hookName, $args)
|
|
{
|
|
/** @var Publication $newPublication */
|
|
$newPublication = &$args[0];
|
|
|
|
$request = Application::get()->getRequest();
|
|
|
|
switch ($newPublication->getData('status')) {
|
|
case PKPSubmission::STATUS_PUBLISHED:
|
|
$this->sendSubmissionToOrcid($newPublication, $request);
|
|
break;
|
|
case PKPSubmission::STATUS_SCHEDULED:
|
|
$this->sendSubmissionToOrcid($newPublication, $request);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* sendSubmissionToOrcid posts JSON consisting of submission, journal and issue meta data
|
|
* to ORCID profiles of submission authors.
|
|
*
|
|
* @see https://github.com/ORCID/ORCID-Source/tree/master/orcid-model/src/main/resources/record_2.1
|
|
* for documentation and examples of the ORCID JSON format.
|
|
*
|
|
* @param Publication $publication Publication for which the data will be sent to ORCID
|
|
*
|
|
* @return bool|bool[]|JSONMessage
|
|
*
|
|
**/
|
|
public function sendSubmissionToOrcid($publication, $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 orcid service');
|
|
return new JSONMessage(false, __('common.sandbox'));
|
|
}
|
|
|
|
$context = $request->getContext();
|
|
$contextId = $this->currentContextId = $context->getId();
|
|
$publicationId = $publication->getId();
|
|
$submissionId = $publication->getData('submissionId');
|
|
|
|
if (!$this->isMemberApiEnabled($contextId)) {
|
|
// Sending to ORCID only works with the member API
|
|
return false;
|
|
}
|
|
|
|
$issueId = $publication->getData('issueId');
|
|
if (isset($issueId)) {
|
|
$issue = Repo::issue()->get($issueId);
|
|
}
|
|
|
|
$authors = Repo::author()
|
|
->getCollector()
|
|
->filterByPublicationIds([$publicationId])
|
|
->getMany();
|
|
|
|
// Collect valid ORCID ids and access tokens from submission contributors
|
|
$authorsWithOrcid = [];
|
|
foreach ($authors as $author) {
|
|
if ($author->getOrcid() && $author->getData('orcidAccessToken')) {
|
|
$orcidAccessExpiresOn = Carbon::parse($author->getData('orcidAccessExpiresOn'));
|
|
if ($orcidAccessExpiresOn->isFuture()) {
|
|
# Extract only the ORCID from the stored ORCID uri
|
|
$orcid = basename(parse_url($author->getOrcid(), PHP_URL_PATH));
|
|
$authorsWithOrcid[$orcid] = $author;
|
|
} else {
|
|
$this->logError("Token expired on {$orcidAccessExpiresOn} for author " . $author->getId() . ', deleting orcidAccessToken!');
|
|
$this->removeOrcidAccessToken($author);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (empty($authorsWithOrcid)) {
|
|
$this->logInfo('No contributor with ORICD id or valid access token for submission ' . $submissionId);
|
|
return false;
|
|
}
|
|
|
|
$orcidWork = $this->buildOrcidWork($publication, $context, $authors->toArray(), $request, $issue);
|
|
$this->logInfo('Request body (without put-code): ' . json_encode($orcidWork));
|
|
|
|
$requestsSuccess = [];
|
|
foreach ($authorsWithOrcid as $orcid => $author) {
|
|
$uri = $this->getSetting($contextId, 'orcidProfileAPIPath') . ORCID_API_VERSION_URL . $orcid . '/' . ORCID_WORK_URL;
|
|
$method = 'POST';
|
|
|
|
if ($putCode = $author->getData('orcidWorkPutCode')) {
|
|
// Submission has already been sent to ORCID. Use PUT to update meta data
|
|
$uri .= '/' . $putCode;
|
|
$method = 'PUT';
|
|
$orcidWork['put-code'] = $putCode;
|
|
} else {
|
|
// Remove put-code from body because the work has not yet been sent
|
|
unset($orcidWork['put-code']);
|
|
}
|
|
|
|
$headers = [
|
|
'Content-type: application/vnd.orcid+json',
|
|
'Accept' => 'application/json',
|
|
'Authorization' => 'Bearer ' . $author->getData('orcidAccessToken')
|
|
];
|
|
|
|
$this->logInfo("{$method} {$uri}");
|
|
$this->logInfo('Header: ' . var_export($headers, true));
|
|
|
|
$httpClient = Application::get()->getHttpClient();
|
|
try {
|
|
$response = $httpClient->request(
|
|
$method,
|
|
$uri,
|
|
[
|
|
'headers' => $headers,
|
|
'json' => $orcidWork,
|
|
'allow_redirects' => ['strict' => true],
|
|
]
|
|
);
|
|
} catch (ClientException $exception) {
|
|
$reason = $exception->getResponse()->getBody();
|
|
$this->logInfo("Publication fail: {$reason}");
|
|
return new JSONMessage(false);
|
|
}
|
|
$httpstatus = $response->getStatusCode();
|
|
$this->logInfo("Response status: {$httpstatus}");
|
|
$responseHeaders = $response->getHeaders();
|
|
|
|
switch ($httpstatus) {
|
|
case 200:
|
|
// Work updated
|
|
$this->logInfo("Work updated in profile, putCode: {$putCode}");
|
|
$requestsSuccess[$orcid] = true;
|
|
break;
|
|
case 201:
|
|
$location = $responseHeaders['Location'][0];
|
|
// Extract the ORCID work put code for updates/deletion.
|
|
$putCode = intval(basename(parse_url($location, PHP_URL_PATH)));
|
|
$this->logInfo("Work added to profile, putCode: {$putCode}");
|
|
$author->setData('orcidWorkPutCode', $putCode);
|
|
Repo::author()->dao->update($author);
|
|
$requestsSuccess[$orcid] = true;
|
|
break;
|
|
case 401:
|
|
// invalid access token, token was revoked
|
|
$error = json_decode($response->getBody(), true);
|
|
if ($error['error'] === 'invalid_token') {
|
|
$this->logError($error['error_description'] . ', deleting orcidAccessToken from author');
|
|
$this->removeOrcidAccessToken($author);
|
|
}
|
|
$requestsSuccess[$orcid] = false;
|
|
break;
|
|
case 403:
|
|
$this->logError('Work update forbidden: ' . $response->getBody());
|
|
$requestsSuccess[$orcid] = false;
|
|
break;
|
|
case 404:
|
|
// a work has been deleted from a ORCID record. putCode is no longer valid.
|
|
if ($method === 'PUT') {
|
|
$this->logError('Work deleted from ORCID record, deleting putCode form author');
|
|
$author->setData('orcidWorkPutCode', null);
|
|
Repo::author()->dao->update($author);
|
|
$requestsSuccess[$orcid] = false;
|
|
} else {
|
|
$this->logError("Unexpected status {$httpstatus} response, body: " . $response->getBody());
|
|
$requestsSuccess[$orcid] = false;
|
|
}
|
|
break;
|
|
case 409:
|
|
$this->logError('Work already added to profile, response body: ' . $response->getBody());
|
|
$requestsSuccess[$orcid] = false;
|
|
break;
|
|
default:
|
|
$this->logError("Unexpected status {$httpstatus} response, body: " . $response->getBody());
|
|
$requestsSuccess[$orcid] = false;
|
|
}
|
|
}
|
|
return array_product($requestsSuccess) ? true : $requestsSuccess;
|
|
}
|
|
|
|
/**
|
|
* Build an associative array with submission meta data, which can be encoded to a valid ORCID work JSON structure.
|
|
*
|
|
* @see https://github.com/ORCID/ORCID-Source/blob/master/orcid-model/src/main/resources/record_2.1/samples/write_sample/bulk-work-2.1.json
|
|
* Example of valid ORCID JSON for adding works to an ORCID record.
|
|
*
|
|
* @param Publication $publication extract data from this Article
|
|
* @param Journal $context Context object the Submission is part of
|
|
* @param Author[] $authors Array of Author objects, the contributors of the publication
|
|
* @param Issue $issue Issue the Article is part of
|
|
* @param Request $request the current request
|
|
*
|
|
* @return array an associative array with article meta data corresponding to ORCID work JSON structure
|
|
*/
|
|
public function buildOrcidWork($publication, $context, $authors, $request, $issue = null)
|
|
{
|
|
$submission = Repo::submission()->get($publication->getData('submissionId'));
|
|
|
|
$applicationName = Application::get()->getName();
|
|
$bibtexCitation = '';
|
|
|
|
$publicationLocale = ($publication->getData('locale')) ? $publication->getData('locale') : 'en';
|
|
$supportedSubmissionLocales = $context->getSupportedSubmissionLocales();
|
|
|
|
$publicationUrl = $request->getDispatcher()->url($request, Application::ROUTE_PAGE, null, 'article', 'view', $submission->getId());
|
|
|
|
$orcidWork = [
|
|
'title' => [
|
|
'title' => [
|
|
'value' => trim(strip_tags($publication->getLocalizedTitle($publicationLocale))) ?? ''
|
|
],
|
|
'subtitle' => [
|
|
'value' => trim(strip_tags($publication->getLocalizedData('subtitle', $publicationLocale))) ?? ''
|
|
]
|
|
],
|
|
'journal-title' => [
|
|
'value' => $context->getName($publicationLocale) ?? ''
|
|
],
|
|
'short-description' => trim(strip_tags($publication->getLocalizedData('abstract', $publicationLocale))) ?? '',
|
|
|
|
'external-ids' => [
|
|
'external-id' => $this->buildOrcidExternalIds($submission, $publication, $context, $issue, $publicationUrl)
|
|
],
|
|
'publication-date' => $this->buildOrcidPublicationDate($publication, $issue),
|
|
'url' => $publicationUrl,
|
|
'language-code' => substr($publicationLocale, 0, 2),
|
|
'contributors' => [
|
|
'contributor' => $this->buildOrcidContributors($authors, $context, $publication)
|
|
]
|
|
];
|
|
|
|
if ($applicationName == 'ojs2') {
|
|
PluginRegistry::loadCategory('generic');
|
|
$citationPlugin = PluginRegistry::getPlugin('generic', 'citationstylelanguageplugin');
|
|
/** @var CitationStyleLanguagePlugin $citationPlugin */
|
|
$bibtexCitation = trim(strip_tags($citationPlugin->getCitation($request, $submission, 'bibtex', $issue, $publication)));
|
|
$orcidWork['citation'] = [
|
|
'citation-type' => 'bibtex',
|
|
'citation-value' => $bibtexCitation,
|
|
];
|
|
$orcidWork['type'] = 'journal-article';
|
|
} elseif ($applicationName == 'ops') {
|
|
$orcidWork['type'] = 'preprint';
|
|
}
|
|
|
|
$translatedTitleAvailable = false;
|
|
foreach ($supportedSubmissionLocales as $defaultLanguage) {
|
|
if ($defaultLanguage !== $publicationLocale) {
|
|
$iso2LanguageCode = substr($defaultLanguage, 0, 2);
|
|
$defaultTitle = $publication->getLocalizedData($iso2LanguageCode);
|
|
if (strlen($defaultTitle) > 0 && !$translatedTitleAvailable) {
|
|
$orcidWork['title']['translated-title'] = ['value' => $defaultTitle, 'language-code' => $iso2LanguageCode];
|
|
$translatedTitleAvailable = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $orcidWork;
|
|
}
|
|
|
|
/**
|
|
* Build the external identifiers ORCID JSON structure from article, journal and issue meta data.
|
|
*
|
|
* @see https://pub.orcid.org/v2.0/identifiers Table of valid ORCID identifier types.
|
|
*
|
|
* @param Submission $submission The Article object for which the external identifiers should be build.
|
|
* @param Publication $publication The Article object for which the external identifiers should be build.
|
|
* @param Journal $context Context the Submission is part of.
|
|
* @param Issue $issue The Issue object the Article object belongs to.
|
|
*
|
|
* @return array An associative array corresponding to ORCID external-id JSON.
|
|
*/
|
|
private function buildOrcidExternalIds($submission, $publication, $context, $issue, $articleUrl)
|
|
{
|
|
$contextId = $context->getId();
|
|
|
|
$externalIds = [];
|
|
$pubIdPlugins = PluginRegistry::loadCategory('pubIds', true, $contextId);
|
|
// Add doi, urn, etc. for article
|
|
$articleHasStoredPubId = false;
|
|
if (is_array($pubIdPlugins) || $context->areDoisEnabled()) {
|
|
// Handle non-DOI pubIds
|
|
if (is_array($pubIdPlugins)) {
|
|
foreach ($pubIdPlugins as $plugin) {
|
|
if (!$plugin->getEnabled()) {
|
|
continue;
|
|
}
|
|
|
|
$pubIdType = $plugin->getPubIdType();
|
|
|
|
# Add article ids
|
|
$pubId = $publication->getStoredPubId($pubIdType);
|
|
|
|
if ($pubId) {
|
|
$externalIds[] = [
|
|
'external-id-type' => self::PUBID_TO_ORCID_EXT_ID[$pubIdType],
|
|
'external-id-value' => $pubId,
|
|
'external-id-url' => [
|
|
'value' => $plugin->getResolvingURL($contextId, $pubId)
|
|
],
|
|
'external-id-relationship' => 'self'
|
|
];
|
|
|
|
$articleHasStoredPubId = true;
|
|
}
|
|
|
|
# Add issue ids if they exist
|
|
$pubId = $issue->getStoredPubId($pubIdType);
|
|
if ($pubId) {
|
|
$externalIds[] = [
|
|
'external-id-type' => self::PUBID_TO_ORCID_EXT_ID[$pubIdType],
|
|
'external-id-value' => $pubId,
|
|
'external-id-url' => [
|
|
'value' => $plugin->getResolvingURL($contextId, $pubId)
|
|
],
|
|
'external-id-relationship' => 'part-of'
|
|
];
|
|
}
|
|
}
|
|
|
|
// Handle DOIs
|
|
if ($context->areDoisEnabled()) {
|
|
# Add article ids
|
|
$doiObject = $publication->getData('doiObject');
|
|
|
|
if ($doiObject) {
|
|
$externalIds[] = [
|
|
'external-id-type' => self::PUBID_TO_ORCID_EXT_ID['doi'],
|
|
'external-id-value' => $doiObject->getData('doi'),
|
|
'external-id-url' => [
|
|
'value' => $doiObject->getResolvingUrl()
|
|
],
|
|
'external-id-relationship' => 'self'
|
|
];
|
|
|
|
$articleHasStoredPubId = true;
|
|
}
|
|
}
|
|
|
|
# Add issue ids if they exist
|
|
if ($issue) {
|
|
$doiObject = $issue->getData('doiObject');
|
|
if ($doiObject) {
|
|
$externalIds[] = [
|
|
'external-id-type' => self::PUBID_TO_ORCID_EXT_ID['doi'],
|
|
'external-id-value' => $doiObject->getData('doi'),
|
|
'external-id-url' => [
|
|
'value' => $doiObject->getResolvingUrl()
|
|
],
|
|
'external-id-relationship' => 'part-of'
|
|
];
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
error_log('OrcidProfilePlugin::buildOrcidExternalIds: No pubId plugins could be loaded');
|
|
}
|
|
|
|
if (!$articleHasStoredPubId) {
|
|
// No pubidplugins available or article does not have any stored pubid
|
|
// Use URL as an external-id
|
|
$externalIds[] = [
|
|
'external-id-type' => 'uri',
|
|
'external-id-value' => $articleUrl,
|
|
'external-id-relationship' => 'self'
|
|
];
|
|
}
|
|
|
|
// Add journal online ISSN
|
|
// TODO What about print ISSN?
|
|
if ($context->getData('onlineIssn')) {
|
|
$externalIds[] = [
|
|
'external-id-type' => 'issn',
|
|
'external-id-value' => $context->getData('onlineIssn'),
|
|
'external-id-relationship' => 'part-of'
|
|
];
|
|
}
|
|
|
|
return $externalIds;
|
|
}
|
|
|
|
/**
|
|
* Parse issue year and publication date and use the older on of the two as
|
|
* the publication date of the ORCID work.
|
|
*
|
|
* @param null|mixed $issue
|
|
*
|
|
* @return array Associative array with year, month and day or only year
|
|
*/
|
|
private function buildOrcidPublicationDate($publication, $issue = null)
|
|
{
|
|
$publicationPublishDate = Carbon::parse($publication->getData('datePublished'));
|
|
|
|
return [
|
|
'year' => ['value' => $publicationPublishDate->format('Y')],
|
|
'month' => ['value' => $publicationPublishDate->format('m')],
|
|
'day' => ['value' => $publicationPublishDate->format('d')]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Build associative array fitting for ORCID contributor mentions in an
|
|
* ORCID work from the supplied Authors array.
|
|
*
|
|
* @param Author[] $authors Array of Author objects
|
|
*
|
|
* @return array[] Array of associative arrays,
|
|
* one for each contributor
|
|
*/
|
|
private function buildOrcidContributors($authors, $context, $publication)
|
|
{
|
|
$contributors = [];
|
|
$first = true;
|
|
|
|
foreach ($authors as $author) {
|
|
// TODO Check if e-mail address should be added
|
|
$fullName = $author->getLocalizedGivenName() . ' ' . $author->getLocalizedFamilyName();
|
|
|
|
if (strlen($fullName) == 0) {
|
|
$this->logError('Contributor Name not defined' . $author->getAllData());
|
|
}
|
|
$contributor = [
|
|
'credit-name' => $fullName,
|
|
'contributor-attributes' => [
|
|
'contributor-sequence' => $first ? 'first' : 'additional'
|
|
]
|
|
];
|
|
|
|
$userGroup = $author->getUserGroup();
|
|
$role = self::USER_GROUP_TO_ORCID_ROLE[$userGroup->getName('en')];
|
|
|
|
if ($role) {
|
|
$contributor['contributor-attributes']['contributor-role'] = $role;
|
|
}
|
|
|
|
if ($author->getOrcid()) {
|
|
$orcid = basename(parse_url($author->getOrcid(), PHP_URL_PATH));
|
|
|
|
if ($author->getData('orcidSandbox')) {
|
|
$uri = ORCID_URL_SANDBOX . $orcid;
|
|
$host = 'sandbox.orcid.org';
|
|
} else {
|
|
$uri = $author->getOrcid();
|
|
$host = 'orcid.org';
|
|
}
|
|
|
|
$contributor['contributor-orcid'] = [
|
|
'uri' => $uri,
|
|
'path' => $orcid,
|
|
'host' => $host
|
|
];
|
|
}
|
|
|
|
$first = false;
|
|
|
|
$contributors[] = $contributor;
|
|
}
|
|
|
|
return $contributors;
|
|
}
|
|
|
|
/**
|
|
* handleEditorAction handles promoting a submission to copyediting.
|
|
*
|
|
* @param string $hookName Name the hook was registered with
|
|
* @param array $args Hook arguments, &$submission, &$editorDecision, &$result, &$recommendation.
|
|
*
|
|
* @see EditorAction::recordDecision() The function calling the hook.
|
|
*/
|
|
public function handleEditorAction($hookName, $args)
|
|
{
|
|
$submission = $args[0];
|
|
/** @var Submission $submission */
|
|
$decision = $args[1];
|
|
|
|
if ($decision['decision'] == Decision::ACCEPT) {
|
|
$publication = $submission->getCurrentPublication();
|
|
|
|
if (isset($publication)) {
|
|
$authors = Repo::author()->getCollector()
|
|
->filterByPublicationIds([$submission->getCurrentPublication()->getId()])
|
|
->getMany();
|
|
|
|
foreach ($authors as $author) {
|
|
$orcidAccessExpiresOn = Carbon::parse($author->getData('orcidAccessExpiresOn'));
|
|
if ($author->getData('orcidAccessToken') == null || $orcidAccessExpiresOn->isPast()) {
|
|
$this->sendAuthorMail($author, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the current id of the context (atm only considered for logging settings).
|
|
*
|
|
* @param int $contextId the Id of the currently active context (journal)
|
|
*/
|
|
public function setCurrentContextId($contextId)
|
|
{
|
|
$this->currentContextId = $contextId;
|
|
}
|
|
|
|
/**
|
|
* Add mailable to the list of mailables in the application
|
|
*/
|
|
public function addMailable(string $hookName, array $args): void
|
|
{
|
|
foreach ([OrcidCollectAuthorId::class, OrcidRequestAuthorAuthorization::class] as $mailable) {
|
|
$args[0]->push($mailable);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @copydoc Plugin::updateSchema()
|
|
*/
|
|
public function updateSchema($hookName, $args)
|
|
{
|
|
$installer = $args[0];
|
|
$result = &$args[1];
|
|
$migration = new OrcidProfileEmailDataMigration($installer, $this);
|
|
try {
|
|
$migration->up();
|
|
} catch (Exception $e) {
|
|
$installer->setError(Installer::INSTALLER_ERROR_DB, __('installer.installMigrationError', ['class' => get_class($migration), 'message' => $e->getMessage()]));
|
|
$result = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pre-publication checks
|
|
*
|
|
* @return false
|
|
*/
|
|
public function validate($hookName, $args)
|
|
{
|
|
$errors = & $args[0];
|
|
$publication = $args[1];
|
|
$orcidIds = [];
|
|
foreach ($publication->getData('authors') as $author) {
|
|
$authorOrcid = $author->getData('orcid');
|
|
if ($authorOrcid and in_array($authorOrcid, $orcidIds)) {
|
|
$errors['hasDuplicateOrcids'] = __('plugins.generic.orcidProfile.verify.duplicateOrcidAuthor');
|
|
} elseif ($authorOrcid && !$author->getData('orcidAccessToken')) {
|
|
$errors['hasUnauthenticatedOrcid'] = __('plugins.generic.orcidProfile.verify.hasUnauthenticatedOrcid');
|
|
} else {
|
|
$orcidIds[] = $authorOrcid;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|