<?php

/**
 * @file OrcidProfilePlugin.inc.php
 *
 * Copyright (c) 2015-2019 University of Pittsburgh
 * Copyright (c) 2014-2020 Simon Fraser University
 * Copyright (c) 2003-2020 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
 */

import('lib.pkp.classes.plugins.GenericPlugin');

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', 'v2.1/');
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');

class OrcidProfilePlugin extends GenericPlugin {
	const PUBID_TO_ORCID_EXT_ID = ["doi" => "doi", "other::urn" => "urn"];
	const USER_GROUP_TO_ORCID_ROLE = ["Author" => "AUTHOR", "Translator" => "CHAIR_OR_TRANSLATOR","Journal manager"=>"AUTHOR"];

	private $submissionIdToBePublished;
	private $currentContextId;

	/**
	 * @copydoc Plugin::register()
	 */
	function register($category, $path, $mainContextId = null) {
		$success = parent::register($category, $path, $mainContextId);
		if (!Config::getVar('general', 'installed') || defined('RUNNING_UPGRADE')) return true;
		if ($success && $this->getEnabled($mainContextId)) {
			$contextId = ($mainContextId === null) ? $this->getCurrentContextId() : $mainContextId;

			HookRegistry::register('ArticleHandler::view', array(&$this, 'submissionView'));
			HookRegistry::register('PreprintHandler::view', array(&$this, 'submissionView'));

			// Insert the OrcidHandler to handle ORCID redirects
			HookRegistry::register('LoadHandler', array($this, 'setupCallbackHandler'));

			// Register callback for Smarty filters; add CSS
			HookRegistry::register('TemplateManager::display', array($this, 'handleTemplateDisplay'));

			// Add "Connect ORCID" button to PublicProfileForm
			HookRegistry::register('User::PublicProfile::AdditionalItems', array($this, 'handleUserPublicProfileDisplay'));

			// Display additional ORCID access information and checkbox to send e-mail to authors in the AuthorForm
			HookRegistry::register('authorform::display', array($this, 'handleFormDisplay'));

			// Send email to author, if the added checkbox was ticked
			HookRegistry::register('authorform::execute', array($this, 'handleAuthorFormExecute'));

			// Handle ORCID on user registration
			HookRegistry::register('registrationform::execute', array($this, 'collectUserOrcidId'));

			// Send emails to authors without ORCID id upon submission
			HookRegistry::register('submissionsubmitstep3form::execute', array($this, 'handleSubmissionSubmitStep3FormExecute'));

			HookRegistry::register('authordao::getAdditionalFieldNames', array($this, 'handleAdditionalFieldNames'));

			// Add more ORCiD fields to UserDAO
			HookRegistry::register('userdao::getAdditionalFieldNames', array($this, 'handleAdditionalFieldNames'));

			// Send emails to authors without authorised ORCID access on promoting a submission to copy editing. Not included in OPS.
			if ($this->getSetting($contextId, 'sendMailToAuthorsOnPublication')) {
				HookRegistry::register('EditorAction::recordDecision', array($this, 'handleEditorAction'));
			}

			HookRegistry::register('Publication::publish', array($this, 'handlePublicationStatusChange'));

			// Add more ORCiD fields to author Schema
			HookRegistry::register('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']
				];
			});
		}

		return $success;
	}

	/**
	 * Get page handler path for this plugin.
	 * @return string Path to plugin's page handler
	 */
	function getHandlerPath() {
		return $this->getPluginPath() . DIRECTORY_SEPARATOR . 'pages';
	}

	/**
	 * Hook callback: register pages for each sushi-lite method
	 * This URL is of the form: orcidapi/{$orcidrequest}
	 * @see PKPPageRouter::route()
	 */
	function setupCallbackHandler($hookName, $params) {
		$page = $params[0];
		if ($this->getEnabled() && $page == 'orcidapi') {
			$this->import('pages/OrcidHandler');
			define('HANDLER_CLASS', 'OrcidHandler');
			return true;
		}
		return false;
	}

	/**
	 * Load a setting for a specific journal or load it from the config.inc.php if it is specified there.
	 *
	 * @param  $contextId int The id of the journal from which the plugin settings should be loaded.
	 * @param  $name string   Name of the setting.
	 * @return mixed          The setting value, either from the database for this context
	 *                        or from the global configuration file.
	 */
	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;
			default:
				return parent::getSetting($contextId, $name);
		}

		return $config_value ?: parent::getSetting($contextId, $name);
	}

	/**
	 * 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
	 */
	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 $hookName string
	 * @param $args Form[]
	 *
	 * @return bool
	 * @see Form::display()
	 *
	 */
	function handleFormDisplay($hookName, $args) {
		$request = PKPApplication::get()->getRequest();
		$templateMgr = TemplateManager::getManager($request);
		switch ($hookName) {
			case 'authorform::display':
				$authorForm =& $args[0];
				$author = $authorForm->getAuthor();
				if ($author) {
					$authenticated = !empty($author->getData('orcidAccessToken'));
					$templateMgr->assign(
						array(
							'orcidAccessToken' => $author->getData('orcidAccessToken'),
							'orcidAccessScope' => $author->getData('orcidAccessScope'),
							'orcidAccessExpiresOn' => $author->getData('orcidAccessExpiresOn'),
							'orcidAccessDenied' => $author->getData('orcidAccessDenied'),
							'orcidAuthenticated' => $authenticated
						)
					);
				}

				$templateMgr->registerFilter("output", array($this, 'authorFormFilter'));
				break;
		}
		return false;
	}

	/**
	 * Hook callback: register output filter for user registration and article display.
	 *
	 * @param $hookName string
	 * @param $args array
	 * @return bool
	 * @see TemplateManager::display()
	 *
	 */
	function handleTemplateDisplay($hookName, $args) {
		$templateMgr =& $args[0];
		$template =& $args[1];
		$request = PKPApplication::get()->getRequest();

		// Assign our private stylesheet, for front and back ends.
		$templateMgr->addStyleSheet(
			'orcidProfile',
			$request->getBaseUrl() . '/' . $this->getStyleSheet(),
			array(
				'contexts' => array('frontend', 'backend')
			)
		);

		switch ($template) {
			case 'frontend/pages/userRegister.tpl':
				$templateMgr->registerFilter("output", array($this, 'registrationFilter'));
				break;
		}
		return false;
	}

	/**
	 * Return the OAUTH path (prod or sandbox) based on the current API configuration
	 *
	 * @return string
	 */
	function getOauthPath() {
		return $this->getOrcidUrl() . 'oauth/';
	}

	/**
	 * Return the ORCID website url (prod or sandbox) based on the current API configuration
	 *
	 * @return string
	 */
	function getOrcidUrl() {
		$request = Application::get()->getRequest();
		$context = $request->getContext();
		$contextId = ($context == null) ? 0 : $context->getId();

		$apiPath = $this->getSetting($contextId, 'orcidProfileAPIPath');
		if ($apiPath == ORCID_API_URL_PUBLIC || $apiPath == ORCID_API_URL_MEMBER) {
			return ORCID_URL;
		} else {
			return ORCID_URL_SANDBOX;
		}
	}

	/**
	 * Return an ORCID OAuth authorization link with
	 *
	 * @param  $handlerMethod string containting a valid method of the OrcidHandler
	 * @param  $redirectParams Array associative array with additional request parameters for the redirect URL
	 */
	function buildOAuthUrl($handlerMethod, $redirectParams) {
		$request = PKPApplication::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, ROUTE_PAGE, null, 'orcidapi',
			$handlerMethod, null, $redirectParams);

		return $this->getOauthPath() . 'authorize?' . http_build_query(
				array(
					'client_id' => $this->getSetting($contextId, 'orcidClientId'),
					'response_type' => 'code',
					'scope' => $scope,
					'redirect_uri' => $redirectUrl)
			);
	}

	/**
	 * Output filter adds ORCiD interaction to registration form.
	 *
	 * @param $output string
	 * @param $templateMgr TemplateManager
	 * @return string
	 */
	function registrationFilter($output, $templateMgr) {
		if (preg_match('/<form[^>]+id="register"[^>]+>/', $output, $matches, PREG_OFFSET_CAPTURE)) {
			$match = $matches[0][0];
			$offset = $matches[0][1];
			$request = Application::get()->getRequest();
			$context = $request->getContext();
			$contextId = ($context == null) ? 0 : $context->getId();
			$targetOp = 'register';
			$templateMgr->assign(array(
				'targetOp' => $targetOp,
				'orcidUrl' => $this->getOrcidUrl(),
				'orcidOAuthUrl' => $this->buildOAuthUrl('orcidAuthorize', array('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', array($this, 'registrationFilter'));
		}
		return $output;
	}

	/**
	 * Renders additional content for the PublicProfileForm.
	 *
	 * Called by @param $output string
	 * @param $templateMgr TemplateManager
	 * @return bool
	 * @see lib/pkp/templates/user/publicProfileForm.tpl
	 *
	 */
	function handleUserPublicProfileDisplay($hookName, $params) {
		$templateMgr =& $params[1];
		$output =& $params[2];
		$request = Application::get()->getRequest();
		$context = $request->getContext();
		$user = $request->getUser();
		$contextId = ($context == null) ? 0 : $context->getId();
		$targetOp = 'profile';
		$templateMgr->assign(
			array(
				'targetOp' => $targetOp,
				'orcidUrl' => $this->getOrcidUrl(),
				'orcidOAuthUrl' => $this->buildOAuthUrl('orcidAuthorize', array('targetOp' => $targetOp)),
				'orcidClientId' => $this->getSetting($contextId, 'orcidClientId'),
				'orcidIcon' => $this->getIcon(),
				'orcidAuthenticated' => !empty($user->getData('orcidAccessToken')),
			)
		);

		$output = $templateMgr->fetch($this->getTemplateResource('orcidProfile.tpl'));
		return true;
	}

	/**
	 * 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', array($this, 'authorFormFilter'));
		}
		return $output;
	}

	/**
	 * handleAuthorFormexecute sends an e-mail to the author if a specific checkbox was ticked in the author form.
	 *
	 * @param $hookname string
	 * @param $args AuthorForm[]
	 * @see AuthorForm::execute() The function calling the hook.
	 *
	 */
	function handleAuthorFormExecute($hookname, $args) {
		$form =& $args[0];
		$form->readUserVars(array('requestOrcidAuthorization', 'deleteOrcid'));

		$requestAuthorization = $form->getData('requestOrcidAuthorization');
		$deleteOrcid = $form->getData('deleteOrcid');
		$author = $form->getAuthor();

		if ($author && $requestAuthorization) {
			$this->sendAuthorMail($author);
		}

		if ($author && $deleteOrcid) {
			$author->setOrcid(null);
			$this->removeOrcidAccessToken($author, false);
		}
	}

	/**
	 * Collect the ORCID when registering a user.
	 *
	 * @param $hookName string
	 * @param $params array
	 * @return bool
	 */
	function collectUserOrcidId($hookName, $params) {
		$form = $params[0];
		$user = $form->user;

		$form->readUserVars(array('orcid'));
		$user->setOrcid($form->getData('orcid'));
		return false;
	}

	/**
	 * Output filter adds ORCiD interaction to the 3rd step submission form.
	 *
	 * @param $output string
	 * @param $templateMgr TemplateManager
	 * @return bool
	 */
	function handleSubmissionSubmitStep3FormExecute($hookName, $params) {
		$form = $params[0];
		// Have to use global Request access because request is not passed to hook
		$publicationDao = DAORegistry::getDAO('PublicationDAO');
		/* @var $publicationDao PublicationDAO */
		$publication = $publicationDao->getById($form->submission->getData('currentPublicationId'));
		$authors = $publication->getData('authors');

		$request = Application::get()->getRequest();
		$user = $request->getUser();
		//error_log("OrcidProfilePlugin: authors[0] = " . var_export($authors[0], true));
		//error_log("OrcidProfilePlugin: user = " . var_export($user, true));
		if ($authors[0]->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'));
			$authors[0]->setData('orcidAccessToken', $user->getData('orcidAccessToken'));
			$authors[0]->setData('orcidAccessScope', $user->getData('orcidAccessScope'));
			$authors[0]->setData('orcidRefreshToken', $user->getData('orcidRefreshToken'));
			$authors[0]->setData('orcidAccessExpiresOn', $user->getData('orcidAccessExpiresOn'));
			$authors[0]->setData('orcidSandbox', $user->getData('orcidSandbox'));

			$authorDao = DAORegistry::getDAO('AuthorDAO');
			/* @var $authorDao AuthorDAO */
			$authorDao->updateObject($authors[0]);

			//error_log("OrcidProfilePlugin: author = " . var_export($authors[0], true));
		}
		return false;
	}

	/**
	 * Add additional ORCID specific fields to the Author and User objects
	 *
	 * @param $hookName string
	 * @param $params array
	 *
	 * @return bool
	 */
	function handleAdditionalFieldNames($hookName, $params) {
		$fields =& $params[1];
		$fields[] = 'orcidSandbox';
		$fields[] = 'orcidAccessToken';
		$fields[] = 'orcidAccessScope';
		$fields[] = 'orcidRefreshToken';
		$fields[] = 'orcidAccessExpiresOn';
		$fields[] = 'orcidAccessDenied';

		return false;
	}

	/**
	 * @copydoc Plugin::getDisplayName()
	 */
	function getDisplayName() {
		return __('plugins.generic.orcidProfile.displayName');
	}

	/**
	 * @copydoc Plugin::getDescription()
	 */
	function getDescription() {
		return __('plugins.generic.orcidProfile.description');
	}

	/**
	 * @see PKPPlugin::getInstallEmailTemplatesFile()
	 */
	function getInstallEmailTemplatesFile() {
		return ($this->getPluginPath() . '/emailTemplates.xml');
	}

	/**
	 * Extend the {url ...} smarty to support this plugin.
	 */
	function smartyPluginUrl($params, $smarty) {
		$path = array($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, array($params['path']));
		} else {
			$params['path'] = $path;
		}

		if (!empty($params['id'])) {
			$params['path'] = array_merge($params['path'], array($params['id']));
			unset($params['id']);
		}
		return $smarty->smartyUrl($params, $smarty);
	}

	function submissionView($hookName, $args) {
		$request = $args[0];
		$templateMgr = TemplateManager::getManager($request);
		$templateMgr->assign(array("orcidIcon" => $this->getIcon()));
	}

	/**
	 * @see Plugin::getActions()
	 */
	function getActions($request, $actionArgs) {
		$router = $request->getRouter();
		import('lib.pkp.classes.linkAction.request.AjaxModal');
		return array_merge(
			$this->getEnabled() ? array(
				new LinkAction(
					'settings',
					new AjaxModal(
						$router->url(
							$request,
							null,
							null,
							'manage',
							null,
							array(
								'verb' => 'settings',
								'plugin' => $this->getName(),
								'category' => 'generic'
							)
						),
						$this->getDisplayName()
					),
					__('manager.plugins.settings'),
					null
				),
			) : array(),
			parent::getActions($request, $actionArgs)
		);
	}

	/**
	 * @see Plugin::manage()
	 */
	function manage($args, $request) {
		switch ($request->getUserVar('verb')) {
			case 'settings':
				$context = $request->getContext();
				$contextId = ($context == null) ? 0 : $context->getId();

				$templateMgr = TemplateManager::getManager();
				$templateMgr->registerPlugin('function', 'plugin_url', array($this, 'smartyPluginUrl'));
				$apiOptions = [
					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'
				];
				$templateMgr->assign('orcidApiUrls', $apiOptions);
				$templateMgr->assign('logLevelOptions', [
					'ERROR' => 'plugins.generic.orcidProfile.manager.settings.logLevel.error',
					'ALL' => 'plugins.generic.orcidProfile.manager.settings.logLevel.all'
				]);
				$this->import('OrcidProfileSettingsForm');
				$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));
		}
		return parent::manage($args, $request);
	}

	/**
	 * Return the location of the plugin's CSS file
	 *
	 * @return string
	 */
	function getStyleSheet() {
		return $this->getPluginPath() . '/css/orcidProfile.css';
	}

	/**
	 * Return a string of the ORCiD SVG icon
	 *
	 * @return string
	 */
	function getIcon() {
		$path = Core::getBaseDir() . '/' . $this->getPluginPath() . '/templates/images/orcid.svg';
		return file_exists($path) ? file_get_contents($path) : '';
	}

	/**
	 * Instantiate a MailTemplate
	 *
	 * @param string $emailKey
	 * @param Context $context
	 *
	 * @return MailTemplate
	 */
	function getMailTemplate($emailKey, $context = null) {
		import('lib.pkp.classes.mail.MailTemplate');
		return new MailTemplate($emailKey, null, $context, 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 = PKPApplication::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)) {
			$mailTemplate = 'ORCID_REQUEST_AUTHOR_AUTHORIZATION';
		} else {
			$mailTemplate = 'ORCID_COLLECT_AUTHOR_ID';
		}

		$mail = $this->getMailTemplate($mailTemplate, $context);
		$emailToken = md5(microtime() . $author->getEmail());

		$author->setData('orcidEmailToken', $emailToken);

		$publicationDao = DAORegistry::getDAO('PublicationDAO');
		/** @var PublicationDAO $publicationDao */
		$publication = $publicationDao->getById($author->getData('publicationId'));

		$oauthUrl = $this->buildOAuthUrl('orcidVerify', array('token' => $emailToken, 'publicationId' => $publication->getId()));
		$aboutUrl = $request->getDispatcher()->url($request, ROUTE_PAGE, null, 'orcidapi', 'about', null);

		// Set From to primary journal contact
		$mail->setFrom($context->getData('contactEmail'), $context->getData('contactName'));

		// Send to author
		$mail->setRecipients(array(array('name' => $author->getFullName(), 'email' => $author->getEmail())));

		// Send the mail with parameters
		$mail->sendWithParams(array(
			'orcidAboutUrl' => $aboutUrl,
			'authorOrcidUrl' => $oauthUrl,
			'authorName' => $author->getFullName(),
			'articleTitle' => $publication->getLocalizedTitle(), // Backwards compatibility only
			'submissionTitle' => $publication->getLocalizedTitle(),
		));

		if ($updateAuthor) {
			$authorDao = DAORegistry::getDAO('AuthorDAO');
			$authorDao->updateObject($author);
		}
	}

	/**
	 * 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 $hookName string
	 * @param $args Issue[] Issue object that will be published
	 *
	 **@see
	 *
	 */
	public function handlePublicationStatusChange($hookName, $args) {
		$newPublication =& $args[0];
		/** @var $newPublication Publication */
		$publication =& $args[1];
		/** @var $publication Publication */
		$submission =& $args[2];
		/** @var $submission Submission */

		$request = PKPApplication::get()->getRequest();

		switch ($newPublication->getData('status')) {
			case STATUS_PUBLISHED:
				$this->sendSubmissionToOrcid($newPublication, $request);
				break;
			case STATUS_SCHEDULED:
				$this->sendSubmissionToOrcid($newPublication, $request);
				break;
		}
	}

	/**
	 * handleEditorAction handles promoting a submission to copyediting.
	 *
	 * @param $hookName string Name the hook was registered with
	 * @param $args array 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'] == SUBMISSION_EDITOR_DECISION_ACCEPT) {
			$publication = $submission->getCurrentPublication();

			if (isset($publication)) {
				$authorDao = DAORegistry::getDAO('AuthorDAO');
				/** @var AuthorDAO $authorDao */

				$authors = $authorDao->getByPublicationId($submission->getCurrentPublication()->getId());

				foreach ($authors as $author) {
					$orcidAccessExpiresOn = Carbon\Carbon::parse($author->getData('orcidAccessExpiresOn'));
					if ($author->getData('orcidAccessToken') == null || $orcidAccessExpiresOn->isPast()) {
						$this->sendAuthorMail($author, true);
					}
				}
			}

		}
	}

	/**
	 * 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 for which the data will be sent to ORCID
	 * @param $submission Submission for which the data will be sent to ORCID
	 * @return bool||bool[]
	 *
	 **/
	public function sendSubmissionToOrcid($publication, $request) {
		$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 = Services::get('issue')->get($issueId);
		}

		$authorDao = DAORegistry::getDAO('AuthorDAO');
		/** @var $authorDao AuthorDAO */
		$authors = $authorDao->getByPublicationId($publicationId);

		// Collect valid ORCID ids and access tokens from submission contributors
		$authorsWithOrcid = [];
		foreach ($authors as $author) {
			if ($author->getOrcid() && $author->getData('orcidAccessToken')) {
				$orcidAccessExpiresOn = Carbon\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, $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']);
			}

			$orcidWorkJson = json_encode($orcidWork);

			$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,
					]
				);
			} catch (\GuzzleHttp\Exception\ClientException $exception) {
				$reason = $exception->getResponse()->getBody(false);
				$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);
					$authorDao->updateObject($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);
						$authorDao->updateObject($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;
			}
		}
		if (array_product($requestsSuccess)) {
			return true;
		} else {
			return $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 Submission $submission the submission we want to extract data from.
	 * @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 = Services::get('submission')->get($publication->getData('submissionId'));

		$applicationName = Application::get()->getName();
		$bibtexCitation= '';

		$publicationLocale = ($publication->getData('locale')) ? $publication->getData('locale') : 'en_US';
		$supportedSubmissionLocales = $context->getSupportedSubmissionLocales();

		$publicationUrl = $request->getDispatcher()->url($request, ROUTE_PAGE, null, 'article', 'view', $submission->getId());

		$orcidWork = [
			'title' => [
				'title' => [
					'value' => $publication->getLocalizedData('title', $publicationLocale) ?? ''
				],
				'subtitle' => [
					'value' => $publication->getLocalizedData('subtitle', $publicationLocale) ?? ''
				]
			],
			'journal-title' => [
				'value' => $context->getName($publicationLocale) ?? ''
			],
			'short-description' => trim(strip_tags($publication->getLocalizedData('abstract', $publicationLocale))) ?? '',
			'type' => 'JOURNAL_ARTICLE',
			'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
			];

		}


		$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;
	}


	/**
	 * Parse issue year and publication date and use the older on of the two as
	 * the publication date of the ORCID work.
	 *
	 * @return array Associative array with year, month and day or only year
	 */
	private function buildOrcidPublicationDate($publication, $issue = null) {
		$publicationPublishDate = Carbon\Carbon::parse($publication->getData('datePublished'));

		return [
			'year' => ['value' => $publicationPublishDate->format("Y")],
			'month' => ['value' => $publicationPublishDate->format("m")],
			'day' => ['value' => $publicationPublishDate->format("d")]
		];
	}

	/**
	 * 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 = array();
		$pubIdPlugins = PluginRegistry::loadCategory('pubIds', true, $contextId);
		// Add doi, urn, etc. for article
		$articleHasStoredPubId = false;
		if (is_array($pubIdPlugins)) {
			foreach ($pubIdPlugins as $plugin) {
				if (!$plugin->getEnabled()) {
					continue;
				}

				$pubIdType = $plugin->getPubIdType();

				# Add article ids
				$pubId = $publication->getData($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'
					];
				}
			}
		} 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;
	}

	/**
	 * Build associative array fitting for ORCID contributor mentions in an
	 * ORCID work from the supplied Authors array.
	 *
	 * @param  $authors Author[] Array of Author objects
	 * @param  $contextId int    Id of the context the Author objects belong to
	 * @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_US')];

			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;
	}

	/**
	 * 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
	 * @return void
	 */
	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) {
			$authorDao = DAORegistry::getDAO('AuthorDAO');
			$authorDao->updateObject($author);
		}
	}

	/**
	 * @return string Path to a custom ORCID log file.
	 */
	public static function logFilePath() {
		return Config::getVar('files', 'files_dir') . '/orcid.log';
	}

	/**
	 * Write error message to log.
	 *
	 * @param  $message string Message to write
	 * @return void
	 */
	public function logError($message) {
		self::writeLog($message, 'ERROR');
	}

	/**
	 * Write info message to log.
	 *
	 * @param  $message string Message to write
	 * @return void
	 */
	public function logInfo($message) {
		if ($this->getSetting($this->currentContextId, 'logLevel') === 'ERROR') {
			return;
		} else {
			self::writeLog($message, 'INFO');
		}
	}

	/**
	 * Write a message with specified level to log
	 *
	 * @param  $message string Message to write
	 * @param  $level   string Error level to add to message
	 * @return void
	 */
	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());
	}

	/**
	 * Set the current id of the context (atm only considered for logging settings).
	 *
	 * @param $contextId int the Id of the currently active context (journal)
	 */
	public function setCurrentContextId($contextId) {
		$this->currentContextId = $contextId;
	}

	/**
	 * @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;
		}
	}
}

