diff --git a/api/v1/orcid/index.php b/api/v1/orcid/index.php
new file mode 100644
index 00000000000..9e3bc82e2d3
--- /dev/null
+++ b/api/v1/orcid/index.php
@@ -0,0 +1,20 @@
+data = $this->build();
+ }
+
+ /**
+ * Returns ORCID review data as an associative array, ready for deposit.
+ */
+ public function toArray(): array
+ {
+ return $this->data;
+ }
+
+ /**
+ * Builds the internal structure for the ORCID review.
+ */
+ private function build(): array
+ {
+ $publicationUrl = Application::get()->getDispatcher()->url(
+ Application::get()->getRequest(),
+ Application::ROUTE_PAGE,
+ $this->context->getPath(),
+ 'article',
+ 'view',
+ $this->submission->getId(),
+ urlLocaleForPage: '',
+ );
+
+ $submissionLocale = $this->submission->getData('locale');
+ $currentPublication = $this->submission->getCurrentPublication();
+
+ if (empty($this->review->getData('dateCompleted')) || empty($this->context->getData('onlineIssn'))) {
+ return [];
+ }
+
+ $reviewCompletionDate = Carbon::parse($this->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:' . $this->context->getData('onlineIssn'),
+
+ 'convening-organization' => [
+ 'name' => $this->context->getData('publisherInstitution'),
+ 'address' => [
+ 'city' => OrcidManager::getCity($this->context),
+ 'country' => OrcidManager::getCountry($this->context),
+
+ ]
+ ],
+ 'review-identifiers' => ['external-id' => [
+ [
+ 'external-id-type' => 'source-work-id',
+ 'external-id-value' => $this->review->getData('reviewRoundId'),
+ 'external-id-relationship' => 'part-of']
+ ]]
+ ];
+ if ($this->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' => $this->submission->getCurrentPublication()->getLocalizedData('title') ?? '']
+ ];
+
+ if (!empty($currentPublication->getDoi())) {
+ /** @var Doi $doiObject */
+ $doiObject = $currentPublication->getData('doiObject');
+ $externalIds = [
+ 'external-id-type' => 'doi',
+ 'external-id-value' => $doiObject->getDoi(),
+ 'external-id-url' => [
+ 'value' => $doiObject->getResolvingUrl(),
+ ],
+ 'external-id-relationship' => 'self'
+
+ ];
+ $orcidReview['subject-external-identifier'] = $externalIds;
+ }
+ }
+
+ $allTitles = $currentPublication->getData('title');
+ foreach ($allTitles as $locale => $title) {
+ if ($locale !== $submissionLocale) {
+ $orcidReview['subject-name']['translated-title'] = ['value' => $title, 'language-code' => LocaleConversion::getIso1FromLocale($locale)];
+ }
+ }
+
+ return $orcidReview;
+ }
+}
diff --git a/classes/orcid/OrcidWork.php b/classes/orcid/OrcidWork.php
new file mode 100644
index 00000000000..74b27469a3e
--- /dev/null
+++ b/classes/orcid/OrcidWork.php
@@ -0,0 +1,116 @@
+publication, $this->context, $this->authors);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function getAppPubIdExternalIds(PubIdPlugin $plugin): array
+ {
+ $ids = [];
+
+ $pubIdType = $plugin->getPubIdType();
+ $pubId = $this->issue?->getStoredPubId($pubIdType);
+ if ($pubId) {
+ $ids[] = [
+ 'external-id-type' => self::PUBID_TO_ORCID_EXT_ID[$pubIdType],
+ 'external-id-value' => $pubId,
+ 'external-id-url' => [
+ 'value' => $plugin->getResolvingURL($this->context->getId(), $pubId)
+ ],
+ 'external-id-relationship' => 'part-of'
+ ];
+ }
+
+ return $ids;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function getAppDoiExternalIds(): array
+ {
+ $ids = [];
+
+ $issueDoiObject = $this->issue->getData('doiObject');
+ if ($issueDoiObject) {
+ $ids[] = [
+ 'external-id-type' => self::PUBID_TO_ORCID_EXT_ID['doi'],
+ 'external-id-value' => $issueDoiObject->getData('doi'),
+ 'external-id-url' => [
+ 'value' => $issueDoiObject->getResolvingUrl()
+ ],
+ 'external-id-relationship' => 'part-of'
+ ];
+ }
+
+ return $ids;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function getOrcidPublicationType(): string
+ {
+ return 'journal-article';
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function getBibtexCitation(Submission $submission): string
+ {
+ $request = Application::get()->getRequest();
+ try {
+ PluginRegistry::loadCategory('generic');
+ /** @var CitationStyleLanguagePlugin $citationPlugin */
+ $citationPlugin = PluginRegistry::getPlugin('generic', 'citationstylelanguageplugin');
+ return trim(
+ strip_tags(
+ $citationPlugin->getCitation(
+ $request,
+ $submission,
+ 'bibtex',
+ $this->issue,
+ $this->publication
+ )
+ )
+ );
+ } catch (\Exception $exception) {
+ return '';
+ }
+ }
+}
diff --git a/classes/orcid/actions/SendReviewToOrcid.php b/classes/orcid/actions/SendReviewToOrcid.php
new file mode 100644
index 00000000000..e72bed6e6f1
--- /dev/null
+++ b/classes/orcid/actions/SendReviewToOrcid.php
@@ -0,0 +1,27 @@
+submission, $this->context, $this->reviewAssignment));
+ }
+}
diff --git a/classes/orcid/actions/SendSubmissionToOrcid.php b/classes/orcid/actions/SendSubmissionToOrcid.php
new file mode 100644
index 00000000000..bb7de09e32e
--- /dev/null
+++ b/classes/orcid/actions/SendSubmissionToOrcid.php
@@ -0,0 +1,44 @@
+publication->getData('issueId');
+ if (isset($issueId)) {
+ $issue = Repo::issue()->get($issueId);
+ }
+
+ return new OrcidWork($this->publication, $this->context, $authors, $issue ?? null);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function canDepositSubmission(): bool
+ {
+ return true;
+ }
+}
diff --git a/dbscripts/xml/upgrade.xml b/dbscripts/xml/upgrade.xml
index 168781f85d4..ad92cd9da0f 100644
--- a/dbscripts/xml/upgrade.xml
+++ b/dbscripts/xml/upgrade.xml
@@ -140,6 +140,7 @@
+
diff --git a/jobs/orcid/DepositOrcidReview.php b/jobs/orcid/DepositOrcidReview.php
new file mode 100644
index 00000000000..f8bd572f262
--- /dev/null
+++ b/jobs/orcid/DepositOrcidReview.php
@@ -0,0 +1,121 @@
+fail('Application is set to sandbox mode and will not interact with the ORCID service');
+ return;
+ }
+
+ if (!OrcidManager::isMemberApiEnabled($this->context)) {
+ return;
+ }
+
+ if (!OrcidManager::getCity($this->context) || !OrcidManager::getCountry($this->context)) {
+ return;
+ }
+
+ $reviewer = Repo::user()->get($this->reviewAssignment->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 = new OrcidReview($this->submission, $this->reviewAssignment, $this->context);
+
+ $uri = OrcidManager::getApiPath($this->context) . OrcidManager::ORCID_API_VERSION_URL . $orcid . '/' . OrcidManager::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->toArray(),
+ ]
+ );
+
+ $httpStatus = $response->getStatusCode();
+ OrcidManager::logInfo("Response status: {$httpStatus}");
+ $responseHeaders = $response->getHeaders();
+ switch ($httpStatus) {
+ case 200:
+ OrcidManager::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']);
+ OrcidManager::logInfo("Review added to profile, putCode: {$putCode}");
+ break;
+ default:
+ OrcidManager::logError("Unexpected status {$httpStatus} response, body: " . json_encode($responseHeaders));
+ }
+ } catch (ClientException $exception) {
+ $reason = $exception->getResponse()->getBody();
+ OrcidManager::logError("Publication fail: {$reason}");
+
+ $this->fail($exception);
+ }
+ }
+ }
+ }
+}
diff --git a/pages/article/ArticleHandler.php b/pages/article/ArticleHandler.php
index 18de5d32962..57ae0b93020 100644
--- a/pages/article/ArticleHandler.php
+++ b/pages/article/ArticleHandler.php
@@ -34,6 +34,7 @@
use PKP\core\PKPApplication;
use PKP\core\PKPJwt as JWT;
use PKP\db\DAORegistry;
+use PKP\orcid\OrcidManager;
use PKP\plugins\Hook;
use PKP\plugins\PluginRegistry;
use PKP\security\authorization\ContextRequiredPolicy;
@@ -307,6 +308,7 @@ public function view($args, $request)
'copyrightYear' => $publication->getData('copyrightYear'),
'pubIdPlugins' => PluginRegistry::loadCategory('pubIds', true),
'keywords' => $publication->getData('keywords'),
+ 'orcidIcon' => OrcidManager::getIcon(),
]);
// Fetch and assign the galley to the template
diff --git a/plugins/themes/default/styles/index.less b/plugins/themes/default/styles/index.less
index 52eea2223f7..5b82741f955 100644
--- a/plugins/themes/default/styles/index.less
+++ b/plugins/themes/default/styles/index.less
@@ -20,6 +20,8 @@
@import "../../../../lib/pkp/styles/variables.less";
@import "../../../../lib/pkp/styles/utils.less";
@import "../../../../lib/pkp/styles/helpers.less";
+// General ORCID styles
+@import '../../../../lib/pkp/styles/orcid.less';
// Styles unique to this theme
@import "variables.less";
diff --git a/registry/emailTemplates.xml b/registry/emailTemplates.xml
index de4eee21cf6..736686fbad0 100644
--- a/registry/emailTemplates.xml
+++ b/registry/emailTemplates.xml
@@ -77,4 +77,6 @@
+
+
diff --git a/registry/uiLocaleKeysBackend.json b/registry/uiLocaleKeysBackend.json
index a0f3825ede5..c0609fb0dd0 100644
--- a/registry/uiLocaleKeysBackend.json
+++ b/registry/uiLocaleKeysBackend.json
@@ -261,6 +261,13 @@
"manager.dois.update.success",
"metadata.property.displayName.doi",
"navigation.backTo",
+ "orcid.field.authorEmailModal.message",
+ "orcid.field.authorEmailModal.title",
+ "orcid.field.deleteOrcidModal.message",
+ "orcid.field.deleteOrcidModal.title",
+ "orcid.field.unverified.shouldRequest",
+ "orcid.field.verification.request",
+ "orcid.field.verification.requested",
"publication.contributors",
"publication.jats.autoCreatedMessage",
"publication.jats.confirmDeleteFileButton",