Open Journal Systems  3.3.0
OrcidProfilePlugin.inc.php
1 <?php
2 
17 import('lib.pkp.classes.plugins.GenericPlugin');
18 
19 define('ORCID_URL', 'https://orcid.org/');
20 define('ORCID_URL_SANDBOX', 'https://sandbox.orcid.org/');
21 define('ORCID_API_URL_PUBLIC', 'https://pub.orcid.org/');
22 define('ORCID_API_URL_PUBLIC_SANDBOX', 'https://pub.sandbox.orcid.org/');
23 define('ORCID_API_URL_MEMBER', 'https://api.orcid.org/');
24 define('ORCID_API_URL_MEMBER_SANDBOX', 'https://api.sandbox.orcid.org/');
25 define('ORCID_API_VERSION_URL', 'v2.1/');
26 define('ORCID_API_SCOPE_PUBLIC', '/authenticate');
27 define('ORCID_API_SCOPE_MEMBER', '/activities/update');
28 
29 define('OAUTH_TOKEN_URL', 'oauth/token');
30 define('ORCID_EMPLOYMENTS_URL', 'employments');
31 define('ORCID_PROFILE_URL', 'person');
32 define('ORCID_EMAIL_URL', 'email');
33 define('ORCID_WORK_URL', 'work');
34 
36  const PUBID_TO_ORCID_EXT_ID = ["doi" => "doi", "other::urn" => "urn"];
37  const USER_GROUP_TO_ORCID_ROLE = ["Author" => "AUTHOR", "Translator" => "CHAIR_OR_TRANSLATOR","Journal manager"=>"AUTHOR"];
38 
39  private $submissionIdToBePublished;
40  private $currentContextId;
41 
45  function register($category, $path, $mainContextId = null) {
46  $success = parent::register($category, $path, $mainContextId);
47  if (!Config::getVar('general', 'installed') || defined('RUNNING_UPGRADE')) return true;
48  if ($success && $this->getEnabled($mainContextId)) {
49  $contextId = ($mainContextId === null) ? $this->getCurrentContextId() : $mainContextId;
50 
51  // Insert the OrcidHandler to handle ORCID redirects
52  HookRegistry::register('LoadHandler', array($this, 'setupCallbackHandler'));
53 
54  // Register callback for Smarty filters; add CSS
55  HookRegistry::register('TemplateManager::display', array($this, 'handleTemplateDisplay'));
56 
57  // Add "Connect ORCID" button to PublicProfileForm
58  HookRegistry::register('User::PublicProfile::AdditionalItems', array($this, 'handleUserPublicProfileDisplay'));
59 
60  // Display additional ORCID access information and checkbox to send e-mail to authors in the AuthorForm
61  HookRegistry::register('authorform::display', array($this, 'handleFormDisplay'));
62 
63  // Send email to author, if the added checkbox was ticked
64  HookRegistry::register('authorform::execute', array($this, 'handleAuthorFormExecute'));
65 
66  // Handle ORCID on user registration
67  HookRegistry::register('registrationform::execute', array($this, 'collectUserOrcidId'));
68 
69  // Send emails to authors without ORCID id upon submission
70  HookRegistry::register('submissionsubmitstep3form::execute', array($this, 'handleSubmissionSubmitStep3FormExecute'));
71 
72  HookRegistry::register('authordao::getAdditionalFieldNames', array($this, 'handleAdditionalFieldNames'));
73 
74  // Add more ORCiD fields to UserDAO
75  HookRegistry::register('userdao::getAdditionalFieldNames', array($this, 'handleAdditionalFieldNames'));
76 
77  // Send emails to authors without authorised ORCID access on promoting a submission to copy editing. Not included in OPS.
78  if ($this->getSetting($contextId, 'sendMailToAuthorsOnPublication')) {
79  HookRegistry::register('EditorAction::recordDecision', array($this, 'handleEditorAction'));
80  }
81 
82  HookRegistry::register('Publication::publish', array($this, 'handlePublicationStatusChange'));
83 
84  // Add more ORCiD fields to author Schema
85  HookRegistry::register('Schema::get::author', function ($hookName, $args) {
86  $schema = $args[0];
87 
88  $schema->properties->orcidSandbox = (object)[
89  'type' => 'string',
90  'apiSummary' => true,
91  'validation' => ['nullable']
92  ];
93  $schema->properties->orcidAccessToken = (object)[
94  'type' => 'string',
95  'apiSummary' => true,
96  'validation' => ['nullable']
97  ];
98  $schema->properties->orcidAccessScope = (object)[
99  'type' => 'string',
100  'apiSummary' => true,
101  'validation' => ['nullable']
102  ];
103  $schema->properties->orcidRefreshToken = (object)[
104  'type' => 'string',
105  'apiSummary' => true,
106  'validation' => ['nullable']
107  ];
108  $schema->properties->orcidAccessExpiresOn = (object)[
109  'type' => 'string',
110  'apiSummary' => true,
111  'validation' => ['nullable']
112  ];
113  $schema->properties->orcidAccessDenied = (object)[
114  'type' => 'string',
115  'apiSummary' => true,
116  'validation' => ['nullable']
117  ];
118  $schema->properties->orcidEmailToken = (object)[
119  'type' => 'string',
120  'apiSummary' => true,
121  'validation' => ['nullable']
122  ];
123  $schema->properties->orcidWorkPutCode = (object)[
124  'type' => 'string',
125  'apiSummary' => true,
126  'validation' => ['nullable']
127  ];
128  });
129  }
130 
131  return $success;
132  }
133 
138  function getHandlerPath() {
139  return $this->getPluginPath() . DIRECTORY_SEPARATOR . 'pages';
140  }
141 
147  function setupCallbackHandler($hookName, $params) {
148  $page = $params[0];
149  if ($this->getEnabled() && $page == 'orcidapi') {
150  $this->import('pages/OrcidHandler');
151  define('HANDLER_CLASS', 'OrcidHandler');
152  return true;
153  }
154  return false;
155  }
156 
165  function getSetting($contextId, $name) {
166  switch ($name) {
167  case 'orcidProfileAPIPath':
168  $config_value = Config::getVar('orcid', 'api_url');
169  break;
170  case 'orcidClientId':
171  $config_value = Config::getVar('orcid', 'client_id');
172  break;
173  case 'orcidClientSecret':
174  $config_value = Config::getVar('orcid', 'client_secret');
175  break;
176  default:
177  return parent::getSetting($contextId, $name);
178  }
179 
180  return $config_value ?: parent::getSetting($contextId, $name);
181  }
182 
187  function isGloballyConfigured() {
188  $apiUrl = Config::getVar('orcid', 'api_url');
189  $clientId = Config::getVar('orcid', 'client_id');
190  $clientSecret = Config::getVar('orcid', 'client_secret');
191  return isset($apiUrl) && trim($apiUrl) && isset($clientId) && trim($clientId) &&
192  isset($clientSecret) && trim($clientSecret);
193  }
194 
206  function handleFormDisplay($hookName, $args) {
207  $request = PKPApplication::get()->getRequest();
208  $templateMgr = TemplateManager::getManager($request);
209  switch ($hookName) {
210  case 'authorform::display':
211  $authorForm =& $args[0];
212  $author = $authorForm->getAuthor();
213  if ($author) {
214  $authenticated = !empty($author->getData('orcidAccessToken'));
215  $templateMgr->assign(
216  array(
217  'orcidAccessToken' => $author->getData('orcidAccessToken'),
218  'orcidAccessScope' => $author->getData('orcidAccessScope'),
219  'orcidAccessExpiresOn' => $author->getData('orcidAccessExpiresOn'),
220  'orcidAccessDenied' => $author->getData('orcidAccessDenied'),
221  'orcidAuthenticated' => $authenticated
222  )
223  );
224  }
225 
226  $templateMgr->registerFilter("output", array($this, 'authorFormFilter'));
227  break;
228  }
229  return false;
230  }
231 
241  function handleTemplateDisplay($hookName, $args) {
242  $templateMgr =& $args[0];
243  $template =& $args[1];
244  $request = PKPApplication::get()->getRequest();
245 
246  // Assign our private stylesheet, for front and back ends.
247  $templateMgr->addStyleSheet(
248  'orcidProfile',
249  $request->getBaseUrl() . '/' . $this->getStyleSheet(),
250  array(
251  'contexts' => array('frontend', 'backend')
252  )
253  );
254 
255  switch ($template) {
256  case 'frontend/pages/userRegister.tpl':
257  $templateMgr->registerFilter("output", array($this, 'registrationFilter'));
258  break;
259  case 'frontend/pages/article.tpl':
260  case 'frontend/pages/preprint.tpl':
261  $script = 'var orcidIconSvg = ' . json_encode($this->getIcon()) . ';';
262 
263  $publication = $templateMgr->getTemplateVars('publication');
264 
265  $authors = $publication->getData('authors');
267  foreach ($authors as $author) {
268  if (!empty($author->getOrcid()) && !empty($author->getData('orcidAccessToken'))) {
269  $script .= '$("a[href=\"' . $author->getOrcid() . '\"]").prepend(orcidIconSvg);';
270  }
271  }
272 
273  $templateMgr->addJavaScript('orcidIconDisplay', $script, ['inline' => true]);
274 
275  break;
276  }
277  return false;
278  }
279 
285  function getOauthPath() {
286  return $this->getOrcidUrl() . 'oauth/';
287  }
288 
294  function getOrcidUrl() {
295  $request = Application::get()->getRequest();
296  $context = $request->getContext();
297  $contextId = ($context == null) ? 0 : $context->getId();
298 
299  $apiPath = $this->getSetting($contextId, 'orcidProfileAPIPath');
300  if ($apiPath == ORCID_API_URL_PUBLIC || $apiPath == ORCID_API_URL_MEMBER) {
301  return ORCID_URL;
302  } else {
303  return ORCID_URL_SANDBOX;
304  }
305  }
306 
313  function buildOAuthUrl($handlerMethod, $redirectParams) {
314  $request = PKPApplication::get()->getRequest();
315  $context = $request->getContext();
316  // This should only ever happen within a context, never site-wide.
317  assert($context != null);
318  $contextId = $context->getId();
319 
320  if ($this->isMemberApiEnabled($contextId)) {
321  $scope = ORCID_API_SCOPE_MEMBER;
322  } else {
323  $scope = ORCID_API_SCOPE_PUBLIC;
324  }
325  // We need to construct a page url, but the request is using the component router.
326  // Use the Dispatcher to construct the url and set the page router.
327  $redirectUrl = $request->getDispatcher()->url($request, ROUTE_PAGE, null, 'orcidapi',
328  $handlerMethod, null, $redirectParams);
329 
330  return $this->getOauthPath() . 'authorize?' . http_build_query(
331  array(
332  'client_id' => $this->getSetting($contextId, 'orcidClientId'),
333  'response_type' => 'code',
334  'scope' => $scope,
335  'redirect_uri' => $redirectUrl)
336  );
337  }
338 
346  function registrationFilter($output, $templateMgr) {
347  if (preg_match('/<form[^>]+id="register"[^>]+>/', $output, $matches, PREG_OFFSET_CAPTURE)) {
348  $match = $matches[0][0];
349  $offset = $matches[0][1];
350  $request = Application::get()->getRequest();
351  $context = $request->getContext();
352  $contextId = ($context == null) ? 0 : $context->getId();
353  $targetOp = 'register';
354  $templateMgr->assign(array(
355  'targetOp' => $targetOp,
356  'orcidUrl' => $this->getOrcidUrl(),
357  'orcidOAuthUrl' => $this->buildOAuthUrl('orcidAuthorize', array('targetOp' => $targetOp)),
358  'orcidIcon' => $this->getIcon(),
359  ));
360 
361  $newOutput = substr($output, 0, $offset + strlen($match));
362  $newOutput .= $templateMgr->fetch($this->getTemplateResource('orcidProfile.tpl'));
363  $newOutput .= substr($output, $offset + strlen($match));
364  $output = $newOutput;
365  $templateMgr->unregisterFilter('output', array($this, 'registrationFilter'));
366  }
367  return $output;
368  }
369 
379  function handleUserPublicProfileDisplay($hookName, $params) {
380  $templateMgr =& $params[1];
381  $output =& $params[2];
382  $request = Application::get()->getRequest();
383  $context = $request->getContext();
384  $user = $request->getUser();
385  $contextId = ($context == null) ? 0 : $context->getId();
386  $targetOp = 'profile';
387  $templateMgr->assign(
388  array(
389  'targetOp' => $targetOp,
390  'orcidUrl' => $this->getOrcidUrl(),
391  'orcidOAuthUrl' => $this->buildOAuthUrl('orcidAuthorize', array('targetOp' => $targetOp)),
392  'orcidClientId' => $this->getSetting($contextId, 'orcidClientId'),
393  'orcidIcon' => $this->getIcon(),
394  'orcidAuthenticated' => !empty($user->getData('orcidAccessToken')),
395  )
396  );
397 
398  $output = $templateMgr->fetch($this->getTemplateResource('orcidProfile.tpl'));
399  return true;
400  }
401 
409  function authorFormFilter($output, $templateMgr) {
410  if (preg_match('/<input[^>]+name="submissionId"[^>]*>/', $output, $matches, PREG_OFFSET_CAPTURE)) {
411  $match = $matches[0][0];
412  $offset = $matches[0][1];
413  $templateMgr->assign('orcidIcon', $this->getIcon());
414  $newOutput = substr($output, 0, $offset + strlen($match));
415  $newOutput .= $templateMgr->fetch($this->getTemplateResource('authorFormOrcid.tpl'));
416  $newOutput .= substr($output, $offset + strlen($match));
417  $output = $newOutput;
418  $templateMgr->unregisterFilter('output', array($this, 'authorFormFilter'));
419  }
420  return $output;
421  }
422 
431  function handleAuthorFormExecute($hookname, $args) {
432  $form =& $args[0];
433  $form->readUserVars(array('requestOrcidAuthorization', 'deleteOrcid'));
434 
435  $requestAuthorization = $form->getData('requestOrcidAuthorization');
436  $deleteOrcid = $form->getData('deleteOrcid');
437  $author = $form->getAuthor();
438 
439  if ($author && $requestAuthorization) {
440  $this->sendAuthorMail($author);
441  }
442 
443  if ($author && $deleteOrcid) {
444  $author->setOrcid(null);
445  $this->removeOrcidAccessToken($author, false);
446  }
447  }
448 
456  function collectUserOrcidId($hookName, $params) {
457  $form = $params[0];
458  $user = $form->user;
459 
460  $form->readUserVars(array('orcid'));
461  $user->setOrcid($form->getData('orcid'));
462  return false;
463  }
464 
472  function handleSubmissionSubmitStep3FormExecute($hookName, $params) {
473  $form = $params[0];
474  // Have to use global Request access because request is not passed to hook
475  $publicationDao = DAORegistry::getDAO('PublicationDAO');
476  /* @var $publicationDao PublicationDAO */
477  $publication = $publicationDao->getById($form->submission->getData('currentPublicationId'));
478  $authors = $publication->getData('authors');
479 
480  $request = Application::get()->getRequest();
481  $user = $request->getUser();
482  //error_log("OrcidProfilePlugin: authors[0] = " . var_export($authors[0], true));
483  //error_log("OrcidProfilePlugin: user = " . var_export($user, true));
484  if ($authors[0]->getOrcid() === $user->getOrcid()) {
485  // if the author and user share the same ORCID id
486  // copy the access token from the user
487  //error_log("OrcidProfilePlugin: user->orcidAccessToken = " . $user->getData('orcidAccessToken'));
488  $authors[0]->setData('orcidAccessToken', $user->getData('orcidAccessToken'));
489  $authors[0]->setData('orcidAccessScope', $user->getData('orcidAccessScope'));
490  $authors[0]->setData('orcidRefreshToken', $user->getData('orcidRefreshToken'));
491  $authors[0]->setData('orcidAccessExpiresOn', $user->getData('orcidAccessExpiresOn'));
492  $authors[0]->setData('orcidSandbox', $user->getData('orcidSandbox'));
493 
494  $authorDao = DAORegistry::getDAO('AuthorDAO');
495  /* @var $authorDao AuthorDAO */
496  $authorDao->updateObject($authors[0]);
497 
498  //error_log("OrcidProfilePlugin: author = " . var_export($authors[0], true));
499  }
500  return false;
501  }
502 
511  function handleAdditionalFieldNames($hookName, $params) {
512  $fields =& $params[1];
513  $fields[] = 'orcidSandbox';
514  $fields[] = 'orcidAccessToken';
515  $fields[] = 'orcidAccessScope';
516  $fields[] = 'orcidRefreshToken';
517  $fields[] = 'orcidAccessExpiresOn';
518  $fields[] = 'orcidAccessDenied';
519 
520  return false;
521  }
522 
526  function getDisplayName() {
527  return __('plugins.generic.orcidProfile.displayName');
528  }
529 
533  function getDescription() {
534  return __('plugins.generic.orcidProfile.description');
535  }
536 
541  return ($this->getPluginPath() . '/emailTemplates.xml');
542  }
543 
547  function smartyPluginUrl($params, $smarty) {
548  $path = array($this->getCategory(), $this->getName());
549  if (is_array($params['path'])) {
550  $params['path'] = array_merge($path, $params['path']);
551  } elseif (!empty($params['path'])) {
552  $params['path'] = array_merge($path, array($params['path']));
553  } else {
554  $params['path'] = $path;
555  }
556 
557  if (!empty($params['id'])) {
558  $params['path'] = array_merge($params['path'], array($params['id']));
559  unset($params['id']);
560  }
561  return $smarty->smartyUrl($params, $smarty);
562  }
563 
567  function getActions($request, $actionArgs) {
568  $router = $request->getRouter();
569  import('lib.pkp.classes.linkAction.request.AjaxModal');
570  return array_merge(
571  $this->getEnabled() ? array(
572  new LinkAction(
573  'settings',
574  new AjaxModal(
575  $router->url(
576  $request,
577  null,
578  null,
579  'manage',
580  null,
581  array(
582  'verb' => 'settings',
583  'plugin' => $this->getName(),
584  'category' => 'generic'
585  )
586  ),
587  $this->getDisplayName()
588  ),
589  __('manager.plugins.settings'),
590  null
591  ),
592  ) : array(),
593  parent::getActions($request, $actionArgs)
594  );
595  }
596 
600  function manage($args, $request) {
601  switch ($request->getUserVar('verb')) {
602  case 'settings':
603  $context = $request->getContext();
604  $contextId = ($context == null) ? 0 : $context->getId();
605 
606  $templateMgr = TemplateManager::getManager();
607  $templateMgr->registerPlugin('function', 'plugin_url', array($this, 'smartyPluginUrl'));
608  $apiOptions = [
609  ORCID_API_URL_PUBLIC => 'plugins.generic.orcidProfile.manager.settings.orcidProfileAPIPath.public',
610  ORCID_API_URL_PUBLIC_SANDBOX => 'plugins.generic.orcidProfile.manager.settings.orcidProfileAPIPath.publicSandbox',
611  ORCID_API_URL_MEMBER => 'plugins.generic.orcidProfile.manager.settings.orcidProfileAPIPath.member',
612  ORCID_API_URL_MEMBER_SANDBOX => 'plugins.generic.orcidProfile.manager.settings.orcidProfileAPIPath.memberSandbox'
613  ];
614  $templateMgr->assign('orcidApiUrls', $apiOptions);
615  $templateMgr->assign('logLevelOptions', [
616  'ERROR' => 'plugins.generic.orcidProfile.manager.settings.logLevel.error',
617  'ALL' => 'plugins.generic.orcidProfile.manager.settings.logLevel.all'
618  ]);
619  $this->import('OrcidProfileSettingsForm');
620  $form = new OrcidProfileSettingsForm($this, $contextId);
621  if ($request->getUserVar('save')) {
622  $form->readInputData();
623  if ($form->validate()) {
624  $form->execute();
625  return new JSONMessage(true);
626  }
627  } else {
628  $form->initData();
629  }
630  return new JSONMessage(true, $form->fetch($request));
631  }
632  return parent::manage($args, $request);
633  }
634 
640  function getStyleSheet() {
641  return $this->getPluginPath() . '/css/orcidProfile.css';
642  }
643 
649  function getIcon() {
650  $path = Core::getBaseDir() . '/' . $this->getPluginPath() . '/templates/images/orcid.svg';
651  return file_exists($path) ? file_get_contents($path) : '';
652  }
653 
662  function getMailTemplate($emailKey, $context = null) {
663  import('lib.pkp.classes.mail.MailTemplate');
664  return new MailTemplate($emailKey, null, $context, false);
665  }
666 
674  public function sendAuthorMail($author, $updateAuthor = false) {
675  $request = PKPApplication::get()->getRequest();
676  $context = $request->getContext();
677 
678  // This should only ever happen within a context, never site-wide.
679  assert($context != null);
680 
681  $contextId = $context->getId();
682 
683  if ($this->isMemberApiEnabled($contextId)) {
684  $mailTemplate = 'ORCID_REQUEST_AUTHOR_AUTHORIZATION';
685  } else {
686  $mailTemplate = 'ORCID_COLLECT_AUTHOR_ID';
687  }
688 
689  $mail = $this->getMailTemplate($mailTemplate, $context);
690  $emailToken = md5(microtime() . $author->getEmail());
691 
692  $author->setData('orcidEmailToken', $emailToken);
693 
694  $publicationDao = DAORegistry::getDAO('PublicationDAO');
696  $publication = $publicationDao->getById($author->getData('publicationId'));
697 
698  $oauthUrl = $this->buildOAuthUrl('orcidVerify', array('token' => $emailToken, 'publicationId' => $publication->getId()));
699  $aboutUrl = $request->getDispatcher()->url($request, ROUTE_PAGE, null, 'orcidapi', 'about', null);
700 
701  // Set From to primary journal contact
702  $mail->setFrom($context->getData('contactEmail'), $context->getData('contactName'));
703 
704  // Send to author
705  $mail->setRecipients(array(array('name' => $author->getFullName(), 'email' => $author->getEmail())));
706 
707  // Send the mail with parameters
708  $mail->sendWithParams(array(
709  'orcidAboutUrl' => $aboutUrl,
710  'authorOrcidUrl' => $oauthUrl,
711  'authorName' => $author->getFullName(),
712  'articleTitle' => $publication->getLocalizedTitle(), // Backwards compatibility only
713  'submissionTitle' => $publication->getLocalizedTitle(),
714  ));
715 
716  if ($updateAuthor) {
717  $authorDao = DAORegistry::getDAO('AuthorDAO');
718  $authorDao->updateObject($author);
719  }
720  }
721 
732  public function handlePublicationStatusChange($hookName, $args) {
733  $newPublication =& $args[0];
735  $publication =& $args[1];
737  $submission =& $args[2];
740  $request = PKPApplication::get()->getRequest();
741 
742  switch ($newPublication->getData('status')) {
743  case STATUS_PUBLISHED:
744  case STATUS_SCHEDULED:
745  $this->sendSubmissionToOrcid($newPublication, $request);
746  break;
747  }
748  }
749 
758  public function handleEditorAction($hookName, $args) {
759  $submission = $args[0];
761  $decision = $args[1];
762 
763  if ($decision['decision'] == SUBMISSION_EDITOR_DECISION_ACCEPT) {
764  $publication = $submission->getCurrentPublication();
765 
766  if (isset($publication)) {
767  $authorDao = DAORegistry::getDAO('AuthorDAO');
770  $authors = $authorDao->getByPublicationId($submission->getCurrentPublication()->getId());
771 
772  foreach ($authors as $author) {
773  $orcidAccessExpiresOn = Carbon\Carbon::parse($author->getData('orcidAccessExpiresOn'));
774  if ($author->getData('orcidAccessToken') == null || $orcidAccessExpiresOn->isPast()) {
775  $this->sendAuthorMail($author, true);
776  }
777  }
778  }
779 
780  }
781  }
782 
795  public function sendSubmissionToOrcid($publication, $request) {
796  $context = $request->getContext();
797  $contextId = $this->currentContextId = $context->getId();
798  $publicationId = $publication->getId();
799  $submissionId = $publication->getData('submissionId');
800 
801  if (!$this->isMemberApiEnabled($contextId)) {
802  // Sending to ORCID only works with the member API
803  return false;
804  }
805 
806  $issueId = $publication->getData('issueId');
807  if (isset($issueId)) {
808  $issue = Services::get('issue')->get($issueId);
809  }
810 
811  $authorDao = DAORegistry::getDAO('AuthorDAO');
813  $authors = $authorDao->getByPublicationId($publicationId);
814 
815  // Collect valid ORCID ids and access tokens from submission contributors
816  $authorsWithOrcid = [];
817  foreach ($authors as $author) {
818  if ($author->getOrcid() && $author->getData('orcidAccessToken')) {
819  $orcidAccessExpiresOn = Carbon\Carbon::parse($author->getData('orcidAccessExpiresOn'));
820  if ($orcidAccessExpiresOn->isFuture()) {
821  # Extract only the ORCID from the stored ORCID uri
822  $orcid = basename(parse_url($author->getOrcid(), PHP_URL_PATH));
823  $authorsWithOrcid[$orcid] = $author;
824  } else {
825  $this->logError("Token expired on $orcidAccessExpiresOn for author " . $author->getId() . ", deleting orcidAccessToken!");
826  $this->removeOrcidAccessToken($author);
827  }
828  }
829  }
830 
831  if (empty($authorsWithOrcid)) {
832  $this->logInfo('No contributor with ORICD id or valid access token for submission ' . $submissionId);
833  return false;
834  }
835 
836  $orcidWork = $this->buildOrcidWork($publication, $context, $authors, $request, $issue);
837  $this::logInfo("Request body (without put-code): " . json_encode($orcidWork));
838 
839  $requestsSuccess = [];
840  foreach ($authorsWithOrcid as $orcid => $author) {
841  $url = $this->getSetting($contextId, 'orcidProfileAPIPath') . ORCID_API_VERSION_URL . $orcid . '/' . ORCID_WORK_URL;
842  $method = "POST";
843 
844  if ($putCode = $author->getData('orcidWorkPutCode')) {
845  // Submission has already been sent to ORCID. Use PUT to update meta data
846  $url .= '/' . $putCode;
847  $method = "PUT";
848  $orcidWork['put-code'] = $putCode;
849  } else {
850  // Remove put-code from body because the work has not yet been sent
851  unset($orcidWork['put-code']);
852  }
853 
854  $orcidWorkJson = json_encode($orcidWork);
855 
856  $header = [
857  'Content-Type: application/vnd.orcid+json',
858  'Content-Length: ' . strlen($orcidWorkJson),
859  'Accept: application/json',
860  'Authorization: Bearer ' . $author->getData('orcidAccessToken')
861  ];
862 
863  $this->logInfo("$method $url");
864  $this->logInfo("Header: " . var_export($header, true));
865 
866  $ch = curl_init($url);
867  curl_setopt_array($ch, [
868  CURLOPT_CUSTOMREQUEST => $method,
869  CURLOPT_POSTFIELDS => $orcidWorkJson,
870  CURLOPT_RETURNTRANSFER => true,
871  CURLOPT_HTTPHEADER => $header
872  ]);
873  // Use proxy if configured
874  if ($httpProxyHost = Config::getVar('proxy', 'http_host')) {
875  curl_setopt($ch, CURLOPT_PROXY, $httpProxyHost);
876  curl_setopt($ch, CURLOPT_PROXYPORT, Config::getVar('proxy', 'http_port', '80'));
877  if ($username = Config::getVar('proxy', 'username')) {
878  curl_setopt($ch, CURLOPT_PROXYUSERPWD, $username . ':' . Config::getVar('proxy', 'password'));
879  }
880  }
881 
882  $responseHeaders = [];
883  // Needed to correctly process response headers.
884  // This function is called by curl for each received line of the header.
885  // Code from StackOverflow answer here: https://stackoverflow.com/a/41135574/8938233
886  // Thanks to StackOverflow user Geoffrey.
887  curl_setopt($ch, CURLOPT_HEADERFUNCTION,
888  function ($curl, $header) use (&$responseHeaders) {
889  $len = strlen($header);
890  $header = explode(':', $header, 2);
891  if (count($header) < 2) {
892  // ignore invalid headers
893  return $len;
894  }
895 
896  $name = strtolower(trim($header[0]));
897  if (!array_key_exists($name, $responseHeaders)) {
898  $responseHeaders[$name] = [trim($header[1])];
899  } else {
900  $responseHeaders[$name][] = trim($header[1]);
901  }
902 
903  return $len;
904  }
905  );
906 
907  $result = curl_exec($ch);
908 
909  if (curl_error($ch)) {
910  $this->logError('Unable to post to ORCID API, curl error: ' . curl_error($ch));
911  curl_close($ch);
912  return false;
913  }
914 
915  $httpstatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
916  curl_close($ch);
917 
918  $this->logInfo("Response status: $httpstatus");
919 
920  switch ($httpstatus) {
921  case 200:
922  // Work updated
923  $this->logInfo("Work updated in profile, putCode: $putCode");
924  $requestsSuccess[$orcid] = true;
925  break;
926  case 201:
927  $location = $responseHeaders['location'][0];
928  // Extract the ORCID work put code for updates/deletion.
929  $putCode = intval(basename(parse_url($location, PHP_URL_PATH)));
930  $this->logInfo("Work added to profile, putCode: $putCode");
931  $author->setData('orcidWorkPutCode', $putCode);
932  $authorDao->updateObject($author);
933  $requestsSuccess[$orcid] = true;
934  break;
935  case 401:
936  // invalid access token, token was revoked
937  $error = json_decode($result);
938  if ($error->error === 'invalid_token') {
939  $this->logError("$error->error_description, deleting orcidAccessToken from author");
940  $this->removeOrcidAccessToken($author);
941  }
942  $requestsSuccess[$orcid] = false;
943  break;
944  case 404:
945  // a work has been deleted from a ORCID record. putCode is no longer valid.
946  if ($method === 'PUT') {
947  $this->logError("Work deleted from ORCID record, deleting putCode form author");
948  $author->setData('orcidWorkPutCode', null);
949  $authorDao->updateObject($author);
950  $requestsSuccess[$orcid] = false;
951  } else {
952  $this->logError("Unexpected status $httpstatus response, body: $result");
953  $requestsSuccess[$orcid] = false;
954  }
955  break;
956  case 409:
957  $this->logError('Work already added to profile, response body: ' . $result);
958  $requestsSuccess[$orcid] = false;
959  break;
960  default:
961  $this->logError("Unexpected status $httpstatus response, body: $result");
962  $requestsSuccess[$orcid] = false;
963  }
964  }
965  if (array_product($requestsSuccess)) {
966  return true;
967  } else {
968  return $requestsSuccess;
969  }
970  }
971 
985  public function buildOrcidWork($publication, $context, $authors, $request, $issue = null) {
986  $submission = Services::get('submission')->get($publication->getData('submissionId'));
987 
988  PluginRegistry::loadCategory('generic');
989  $citationPlugin = PluginRegistry::getPlugin('generic', 'citationstylelanguageplugin');
991  $bibtexCitation = trim(strip_tags($citationPlugin->getCitation($request, $submission, 'bibtex', $issue, $publication)));
992 
993  $publicationLocale = ($publication->getData('locale')) ? $publication->getData('locale') : 'en_US';
994  $supportedSubmissionLocales = $context->getSupportedSubmissionLocales();
995 
996  $publicationUrl = $request->getDispatcher()->url($request, ROUTE_PAGE, null, 'article', 'view', $submission->getId());
997 
998  $orcidWork = [
999  'title' => [
1000  'title' => [
1001  'value' => $publication->getLocalizedData('title', $publicationLocale) ?? ''
1002  ],
1003  'subtitle' => [
1004  'value' => $publication->getLocalizedData('subtitle', $publicationLocale) ?? ''
1005  ]
1006  ],
1007  'journal-title' => [
1008  'value' => $context->getName($publicationLocale) ?? ''
1009  ],
1010  'short-description' => trim(strip_tags($publication->getLocalizedData('abstract', $publicationLocale))) ?? '',
1011  'type' => 'JOURNAL_ARTICLE',
1012  'external-ids' => [
1013  'external-id' => $this->buildOrcidExternalIds($submission, $publication, $context, $issue, $publicationUrl)
1014  ],
1015  'publication-date' => $this->buildOrcidPublicationDate($publication, $issue),
1016  'url' => $publicationUrl,
1017  'citation' => [
1018  'citation-type' => 'BIBTEX',
1019  'citation-value' => $bibtexCitation
1020  ],
1021  'language-code' => substr($publicationLocale, 0, 2),
1022  'contributors' => [
1023  'contributor' => $this->buildOrcidContributors($authors, $context, $publication)
1024  ]
1025  ];
1026  $translatedTitleAvailable = false;
1027  foreach ($supportedSubmissionLocales as $defaultLanguage) {
1028  if ($defaultLanguage !== $publicationLocale) {
1029  $iso2LanguageCode = substr($defaultLanguage, 0, 2);
1030  $defaultTitle = $publication->getLocalizedData($iso2LanguageCode);
1031  if (strlen($defaultTitle) > 0 && !$translatedTitleAvailable) {
1032  $orcidWork['title']['translated-title'] = ['value' => $defaultTitle, 'language-code' => $iso2LanguageCode];
1033  $translatedTitleAvailable = true;
1034  }
1035  }
1036  }
1037 
1038  return $orcidWork;
1039  }
1040 
1041 
1048  private function buildOrcidPublicationDate($publication, $issue = null) {
1049  $publicationPublishDate = Carbon\Carbon::parse($publication->getData('datePublished'));
1050 
1051  return [
1052  'year' => ['value' => $publicationPublishDate->format("Y")],
1053  'month' => ['value' => $publicationPublishDate->format("m")],
1054  'day' => ['value' => $publicationPublishDate->format("d")]
1055  ];
1056  }
1057 
1069  private function buildOrcidExternalIds($submission, $publication, $context, $issue, $articleUrl) {
1070  $contextId = $context->getId();
1071 
1072  $externalIds = array();
1073  $pubIdPlugins = PluginRegistry::loadCategory('pubIds', true, $contextId);
1074  // Add doi, urn, etc. for article
1075  $articleHasStoredPubId = false;
1076  if (is_array($pubIdPlugins)) {
1077  foreach ($pubIdPlugins as $plugin) {
1078  if (!$plugin->getEnabled()) {
1079  continue;
1080  }
1081 
1082  $pubIdType = $plugin->getPubIdType();
1083 
1084  # Add article ids
1085  $pubId = $publication->getData($pubIdType);
1086 
1087  if ($pubId) {
1088  $externalIds[] = [
1089  'external-id-type' => self::PUBID_TO_ORCID_EXT_ID[$pubIdType],
1090  'external-id-value' => $pubId,
1091  'external-id-url' => [
1092  'value' => $plugin->getResolvingURL($contextId, $pubId)
1093  ],
1094  'external-id-relationship' => 'SELF'
1095  ];
1096 
1097  $articleHasStoredPubId = true;
1098  }
1099 
1100  # Add issue ids if they exist
1101  $pubId = $issue->getStoredPubId($pubIdType);
1102  if ($pubId) {
1103  $externalIds[] = [
1104  'external-id-type' => self::PUBID_TO_ORCID_EXT_ID[$pubIdType],
1105  'external-id-value' => $pubId,
1106  'external-id-url' => [
1107  'value' => $plugin->getResolvingURL($contextId, $pubId)
1108  ],
1109  'external-id-relationship' => 'PART_OF'
1110  ];
1111  }
1112  }
1113  } else {
1114  error_log("OrcidProfilePlugin::buildOrcidExternalIds: No pubId plugins could be loaded");
1115  }
1116 
1117  if (!$articleHasStoredPubId) {
1118  // No pubidplugins available or article does not have any stored pubid
1119  // Use URL as an external-id
1120  $externalIds[] = [
1121  'external-id-type' => 'uri',
1122  'external-id-value' => $articleUrl,
1123  'external-id-relationship' => 'SELF'
1124  ];
1125  }
1126 
1127  // Add journal online ISSN
1128  // TODO What about print ISSN?
1129  if ($context->getData('onlineIssn')) {
1130  $externalIds[] = [
1131  'external-id-type' => 'issn',
1132  'external-id-value' => $context->getData('onlineIssn'),
1133  'external-id-relationship' => 'PART_OF'
1134  ];
1135  }
1136 
1137  return $externalIds;
1138  }
1139 
1149  private function buildOrcidContributors($authors, $context, $publication) {
1150  $contributors = [];
1151  $first = true;
1152 
1153  foreach ($authors as $author) {
1154  // TODO Check if e-mail address should be added
1155  $fullName = $author->getLocalizedGivenName() . " " . $author->getLocalizedFamilyName();
1156 
1157  if (strlen($fullName) == 0) {
1158  $this->logError("Contributor Name not defined" . $author->getAllData());
1159  }
1160  $contributor = [
1161  'credit-name' => $fullName,
1162  'contributor-attributes' => [
1163  'contributor-sequence' => $first ? 'FIRST' : 'ADDITIONAL'
1164  ]
1165  ];
1166 
1167  $userGroup = $author->getUserGroup();
1168  $role = self::USER_GROUP_TO_ORCID_ROLE[$userGroup->getName('en_US')];
1169 
1170  if ($role) {
1171  $contributor['contributor-attributes']['contributor-role'] = $role;
1172  }
1173 
1174  if ($author->getOrcid()) {
1175  $orcid = basename(parse_url($author->getOrcid(), PHP_URL_PATH));
1176 
1177  if ($author->getData('orcidSandbox')) {
1178  $uri = ORCID_URL_SANDBOX . $orcid;
1179  $host = 'sandbox.orcid.org';
1180  } else {
1181  $uri = $author->getOrcid();
1182  $host = 'orcid.org';
1183  }
1184 
1185  $contributor['contributor-orcid'] = [
1186  'uri' => $uri,
1187  'path' => $orcid,
1188  'host' => $host
1189  ];
1190  }
1191 
1192  $first = false;
1193 
1194  $contributors[] = $contributor;
1195  }
1196 
1197  return $contributors;
1198  }
1199 
1207  public function removeOrcidAccessToken($author, $saveAuthor = true) {
1208  $author->setData('orcidAccessToken', null);
1209  $author->setData('orcidAccessScope', null);
1210  $author->setData('orcidRefreshToken', null);
1211  $author->setData('orcidAccessExpiresOn', null);
1212  $author->setData('orcidSandbox', null);
1213 
1214  if ($saveAuthor) {
1215  $authorDao = DAORegistry::getDAO('AuthorDAO');
1216  $authorDao->updateObject($author);
1217  }
1218  }
1219 
1223  public static function logFilePath() {
1224  return Config::getVar('files', 'files_dir') . '/orcid.log';
1225  }
1226 
1233  public function logError($message) {
1234  self::writeLog($message, 'ERROR');
1235  }
1236 
1243  public function logInfo($message) {
1244  if ($this->getSetting($this->currentContextId, 'logLevel') === 'ERROR') {
1245  return;
1246  } else {
1247  self::writeLog($message, 'INFO');
1248  }
1249  }
1250 
1258  private static function writeLog($message, $level) {
1259  $fineStamp = date('Y-m-d H:i:s') . substr(microtime(), 1, 4);
1260  error_log("$fineStamp $level $message\n", 3, self::logFilePath());
1261  }
1262 
1268  public function setCurrentContextId($contextId) {
1269  $this->currentContextId = $contextId;
1270  }
1271 
1275  public function isMemberApiEnabled($contextId) {
1276  $apiUrl = $this->getSetting($contextId, 'orcidProfileAPIPath');
1277  if ($apiUrl === ORCID_API_URL_MEMBER || $apiUrl === ORCID_API_URL_MEMBER_SANDBOX) {
1278  return true;
1279  } else {
1280  return false;
1281  }
1282  }
1283 }
1284 
OrcidProfilePlugin\getDisplayName
getDisplayName()
Definition: OrcidProfilePlugin.inc.php:526
OrcidProfilePlugin\authorFormFilter
authorFormFilter($output, $templateMgr)
Definition: OrcidProfilePlugin.inc.php:409
OrcidProfilePlugin\sendAuthorMail
sendAuthorMail($author, $updateAuthor=false)
Definition: OrcidProfilePlugin.inc.php:674
OrcidProfilePlugin\PUBID_TO_ORCID_EXT_ID
const PUBID_TO_ORCID_EXT_ID
Definition: OrcidProfilePlugin.inc.php:36
OrcidProfilePlugin\handleAuthorFormExecute
handleAuthorFormExecute($hookname, $args)
Definition: OrcidProfilePlugin.inc.php:431
OrcidProfilePlugin\isMemberApiEnabled
isMemberApiEnabled($contextId)
Definition: OrcidProfilePlugin.inc.php:1275
OrcidProfilePlugin\handlePublicationStatusChange
handlePublicationStatusChange($hookName, $args)
Definition: OrcidProfilePlugin.inc.php:732
Plugin\getCategory
getCategory()
Definition: Plugin.inc.php:322
OrcidProfilePlugin\isGloballyConfigured
isGloballyConfigured()
Definition: OrcidProfilePlugin.inc.php:187
OrcidProfilePlugin\smartyPluginUrl
smartyPluginUrl($params, $smarty)
Definition: OrcidProfilePlugin.inc.php:547
OrcidProfileSettingsForm
Form for site admins to modify ORCID Profile plugin settings.
Definition: OrcidProfileSettingsForm.inc.php:20
DAORegistry\getDAO
static & getDAO($name, $dbconn=null)
Definition: DAORegistry.inc.php:57
OrcidProfilePlugin\handleTemplateDisplay
handleTemplateDisplay($hookName, $args)
Definition: OrcidProfilePlugin.inc.php:241
OrcidProfilePlugin\USER_GROUP_TO_ORCID_ROLE
const USER_GROUP_TO_ORCID_ROLE
Definition: OrcidProfilePlugin.inc.php:37
OrcidProfilePlugin\buildOAuthUrl
buildOAuthUrl($handlerMethod, $redirectParams)
Definition: OrcidProfilePlugin.inc.php:313
OrcidProfilePlugin\handleSubmissionSubmitStep3FormExecute
handleSubmissionSubmitStep3FormExecute($hookName, $params)
Definition: OrcidProfilePlugin.inc.php:472
OrcidProfilePlugin\manage
manage($args, $request)
Definition: OrcidProfilePlugin.inc.php:600
OrcidProfilePlugin\logInfo
logInfo($message)
Definition: OrcidProfilePlugin.inc.php:1243
OrcidProfilePlugin\getInstallEmailTemplatesFile
getInstallEmailTemplatesFile()
Definition: OrcidProfilePlugin.inc.php:540
OrcidProfilePlugin\sendSubmissionToOrcid
sendSubmissionToOrcid($publication, $request)
Definition: OrcidProfilePlugin.inc.php:795
OrcidProfilePlugin\logFilePath
static logFilePath()
Definition: OrcidProfilePlugin.inc.php:1223
OrcidProfilePlugin\getSetting
getSetting($contextId, $name)
Definition: OrcidProfilePlugin.inc.php:165
PluginRegistry\loadCategory
static loadCategory($category, $enabledOnly=false, $mainContextId=null)
Definition: PluginRegistry.inc.php:103
OrcidProfilePlugin\setCurrentContextId
setCurrentContextId($contextId)
Definition: OrcidProfilePlugin.inc.php:1268
MailTemplate
Subclass of Mail for mailing a template email.
Definition: MailTemplate.inc.php:21
LazyLoadPlugin\getCurrentContextId
getCurrentContextId()
Definition: LazyLoadPlugin.inc.php:101
Plugin\getEnabled
getEnabled()
Definition: Plugin.inc.php:868
OrcidProfilePlugin
ORCID Profile plugin class.
Definition: OrcidProfilePlugin.inc.php:35
JSONMessage
Class to represent a JSON (Javascript Object Notation) message.
Definition: JSONMessage.inc.php:18
Config\getVar
static getVar($section, $key, $default=null)
Definition: Config.inc.php:35
AjaxModal
A modal that retrieves its content from via AJAX.
Definition: AjaxModal.inc.php:18
LinkAction
Base class defining an action that can be performed by the user in the user interface.
Definition: LinkAction.inc.php:22
OrcidProfilePlugin\registrationFilter
registrationFilter($output, $templateMgr)
Definition: OrcidProfilePlugin.inc.php:346
OrcidProfilePlugin\getOauthPath
getOauthPath()
Definition: OrcidProfilePlugin.inc.php:285
OrcidProfilePlugin\setupCallbackHandler
setupCallbackHandler($hookName, $params)
Definition: OrcidProfilePlugin.inc.php:147
OrcidProfilePlugin\buildOrcidWork
buildOrcidWork($publication, $context, $authors, $request, $issue=null)
Definition: OrcidProfilePlugin.inc.php:985
LazyLoadPlugin\getName
getName()
Definition: LazyLoadPlugin.inc.php:40
PKPTemplateManager\getManager
static & getManager($request=null)
Definition: PKPTemplateManager.inc.php:1239
OrcidProfilePlugin\getOrcidUrl
getOrcidUrl()
Definition: OrcidProfilePlugin.inc.php:294
OrcidProfilePlugin\handleUserPublicProfileDisplay
handleUserPublicProfileDisplay($hookName, $params)
Definition: OrcidProfilePlugin.inc.php:379
OrcidProfilePlugin\getActions
getActions($request, $actionArgs)
Definition: OrcidProfilePlugin.inc.php:567
Plugin\getTemplateResource
getTemplateResource($template=null, $inCore=false)
Definition: Plugin.inc.php:349
Plugin\getPluginPath
getPluginPath()
Definition: Plugin.inc.php:330
OrcidProfilePlugin\logError
logError($message)
Definition: OrcidProfilePlugin.inc.php:1233
Plugin\$request
$request
Definition: Plugin.inc.php:68
PluginRegistry\getPlugin
static getPlugin($category, $name)
Definition: PluginRegistry.inc.php:85
OrcidProfilePlugin\getStyleSheet
getStyleSheet()
Definition: OrcidProfilePlugin.inc.php:640
HookRegistry\register
static register($hookName, $callback, $hookSequence=HOOK_SEQUENCE_NORMAL)
Definition: HookRegistry.inc.php:70
Core\getBaseDir
static getBaseDir()
Definition: Core.inc.php:37
OrcidProfilePlugin\removeOrcidAccessToken
removeOrcidAccessToken($author, $saveAuthor=true)
Definition: OrcidProfilePlugin.inc.php:1207
PKPApplication\get
static get()
Definition: PKPApplication.inc.php:235
OrcidProfilePlugin\getMailTemplate
getMailTemplate($emailKey, $context=null)
Definition: OrcidProfilePlugin.inc.php:662
OrcidProfilePlugin\getIcon
getIcon()
Definition: OrcidProfilePlugin.inc.php:649
OrcidProfilePlugin\handleFormDisplay
handleFormDisplay($hookName, $args)
Definition: OrcidProfilePlugin.inc.php:206
OrcidProfilePlugin\getHandlerPath
getHandlerPath()
Definition: OrcidProfilePlugin.inc.php:138
OrcidProfilePlugin\handleEditorAction
handleEditorAction($hookName, $args)
Definition: OrcidProfilePlugin.inc.php:758
OrcidProfilePlugin\collectUserOrcidId
collectUserOrcidId($hookName, $params)
Definition: OrcidProfilePlugin.inc.php:456
OrcidProfilePlugin\handleAdditionalFieldNames
handleAdditionalFieldNames($hookName, $params)
Definition: OrcidProfilePlugin.inc.php:511
GenericPlugin
Abstract class for generic plugins.
Definition: GenericPlugin.inc.php:18
PKPServices\get
static get($service)
Definition: PKPServices.inc.php:49
OrcidProfilePlugin\getDescription
getDescription()
Definition: OrcidProfilePlugin.inc.php:533