diff --git a/classes/core/PKPContainer.php b/classes/core/PKPContainer.php
index f3e639adb97..8b870333b31 100644
--- a/classes/core/PKPContainer.php
+++ b/classes/core/PKPContainer.php
@@ -450,7 +450,7 @@ protected function loadConfiguration(): void
// Cache configuration
$items['cache'] = [
- 'default' => Config::getVar('cache', 'default', 'opcache'),
+ 'default' => Config::getVar('cache', 'default', 'file'),
'stores' => [
'opcache' => [
'driver' => 'opcache',
diff --git a/classes/mail/mailables/OrcidRequestUpdateScope.php b/classes/mail/mailables/OrcidRequestUpdateScope.php
new file mode 100644
index 00000000000..c44a892a1ed
--- /dev/null
+++ b/classes/mail/mailables/OrcidRequestUpdateScope.php
@@ -0,0 +1,52 @@
+setupOrcidVariables($oauthUrl, $context);
+ }
+
+ public static function getDataDescriptions(): array
+ {
+ /**
+ * Adds ORCID URLs to email template
+ */
+ return array_merge(
+ parent::getDataDescriptions(),
+ static::getOrcidDataDescriptions()
+ );
+ }
+}
diff --git a/classes/mail/traits/OrcidVariables.php b/classes/mail/traits/OrcidVariables.php
index 3188ef2ec04..274f5e0dc3e 100644
--- a/classes/mail/traits/OrcidVariables.php
+++ b/classes/mail/traits/OrcidVariables.php
@@ -50,7 +50,13 @@ protected function setupOrcidVariables(string $oauthUrl, Context $context): void
$this->addData([
self::$authorOrcidUrl => $oauthUrl,
- self::$orcidAboutUrl => $dispatcher->url($request, Application::ROUTE_PAGE, null, 'orcid', 'about', urlLocaleForPage: ''),
+ self::$orcidAboutUrl => $dispatcher->url(
+ $request,
+ Application::ROUTE_PAGE,
+ newContext: $context->getPath(),
+ handler: 'orcid',
+ op: 'about',
+ urlLocaleForPage: ''),
self::$principalContactSignature => $principalContact->getLocalizedSignature(),
]);
}
diff --git a/classes/migration/upgrade/v3_5_0/I10819_OrcidOauthScopeMail.php b/classes/migration/upgrade/v3_5_0/I10819_OrcidOauthScopeMail.php
new file mode 100644
index 00000000000..3528c8eeddf
--- /dev/null
+++ b/classes/migration/upgrade/v3_5_0/I10819_OrcidOauthScopeMail.php
@@ -0,0 +1,45 @@
+dao->installEmailTemplates(
+ Repo::emailTemplate()->dao->getMainEmailTemplatesFilename(),
+ [],
+ 'ORCID_REQUEST_UPDATE_SCOPE',
+ true,
+ );
+ }
+
+ /**
+ * @inheritDoc
+ * @throws DowngradeNotSupportedException
+ */
+ public function down(): void
+ {
+ throw new DowngradeNotSupportedException();
+ }
+}
diff --git a/classes/orcid/OrcidManager.php b/classes/orcid/OrcidManager.php
index f64c44d5171..7c45605be83 100644
--- a/classes/orcid/OrcidManager.php
+++ b/classes/orcid/OrcidManager.php
@@ -152,30 +152,29 @@ public static function isSandbox(?Context $context = null): bool
*
* @throws \Exception
*/
- public static function buildOAuthUrl(string $handlerMethod, array $redirectParams): string
+ public static function buildOAuthUrl(string $handlerMethod, array $redirectParams, ?Context $context = null): string
{
$request = Application::get()->getRequest();
- $context = $request->getContext();
+ $context = $context ?? $request->getContext();
if ($context === null) {
throw new \Exception('OAuth URLs can only be made in a Context, not site wide.');
}
- $scope = self::isMemberApiEnabled() ? self::ORCID_API_SCOPE_MEMBER : self::ORCID_API_SCOPE_PUBLIC;
+ $scope = self::isMemberApiEnabled($context) ? self::ORCID_API_SCOPE_MEMBER : self::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,
- 'orcid',
- $handlerMethod,
- null,
- $redirectParams,
+ newContext: $context->getPath(),
+ handler: 'orcid',
+ op: $handlerMethod,
+ params: $redirectParams,
urlLocaleForPage: '',
);
- return self::getOauthPath() . 'authorize?' . http_build_query(
+ return self::getOauthPath($context) . 'authorize?' . http_build_query(
[
'client_id' => self::getClientId($context),
'response_type' => 'code',
@@ -232,7 +231,7 @@ public static function getLogLevel(?Context $context = null): string
$context = Application::get()->getRequest()->getContext();
}
- return $context->getData(self::LOG_LEVEL) ?? self::LOG_LEVEL_ERROR;
+ return $context?->getData(self::LOG_LEVEL) ?? self::LOG_LEVEL_ERROR;
}
@@ -251,9 +250,9 @@ public static function shouldSendMailToAuthors(?Context $context = null): bool
/**
* Helper method that gets OAuth endpoint for configured ORCID URL (production or sandbox)
*/
- public static function getOauthPath(): string
+ public static function getOauthPath(?Context $context = null): string
{
- return self::getOrcidUrl() . 'oauth/';
+ return self::getOrcidUrl($context) . 'oauth/';
}
/**
@@ -352,9 +351,9 @@ private static function writeLog(string $message, string $level): void
/**
* Gets the ORCID API endpoint to revoke an access token
*/
- public static function getTokenRevocationUrl(): string
+ public static function getTokenRevocationUrl(?Context $context = null): string
{
- return self::getOauthPath() . 'revoke';
+ return self::getOauthPath($context) . 'revoke';
}
/**
diff --git a/classes/orcid/actions/PKPSendSubmissionToOrcid.php b/classes/orcid/actions/PKPSendSubmissionToOrcid.php
index 3af43d121b9..c16a97f250c 100644
--- a/classes/orcid/actions/PKPSendSubmissionToOrcid.php
+++ b/classes/orcid/actions/PKPSendSubmissionToOrcid.php
@@ -18,6 +18,7 @@
use APP\orcid\actions\SendReviewToOrcid;
use APP\publication\Publication;
use Carbon\Carbon;
+use Illuminate\Contracts\Queue\ShouldBeUnique;
use PKP\context\Context;
use PKP\jobs\orcid\DepositOrcidSubmission;
use PKP\orcid\OrcidManager;
diff --git a/classes/orcid/actions/VerifyAuthorWithOrcid.php b/classes/orcid/actions/VerifyAuthorWithOrcid.php
deleted file mode 100644
index c824be7202a..00000000000
--- a/classes/orcid/actions/VerifyAuthorWithOrcid.php
+++ /dev/null
@@ -1,177 +0,0 @@
-request->getContext();
- if (!OrcidManager::isEnabled($context)) {
- return $this;
- }
-
- // Fetch the access token
- $oauthTokenUrl = OrcidManager::getApiPath($context) . OrcidManager::OAUTH_TOKEN_URL;
-
- $httpClient = Application::get()->getHttpClient();
- $headers = ['Accept' => 'application/json'];
- $postData = [
- 'code' => $this->request->getUserVar('code'),
- 'grant_type' => 'authorization_code',
- 'client_id' => OrcidManager::getClientId($context),
- 'client_secret' => OrcidManager::getClientSecret($context)
- ];
-
- OrcidManager::logInfo('POST ' . $oauthTokenUrl);
- OrcidManager::logInfo('Request headers: ' . var_export($headers, true));
- OrcidManager::logInfo('Request body: ' . http_build_query($postData));
-
- try {
- $response = $httpClient->request(
- 'POST',
- $oauthTokenUrl,
- [
- 'headers' => $headers,
- 'form_params' => $postData,
- 'allow_redirects' => ['strict' => true],
- ],
- );
-
- if ($response->getStatusCode() !== 200) {
- OrcidManager::logError('VerifyAuthorWithOrcid::execute - unexpected response: ' . $response->getStatusCode());
- $this->addTemplateVar('authFailure', true);
- }
- $results = json_decode($response->getBody(), true);
-
- // Check for errors
- OrcidManager::logInfo('Response body: ' . print_r($results, true));
- if (($results['error'] ?? null) === 'invalid_grant') {
- OrcidManager::logError('Authorization code invalid, maybe already used');
- $this->addTemplateVar('authFailure', true);
- }
- if (isset($results['error'])) {
- OrcidManager::logError('Invalid ORCID response: ' . $results['error']);
- $this->addTemplateVar('authFailure', true);
- }
-
- // Check for duplicate ORCID for author
- $orcidUri = OrcidManager::getOrcidUrl($context) . $results['orcid'];
- if (!empty($this->author->getOrcid()) && $orcidUri !== $this->author->getOrcid()) {
- $this->addTemplateVar('duplicateOrcid', true);
- }
- $this->addTemplateVar('orcid', $orcidUri);
-
- $this->author->setOrcid($orcidUri);
- $this->author->setOrcidVerified(true);
- $this->author->setData('orcidVerificationRequested', null);
- if (OrcidManager::isSandbox($context)) {
- $this->author->setData('orcidEmailToken', null);
- }
- $this->setOrcidAccessData($orcidUri, $results);
- Repo::author()->dao->update($this->author);
-
- // Send member submissions to ORCID
- if (OrcidManager::isMemberApiEnabled($context)) {
- $publicationId = $this->request->getUserVar('state');
- $publication = Repo::publication()->get($publicationId);
-
- if ($publication->getData('status') == PKPSubmission::STATUS_PUBLISHED) {
- (new SendSubmissionToOrcid($publication, $context))->execute();
- $this->addTemplateVar('sendSubmissionSuccess', true);
- } else {
- $this->addTemplateVar('submissionNotPublished', true);
- }
- }
-
- $this->addTemplateVar('verifySuccess', true);
- $this->addTemplateVar('orcidIcon', OrcidManager::getIcon());
- } catch (GuzzleException $exception) {
- OrcidManager::logError('Publication fail: ' . $exception->getMessage());
- $this->addTemplateVar('orcidAPIError', $exception->getMessage());
- }
-
- // Does not indicate auth was a failure, but if verifySuccess was not set to true above,
- // the nature of the failure will be auth related . If verifySuccess is set to true above, an `else` branch
- // in the template that covers various failure/error messages will never be reached.
- $this->addTemplateVar('authFailure', true);
- return $this;
- }
-
- /**
- * Takes template variables for frontend display from OAuth process and assigns them to the TemplateManager.
- *
- * @param TemplateManager $templateMgr The template manager to which the variable should be set
- */
- public function updateTemplateMgrVars(TemplateManager &$templateMgr): void
- {
- foreach ($this->templateVarsToSet as $key => $value) {
- $templateMgr->assign($key, $value);
- }
- }
-
- /**
- * Helper to set ORCID and OAuth values to the author. NB: Does not save updated Author instance to the database.
- *
- * @param string $orcidUri Complete ORCID URL
- */
- private function setOrcidAccessData(string $orcidUri, array $results): void
- {
- // Save the access token
- $orcidAccessExpiresOn = Carbon::now();
- // expires_in field from the response contains the lifetime in seconds of the token
- // See https://members.orcid.org/api/get-oauthtoken
- $orcidAccessExpiresOn->addSeconds($results['expires_in']);
- $this->author->setOrcid($orcidUri);
- // remove the access denied marker, because now the access was granted
- $this->author->setData('orcidAccessDenied', null);
- $this->author->setData('orcidAccessToken', $results['access_token']);
- $this->author->setData('orcidAccessScope', $results['scope']);
- $this->author->setData('orcidRefreshToken', $results['refresh_token']);
- $this->author->setData('orcidAccessExpiresOn', $orcidAccessExpiresOn->toDateTimeString());
-
- }
-
- /**
- * Stores key, value pair to be assigned to the TemplateManager for display in frontend UI.
- */
- private function addTemplateVar(string $key, mixed $value): void
- {
- $this->templateVarsToSet[$key] = $value;
- }
-}
diff --git a/classes/orcid/actions/VerifyIdentityWithOrcid.php b/classes/orcid/actions/VerifyIdentityWithOrcid.php
new file mode 100644
index 00000000000..6bd8d9c0973
--- /dev/null
+++ b/classes/orcid/actions/VerifyIdentityWithOrcid.php
@@ -0,0 +1,238 @@
+context = $this->request->getContext();
+ }
+
+ /**
+ * Execute the action.
+ *
+ * NB: This method returns itself so template variables can be added to a template via `updateTemplateMgrVars()` below.
+ */
+ public function execute(): self
+ {
+ if (!OrcidManager::isEnabled($this->context)) {
+ return $this;
+ }
+
+ $response = $this->getHandshakeResponse();
+ if ($response !== null) {
+ $this->handleResponseErrors($response);
+ $this->setIdentityData($response);
+ $this->saveIdentityData();
+ $this->depositOrcidItem();
+ }
+
+ // Does not indicate auth was a failure, but if verifySuccess was not set to true above,
+ // the nature of the failure will be auth related . If verifySuccess is set to true above, an `else` branch
+ // in the template that covers various failure/error messages will never be reached.
+ $this->addTemplateVar('authFailure', true);
+
+ return $this;
+ }
+
+ /**
+ * Takes template variables for frontend display from OAuth process and assigns them to the TemplateManager.
+ *
+ * @param TemplateManager $templateMgr The template manager to which the variable should be set
+ */
+ public function updateTemplateMgrVars(TemplateManager &$templateMgr): void
+ {
+ foreach ($this->templateVarsToSet as $key => $value) {
+ $templateMgr->assign($key, $value);
+ }
+ }
+
+ /**
+ * Returns the decoded JSON contents of the OAuth handshake response
+ */
+ private function getHandshakeResponse(): ?array
+ {
+ // Fetch access token
+ $oauthTokenUrl = OrcidManager::getApiPath($this->context) . OrcidManager::OAUTH_TOKEN_URL;
+
+ $httpClient = Application::get()->getHttpClient();
+ $headers = ['Accept' => 'application/json'];
+ $postData = [
+ 'code' => $this->request->getUserVar('code'),
+ 'grant_type' => 'authorization_code',
+ 'client_id' => OrcidManager::getClientId($this->context),
+ 'client_secret' => OrcidManager::getClientSecret($this->context)
+ ];
+
+ OrcidManager::logInfo('POST ' . $oauthTokenUrl);
+ OrcidManager::logInfo('Request headers: ' . var_export($headers, true));
+ OrcidManager::logInfo('Request body: ' . http_build_query($postData));
+
+ try {
+ $response = $httpClient->request(
+ 'POST',
+ $oauthTokenUrl,
+ [
+ 'headers' => $headers,
+ 'form_params' => $postData,
+ 'allow_redirects' => ['strict' => true],
+ ],
+ );
+
+ if ($response->getStatusCode() !== 200) {
+ OrcidManager::logError('VerifyIdentityWithOrcid::getHandshakeResponse - unexpected response: ' . $response->getStatusCode());
+ $this->addTemplateVar('authFailure', true);
+ }
+
+ $this->addTemplateVar('verifySuccess', true);
+ $this->addTemplateVar('orcidIcon', OrcidManager::getIcon());
+
+ return json_decode($response->getBody(), true);
+ } catch (GuzzleException $exception) {
+ OrcidManager::logError('Publication fail: ' . $exception->getMessage());
+ $this->addTemplateVar('orcidAPIError', $exception->getMessage());
+ }
+
+ return null;
+ }
+
+ /**
+ * Handles setting relevant ORCID data to user or author.
+ *
+ * @param array $response
+ * @return void
+ */
+ private function setIdentityData(array $response): void
+ {
+ $orcidUri = OrcidManager::getOrcidUrl($this->context) . $response['orcid'];
+ $this->addTemplateVar('orcid', $orcidUri);
+
+ $this->identity->setOrcid($orcidUri);
+ $this->identity->setOrcidVerified(true);
+
+ // Save the access token
+ $orcidAccessExpiresOn = Carbon::now();
+ // expires_in field from the response contains the lifetime in seconds of the token
+ // See https://members.orcid.org/api/get-oauthtoken
+ $orcidAccessExpiresOn->addSeconds($response['expires_in']);
+ // remove the access denied marker, because now the access was granted
+ $this->identity->setData('orcidAccessDenied', null);
+ $this->identity->setData('orcidAccessToken', $response['access_token']);
+ $this->identity->setData('orcidAccessScope', $response['scope']);
+ $this->identity->setData('orcidRefreshToken', $response['refresh_token']);
+ $this->identity->setData('orcidAccessExpiresOn', $orcidAccessExpiresOn->toDateTimeString());
+
+
+ if ($this->identity instanceof Author) {
+ $this->identity->setData('orcidEmailToken', null);
+ }
+ }
+
+ /**
+ * Saves identity information using the correct DAO (`Author` or `User`).
+ *
+ * @throws \Exception
+ */
+ private function saveIdentityData(): void
+ {
+ if ($this->identity instanceof Author) {
+ Repo::author()->dao->update($this->identity);
+ } else if ($this->identity instanceof User) {
+ Repo::user()->dao->update($this->identity);
+ } else {
+ throw new \Exception('Identity must be an instance of Author or User');
+ }
+ }
+
+ /**
+ * Stores key, value pair to be assigned to the TemplateManager for display in frontend UI.
+ */
+ protected function addTemplateVar(string $key, mixed $value): void
+ {
+ $this->templateVarsToSet[$key] = $value;
+ }
+
+ /**
+ * Dispatches job to deposit either ORCID work or review
+ */
+ private function depositOrcidItem(): void
+ {
+ if (!OrcidManager::isMemberApiEnabled($this->context)) {
+ return;
+ }
+
+ if ($this->depositType === OrcidDepositType::WORK) {
+ $publicationId = $this->request->getUserVar('state');
+ $publication = Repo::publication()->get($publicationId);
+
+ if ($publication->getData('status') == PKPSubmission::STATUS_PUBLISHED) {
+ (new SendSubmissionToOrcid($publication, $this->context))->execute();
+ $this->addTemplateVar('sendSubmissionSuccess', true);
+ } else {
+ $this->addTemplateVar('submissionNotPublished', true);
+ }
+ } else if ($this->depositType === OrcidDepositType::REVIEW) {
+ $reviewAssignmentId = $this->request->getUserVar('itemId');
+ (new SendReviewToOrcid($reviewAssignmentId))->execute();
+ $this->addTemplateVar('sendSubmissionSuccess', true);
+ }
+ }
+
+ /**
+ * Handles logging and template variable updates for OAuth response errors.
+ */
+ private function handleResponseErrors(array $response): void
+ {
+ OrcidManager::logInfo('Response body: ' . print_r($response, true));
+ if (($response['error'] ?? null) === 'invalid_grant') {
+ OrcidManager::logError('Authorization code invalid, maybe already used');
+ $this->addTemplateVar('authFailure', true);
+ }
+ if (isset($response['error'])) {
+ OrcidManager::logError('Invalid ORCID response: ' . $response['error']);
+ $this->addTemplateVar('authFailure', true);
+ }
+
+ $orcidUri = OrcidManager::getOrcidUrl($this->context) . $response['orcid'];
+ if (!empty($this->identity->getOrcid()) && $orcidUri !== $this->identity->getOrcid()) {
+ $this->addTemplateVar('duplicateOrcid', true);
+ }
+ }
+}
diff --git a/classes/orcid/enums/OrcidDepositType.php b/classes/orcid/enums/OrcidDepositType.php
new file mode 100644
index 00000000000..73614bfd11b
--- /dev/null
+++ b/classes/orcid/enums/OrcidDepositType.php
@@ -0,0 +1,21 @@
+context)) {
+ return;
+ }
+
+ // Check author scope, if public API, stop here and request member scope
+ if ($this->author->getData('orcidAccessScope') !== OrcidManager::ORCID_API_SCOPE_MEMBER) {
+ // Request member scope and retry deposit
+ dispatch(new SendUpdateScopeMail($this->author, $this->context->getId(), $this->author->getData('publicationId'), OrcidDepositType::WORK));
+ return;
+ }
+
$uri = OrcidManager::getApiPath($this->context) . OrcidManager::ORCID_API_VERSION_URL . $this->authorOrcid . '/' . OrcidManager::ORCID_WORK_URL;
$method = 'POST';
if ($putCode = $this->author->getData('orcidWorkPutCode')) {
- // Submission has already been sent to ORCID. Use PUT to update meta data
+ // Submission has already been sent to ORCID. Use PUT to update metadata
$uri .= '/' . $putCode;
$method = 'PUT';
$this->orcidWork['put-code'] = $putCode;
@@ -134,4 +147,9 @@ public function handle(): void
OrcidManager::logError("Unexpected status {$httpStatus} response, body: " . $response->getBody());
}
}
+
+ public function uniqueId(): string
+ {
+ return $this->author->getId();
+ }
}
diff --git a/jobs/orcid/RevokeOrcidToken.php b/jobs/orcid/RevokeOrcidToken.php
index e5b1c64a659..d461e9e9fb2 100644
--- a/jobs/orcid/RevokeOrcidToken.php
+++ b/jobs/orcid/RevokeOrcidToken.php
@@ -55,7 +55,7 @@ public function handle(): void
try {
$httpClient->request(
'POST',
- OrcidManager::getTokenRevocationUrl(),
+ OrcidManager::getTokenRevocationUrl($this->context),
[
'headers' => $headers,
'form_params' => $postData,
diff --git a/jobs/orcid/SendAuthorMail.php b/jobs/orcid/SendAuthorMail.php
index 9952824b237..90f5397346b 100644
--- a/jobs/orcid/SendAuthorMail.php
+++ b/jobs/orcid/SendAuthorMail.php
@@ -18,6 +18,7 @@
use APP\author\Author;
use APP\facades\Repo;
+use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Support\Facades\Mail;
use PKP\context\Context;
use PKP\jobs\BaseJob;
@@ -25,7 +26,7 @@
use PKP\mail\mailables\OrcidRequestAuthorAuthorization;
use PKP\orcid\OrcidManager;
-class SendAuthorMail extends BaseJob
+class SendAuthorMail extends BaseJob implements ShouldBeUnique
{
public function __construct(
private Author $author,
@@ -55,7 +56,11 @@ public function handle()
$emailToken = md5(microtime() . $this->author->getEmail());
$this->author->setData('orcidEmailToken', $emailToken);
- $oauthUrl = OrcidManager::buildOAuthUrl('verify', ['token' => $emailToken, 'state' => $publicationId]);
+ $oauthUrl = OrcidManager::buildOAuthUrl(
+ 'verify',
+ ['token' => $emailToken, 'state' => $publicationId],
+ $this->context
+ );
if (OrcidManager::isMemberApiEnabled($this->context)) {
$mailable = new OrcidRequestAuthorAuthorization($this->context, $submission, $oauthUrl);
@@ -78,4 +83,9 @@ public function handle()
Repo::author()->dao->update($this->author);
}
}
+
+ public function uniqueId(): string
+ {
+ return $this->author->getId();
+ }
}
diff --git a/jobs/orcid/SendUpdateScopeMail.php b/jobs/orcid/SendUpdateScopeMail.php
new file mode 100644
index 00000000000..8538d6c219f
--- /dev/null
+++ b/jobs/orcid/SendUpdateScopeMail.php
@@ -0,0 +1,125 @@
+getById($this->contextId);
+ if ($context === null) {
+ $this->fail("ORCID emails can only be sent from a context. A context could not be found for contextId: $this->contextId.");
+ }
+
+ $submission = $this->getSubmission();
+ if (!$submission) {
+ $this->fail('A submission could not be found for the associated item');
+ }
+
+ $emailToken = md5(microtime() . $this->identity->getEmail());
+ $this->identity->setData('orcidEmailToken', $emailToken);
+ $oauthUrl = OrcidManager::buildOAuthUrl(
+ 'updateScope',
+ [
+ 'token' => $emailToken,
+ 'itemId' => $this->itemId,
+ 'itemType' => $this->depositType->value,
+ 'userId' => $this->identity->getId(),
+ 'userIdType' => $this->identity instanceof User ? 'user' : 'author',
+ ],
+ $context
+ );
+
+ $mailable = new OrcidRequestUpdateScope($context, $submission, $oauthUrl);
+
+ // Set From to primary journal contact
+ $mailable->from($context->getData('contactEmail'), $context->getData('contactName'));
+
+ // Send mail
+ $mailable->recipients([$this->identity]);
+ $emailTemplateKey = $mailable::getEmailTemplateKey();
+ $emailTemplate = Repo::emailTemplate()->getByKey($context->getId(), $emailTemplateKey);
+ $mailable->body($emailTemplate->getLocalizedData('body'))
+ ->subject($emailTemplate->getLocalizedData('subject'));
+ Mail::send($mailable);
+
+ $this->saveIdentity();
+ }
+
+ /**
+ * Saves identity information using the correct DAO (`Author` or `User`).
+ */
+ private function saveIdentity(): void
+ {
+ if ($this->identity instanceof Author) {
+ Repo::author()->dao->update($this->identity);
+ } else if ($this->identity instanceof User) {
+ Repo::user()->dao->update($this->identity);
+ }
+ }
+
+ /**
+ * Gets the associated submission in the correct way depending on the type of item deposit (work or review).
+ */
+ private function getSubmission(): ?Submission
+ {
+ switch ($this->depositType) {
+ case OrcidDepositType::WORK:
+ $publication = Repo::publication()->get($this->itemId);
+ return Repo::submission()->get($publication->getData('submissionId'));
+ case OrcidDepositType::REVIEW:
+ $reviewAssigment = Repo::reviewAssignment()->get($this->itemId);
+ return Repo::submission()->get($reviewAssigment->getSubmissionId());
+ }
+
+ return null;
+ }
+
+ /**
+ * Provides a unique ID for the job to ensure only one job is dispatched per User/Author at a time.
+ */
+ public function uniqueId(): string
+ {
+ return $this->identity->getId();
+ }
+}
diff --git a/locale/en/emails.po b/locale/en/emails.po
index 9b704a8bf5a..9a148012a04 100644
--- a/locale/en/emails.po
+++ b/locale/en/emails.po
@@ -540,6 +540,43 @@ msgstr ""
msgid "emails.orcidRequestAuthorAuthorization.description"
msgstr "This email is sent to request ORCID record access from authors."
+msgid "emails.orcidRequestUpdateScope.description"
+msgstr "This email is sent to request member API OAuth scope for ORCID."
+
+msgid "emails.orcidRequestUpdateScope.subject"
+msgstr "Requesting updated ORCID record access"
+
+msgid "emails.orcidRequestUpdateScope.body"
+msgstr ""
+"Dear {$recipientName},
\n"
+"
\n"
+"You are listed as a contributor (author or reviewer) on the manuscript submission \""
+"{$submissionTitle}\" to {$contextName}.\n"
+"
\n"
+"
\n"
+"You have previously authorized {$contextName} to list your ORCID id on the site, "
+"and we require updateded permissions to add your contribution to your ORCID profile.
\n"
+"Visit the link to the official ORCID website, login with your profile and "
+"authorize the access by following the instructions.
\n"
+"
\n"
+""
+"
Register or Connect your ORCID "
+"iD
\n"
+"
\n"
+"
\n"
+"Click here to update your account with ORCID: {$authorOrcidUrl}.\n"
+"
\n"
+"
\n"
+"More about ORCID at {$contextName}
\n"
+"
\n"
+"
\n"
+"If you have any questions, please contact me.
\n"
+"
\n"
+"{$principalContactSignature}
\n"
+
msgid "emailTemplate.variable.authorOrcidUrl"
msgstr "ORCID OAuth authorization link"
@@ -550,6 +587,9 @@ msgstr "URL to the page about ORCID"
msgid "orcid.orcidRequestAuthorAuthorization.name"
msgstr "orcidRequestAuthorAuthorization"
+msgid "orcid.orcidRequestUpdateScope.name"
+msgstr "orcidRequestUpdateScope"
+
#, fuzzy
msgid "orcid.orcidCollectAuthorId.name"
msgstr "orcidCollectAuthorId"
diff --git a/pages/orcid/OrcidHandler.php b/pages/orcid/OrcidHandler.php
index ab16bf31679..ca00a841815 100644
--- a/pages/orcid/OrcidHandler.php
+++ b/pages/orcid/OrcidHandler.php
@@ -25,11 +25,15 @@
use PKP\config\Config;
use PKP\core\Core;
use PKP\core\PKPSessionGuard;
+use PKP\identity\Identity;
use PKP\orcid\actions\AuthorizeUserData;
use PKP\orcid\actions\VerifyAuthorWithOrcid;
+use PKP\orcid\actions\VerifyIdentityWithOrcid;
+use PKP\orcid\enums\OrcidDepositType;
use PKP\orcid\OrcidManager;
use PKP\security\authorization\PKPSiteAccessPolicy;
use PKP\security\authorization\UserRequiredPolicy;
+use PKP\user\User;
class OrcidHandler extends Handler
{
@@ -42,7 +46,7 @@ public function authorize($request, &$args, $roleAssignments)
// Authorize all requests
$this->addPolicy(new PKPSiteAccessPolicy(
$request,
- ['verify', 'authorizeOrcid', 'about'],
+ ['verify', 'authorizeOrcid', 'about', 'updateScope'],
PKPSiteAccessPolicy::SITE_ACCESS_ALL_ROLES
));
@@ -96,7 +100,9 @@ public function verify(array $args, Request $request): void
$this->handleUserDeniedAccess($author, $templateMgr, $request->getUserVar('error_description'));
}
- (new VerifyAuthorWithOrcid($author, $request))->execute()->updateTemplateMgrVars($templateMgr);
+ (new VerifyIdentityWithOrcid($author, $request, OrcidDepositType::WORK))
+ ->execute()
+ ->updateTemplateMgrVars($templateMgr);
$templateMgr->display(self::VERIFY_TEMPLATE_PATH);
}
@@ -129,6 +135,51 @@ public function about(array $args, Request $request): void
}
+ /**
+ * Displays page for completed OAuth process when the goal is to update the author's/user's OAuth scope and
+ * resubmit the ORCID item for deposit requiring the updated scope.
+ */
+ public function updateScope(array $args, Request $request): void
+ {
+ // If the application is set to sandbox mode, it will not reach out to external services
+ if (Config::getVar('general', 'sandbox', false)) {
+ error_log('Application is set to sandbox mode and will not interact with the ORCID service');
+ return;
+ }
+
+ $templateMgr = TemplateManager::getManager($request);
+
+ // Initialise template parameters
+ $templateMgr->assign([
+ 'currentUrl' => $request->url(null, 'index'),
+ 'verifySuccess' => false,
+ 'authFailure' => false,
+ 'notPublished' => false,
+ 'sendSubmission' => false,
+ 'sendSubmissionSuccess' => false,
+ 'denied' => false,
+ 'contextName' => $request->getContext()->getName($request->getContext()->getPrimaryLocale()),
+ ]);
+
+ $identity = $this->getIdentityToVerify($request);
+
+ if ($identity === null) {
+ $this->handleNoAuthorWithToken($templateMgr);
+ } elseif ($request->getUserVar('error') === 'access_denied') {
+ // Handle access denied
+ $this->handleUserDeniedAccess($identity, $templateMgr, $request->getUserVar('error_description'));
+ }
+
+ $depositType = OrcidDepositType::tryFrom($request->getUserVar('itemType'));
+ if ($depositType !== null) {
+ (new VerifyIdentityWithOrcid($identity, $request, $depositType))
+ ->execute()
+ ->updateTemplateMgrVars($templateMgr);
+ }
+
+ $templateMgr->display(self::VERIFY_TEMPLATE_PATH);
+ }
+
/**
* Helper to retrieve author for which the ORCID verification was requested.
*/
@@ -165,19 +216,49 @@ private function handleNoAuthorWithToken(TemplateManager $templateMgr): void
/**
* Remove previously assigned ORCID OAuth related fields and assign denied variable to TemplateManagerA
*/
- private function handleUserDeniedAccess(Author $author, TemplateManager $templateMgr, string $errorDescription): void
+ private function handleUserDeniedAccess(Identity $identity, TemplateManager $templateMgr, string $errorDescription): void
{
// User denied access
// Store the date time the author denied ORCID access to remember this
- $author->setData('orcidAccessDenied', Core::getCurrentDate());
+ $identity->setData('orcidAccessDenied', Core::getCurrentDate());
// remove all previously stored ORCID access token
- $author->setData('orcidAccessToken', null);
- $author->setData('orcidAccessScope', null);
- $author->setData('orcidRefreshToken', null);
- $author->setData('orcidAccessExpiresOn', null);
- $author->setData('orcidEmailToken', null);
- Repo::author()->dao->update($author);
+ $identity->setData('orcidAccessToken', null);
+ $identity->setData('orcidAccessScope', null);
+ $identity->setData('orcidRefreshToken', null);
+ $identity->setData('orcidAccessExpiresOn', null);
+ $identity->setData('orcidEmailToken', null);
+
+ if ($identity instanceof Author) {
+ Repo::author()->dao->update($identity);
+ } else if ($identity instanceof User) {
+ Repo::user()->dao->update($identity);
+ }
OrcidManager::logError('OrcidHandler::verify - ORCID access denied. Error description: ' . $errorDescription);
$templateMgr->assign('denied', true);
}
+
+ private function getIdentityToVerify(Request $request): ?Identity
+ {
+ return match (OrcidDepositType::tryFrom($request->getUserVar('itemType'))) {
+ OrcidDepositType::WORK => $this->getAuthorToVerify($request),
+ OrcidDepositType::REVIEW => $this->getReviewerToVerify($request),
+ default => null,
+ };
+ }
+
+ private function getReviewerToVerify(Request $request): ?User
+ {
+ $user = null;
+ $userId = $request->getUserVar('userId');
+ $user = Repo::user()->get($userId);
+ if (
+ $user === null ||
+ empty($request->getUserVar('token')) ||
+ $user->getData('orcidEmailToken') != $request->getUserVar('token')
+ ) {
+ return null;
+ }
+
+ return $user;
+ }
}
diff --git a/pages/orcid/index.php b/pages/orcid/index.php
index 991b8293977..3a3efc00c72 100644
--- a/pages/orcid/index.php
+++ b/pages/orcid/index.php
@@ -21,5 +21,6 @@
case 'verify':
case 'authorizeOrcid':
case 'about':
+ case 'updateScope':
return new \PKP\pages\orcid\OrcidHandler();
}
diff --git a/schemas/user.json b/schemas/user.json
index 7dc5d944f0a..89231b2592c 100644
--- a/schemas/user.json
+++ b/schemas/user.json
@@ -280,6 +280,13 @@
"nullable"
]
},
+ "orcidEmailToken": {
+ "type": "string",
+ "apiSummary": true,
+ "validation": [
+ "nullable"
+ ]
+ },
"orcidIsVerified": {
"type": "boolean",
"apiSummary": true,