<?php

/**
 * @file classes/core/PKPApplication.inc.php
 *
 * Copyright (c) 2014-2021 Simon Fraser University
 * Copyright (c) 2000-2021 John Willinsky
 * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
 *
 * @class PKPApplication
 * @ingroup core
 *
 * @brief Class describing this application.
 *
 */

define('PHP_REQUIRED_VERSION', '7.3.0');

define('ROUTE_COMPONENT', 'component');
define('ROUTE_PAGE', 'page');
define('ROUTE_API', 'api');

define('API_VERSION', 'v1');

define('CONTEXT_SITE', 0);
define('CONTEXT_ID_NONE', 0);
define('CONTEXT_ID_ALL', '_');
define('REVIEW_ROUND_NONE', 0);

define('ASSOC_TYPE_PRODUCTION_ASSIGNMENT',	0x0000202);
define('ASSOC_TYPE_SUBMISSION_FILE',		0x0000203);
define('ASSOC_TYPE_REVIEW_RESPONSE',		0x0000204);
define('ASSOC_TYPE_REVIEW_ASSIGNMENT',		0x0000205);
define('ASSOC_TYPE_SUBMISSION_EMAIL_LOG_ENTRY',	0x0000206);
define('ASSOC_TYPE_WORKFLOW_STAGE',		0x0000207);
define('ASSOC_TYPE_NOTE',			0x0000208);
define('ASSOC_TYPE_REPRESENTATION',		0x0000209);
define('ASSOC_TYPE_ANNOUNCEMENT',		0x000020A);
define('ASSOC_TYPE_REVIEW_ROUND',		0x000020B);
define('ASSOC_TYPE_SUBMISSION_FILES',		0x000020F);
define('ASSOC_TYPE_PLUGIN',			0x0000211);
define('ASSOC_TYPE_SECTION',			0x0000212);
define('ASSOC_TYPE_CATEGORY',			0x000020D);
define('ASSOC_TYPE_USER',			0x0001000); // This value used because of bug #6068
define('ASSOC_TYPE_USER_GROUP',			0x0100002);
define('ASSOC_TYPE_CITATION',			0x0100003);
define('ASSOC_TYPE_AUTHOR',			0x0100004);
define('ASSOC_TYPE_EDITOR',			0x0100005);
define('ASSOC_TYPE_USER_ROLES',			0x0100007);
define('ASSOC_TYPE_ACCESSIBLE_WORKFLOW_STAGES',	0x0100008);
define('ASSOC_TYPE_SUBMISSION',			0x0100009);
define('ASSOC_TYPE_QUERY',			0x010000a);
define('ASSOC_TYPE_QUEUED_PAYMENT',		0x010000b);
define('ASSOC_TYPE_PUBLICATION', 0x010000c);
define('ASSOC_TYPE_ACCESSIBLE_FILE_STAGES',    0x010000d);

// Constant used in UsageStats for submission files that are not full texts
define('ASSOC_TYPE_SUBMISSION_FILE_COUNTER_OTHER', 0x0000213);

// FIXME: these were defined in userGroup. they need to be moved somewhere with classes that do mapping.
define('WORKFLOW_STAGE_PATH_SUBMISSION', 'submission');
define('WORKFLOW_STAGE_PATH_INTERNAL_REVIEW', 'internalReview');
define('WORKFLOW_STAGE_PATH_EXTERNAL_REVIEW', 'externalReview');
define('WORKFLOW_STAGE_PATH_EDITING', 'editorial');
define('WORKFLOW_STAGE_PATH_PRODUCTION', 'production');

// Constant used to distinguish between editorial and author workflows
define('WORKFLOW_TYPE_EDITORIAL', 'editorial');
define('WORKFLOW_TYPE_AUTHOR', 'author');

use Illuminate\Database\Capsule\Manager as Capsule;

interface iPKPApplicationInfoProvider {
	/**
	 * Get the top-level context DAO.
	 */
	public static function getContextDAO();

	/**
	 * Get the section DAO.
	 * @return DAO
	 */
	public static function getSectionDAO();

	/**
	 * Get the representation DAO.
	 */
	public static function getRepresentationDAO();

	/**
	 * Get a SubmissionSearchIndex instance.
	 */
	public static function getSubmissionSearchIndex();

	/**
	 * Get a SubmissionSearchDAO instance.
	 */
	public static function getSubmissionSearchDAO();

	/**
	 * Get the stages used by the application.
	 */
	public static function getApplicationStages();

	/**
	 * Get the file directory array map used by the application.
	 * should return array('context' => ..., 'submission' => ...)
	 */
	public static function getFileDirectories();

	/**
	 * Returns the context type for this application.
	 */
	public static function getContextAssocType();
}

abstract class PKPApplication implements iPKPApplicationInfoProvider {
	var $enabledProducts = array();
	var $allProducts;

	/**
	 * Constructor
	 */
	public function __construct() {
		import('lib.pkp.classes.core.Core');
		import('lib.pkp.classes.core.PKPString');
		import('lib.pkp.classes.core.Registry');

		import('lib.pkp.classes.config.Config');

		// Load Composer autoloader
		require_once('lib/pkp/lib/vendor/autoload.php');

		ini_set('display_errors', Config::getVar('debug', 'display_errors', ini_get('display_errors')));
		if (!defined('SESSION_DISABLE_INIT') && !Config::getVar('general', 'installed')) {
			define('SESSION_DISABLE_INIT', true);
		}

		Registry::set('application', $this);

		import('lib.pkp.classes.db.DAORegistry');
		import('lib.pkp.classes.db.XMLDAO');

		import('lib.pkp.classes.cache.CacheManager');

		import('lib.pkp.classes.security.RoleDAO');
		import('lib.pkp.classes.security.Validation');
		import('lib.pkp.classes.session.SessionManager');
		import('classes.template.TemplateManager');
		import('classes.notification.NotificationManager');
		import('lib.pkp.classes.statistics.PKPStatisticsHelper');

		import('lib.pkp.classes.plugins.PluginRegistry');
		import('lib.pkp.classes.plugins.HookRegistry');

		import('classes.i18n.AppLocale');

		PKPString::init();

		$microTime = Core::microtime();
		Registry::set('system.debug.startTime', $microTime);

		$notes = array();
		Registry::set('system.debug.notes', $notes);

		if (Config::getVar('general', 'installed')) $this->initializeDatabaseConnection();

		// Register custom autoloader functions for namespaces
		spl_autoload_register(function($class) {
			$prefix = 'PKP\\';
			$rootPath = BASE_SYS_DIR . "/lib/pkp/classes";
			customAutoload($rootPath, $prefix, $class);
		});
		spl_autoload_register(function($class) {
			$prefix = 'APP\\';
			$rootPath = BASE_SYS_DIR . "/classes";
			customAutoload($rootPath, $prefix, $class);
		});
	}

	/**
	 * Initialize the database connection (and related systems)
	 */
	public function initializeDatabaseConnection() {
		// Ensure multiple calls to this function don't cause trouble
		static $databaseConnectionInitialized = false;
		if ($databaseConnectionInitialized) return;

		$databaseConnectionInitialized = true;
		$laravelContainer = new Illuminate\Container\Container();
		Registry::set('laravelContainer', $laravelContainer);
		(new Illuminate\Bus\BusServiceProvider($laravelContainer))->register();
		(new Illuminate\Events\EventServiceProvider($laravelContainer))->register();
		$eventDispatcher = new \Illuminate\Events\Dispatcher($laravelContainer);
		$laravelContainer->instance('Illuminate\Contracts\Events\Dispatcher', $eventDispatcher);
		$laravelContainer->instance('Illuminate\Contracts\Container\Container', $laravelContainer);

		// Map valid config options to Illuminate database drivers
		$driver = strtolower(Config::getVar('database', 'driver'));
		if (substr($driver, 0, 8) === 'postgres') {
			$driver = 'pgsql';
		} else {
			$driver = 'mysql';
		}

		$capsule = new Capsule;
		$capsule->addConnection([
			'driver'    => $driver,
			'host'      => Config::getVar('database', 'host'),
			'database'  => Config::getVar('database', 'name'),
			'username'  => Config::getVar('database', 'username'),
			'port'      => Config::getVar('database', 'port'),
			'unix_socket'=> Config::getVar('database', 'unix_socket'),
			'password'  => Config::getVar('database', 'password'),
			'charset'   => Config::getVar('i18n', 'connection_charset', 'utf8'),
			'collation' => Config::getVar('database', 'collation', 'utf8_general_ci'),
		]);
		$capsule->setEventDispatcher($eventDispatcher);
		$capsule->setAsGlobal();
		if (Config::getVar('database', 'debug')) Capsule::listen(function($query) {
			error_log("Database query\n$query->sql\n" . json_encode($query->bindings));//\n Bindings: " . print_r($query->bindings, true));
		});


		// Set up Laravel queue handling
		$laravelContainer->bind('exception.handler', function () {
			return new class implements Illuminate\Contracts\Debug\ExceptionHandler {
				public function shouldReport(Throwable $e) {
					return true;
				}

				public function report(Throwable $e) {
					error_log((string) $e);
				}

				public function render($request, Throwable $e) {
					return null;
				}

				public function renderForConsole($output, Throwable $e) {
					echo (string) $e;
				}
			};
		});

		$queue = new Illuminate\Queue\Capsule\Manager($laravelContainer);

		// Synchronous (immediate) queue
		$queue->addConnection(['driver' => 'sync']);

		// Persistent queue
		$connection = Capsule::schema()->getConnection();
		$resolver = new \Illuminate\Database\ConnectionResolver(['default' => $connection]);
		$manager = $queue->getQueueManager();
		$laravelContainer['queue'] = $queue->getQueueManager();
		$manager->addConnector('database', function () use ($resolver) {
			return new Illuminate\Queue\Connectors\DatabaseConnector($resolver);
		});
		$queue->addConnection([
			'driver' => 'database',
			'table' => 'jobs',
			'connection' => 'default',
			'queue' => 'default',
		], 'persistent');

		$queue->setAsGlobal();
	}

	/**
	 * @copydoc PKPApplication::get()
	 * @deprecated Use PKPApplication::get() instead.
	 */
	public static function getApplication() {
		return self::get();
	}

	/**
	 * Get the current application object
	 * @return Application
	 */
	public static function get() {
		return Registry::get('application');
	}

	/**
	 * Return a HTTP client implementation.
	 * @return \GuzzleHttp\Client
	 */
	public function getHttpClient() {
		$application = Application::get();
		$userAgent = $application->getName() . '/';
		if (Config::getVar('general', 'installed') && !defined('RUNNING_UPGRADE')) {
			$versionDao = DAORegistry::getDAO('VersionDAO');
			$currentVersion = $versionDao->getCurrentVersion();
			$userAgent .= $currentVersion->getVersionString();
		} else {
			$userAgent .= '?';
		}

		return new \GuzzleHttp\Client([
			'proxy' => [
				'http' => Config::getVar('proxy', 'http_proxy', null),
				'https' => Config::getVar('proxy', 'https_proxy', null),
			],
			'headers' => [
				'User-Agent' => $userAgent,
			],
			'allow_redirects' => ['strict' => true],
		]);
	}

	/**
	 * Get the request implementation singleton
	 * @return Request
	 */
	public function getRequest() {
		$request =& Registry::get('request', true, null); // Ref req'd

		if (is_null($request)) {
			import('classes.core.Request');

			// Implicitly set request by ref in the registry
			$request = new Request();
		}

		return $request;
	}

	/**
	 * Get the dispatcher implementation singleton
	 * @return Dispatcher
	 */
	public function getDispatcher() {
		$dispatcher =& Registry::get('dispatcher', true, null); // Ref req'd
		if (is_null($dispatcher)) {
			import('lib.pkp.classes.core.Dispatcher');

			// Implicitly set dispatcher by ref in the registry
			$dispatcher = new Dispatcher();

			// Inject dependency
			$dispatcher->setApplication(PKPApplication::get());

			// Inject router configuration
			$dispatcher->addRouterName('lib.pkp.classes.core.APIRouter', ROUTE_API);
			$dispatcher->addRouterName('lib.pkp.classes.core.PKPComponentRouter', ROUTE_COMPONENT);
			$dispatcher->addRouterName('classes.core.PageRouter', ROUTE_PAGE);
		}

		return $dispatcher;
	}

	/**
	 * This executes the application by delegating the
	 * request to the dispatcher.
	 */
	public function execute() {
		// Dispatch the request to the correct handler
		$dispatcher = $this->getDispatcher();
		$dispatcher->dispatch($this->getRequest());
	}

	/**
	 * Get the symbolic name of this application
	 * @return string
	 */
	public static function getName() {
		return 'pkp-lib';
	}

	/**
	 * Get the locale key for the name of this application.
	 * @return string
	 */
	abstract public function getNameKey();

	/**
	 * Get the "context depth" of this application, i.e. the number of
	 * parts of the URL after index.php that represent the context of
	 * the current request (e.g. Journal [1], or Conference and
	 * Scheduled Conference [2]).
	 * @return int
	 */
	abstract public function getContextDepth();

	/**
	 * Get the list of the contexts available for this application
	 * i.e. the various parameters that are needed to represent the
	 * (e.g. array('journal') or array('conference', 'schedConf'))
	 * @return Array
	 */
	abstract public function getContextList();

	/**
	 * Get the URL to the XML descriptor for the current version of this
	 * application.
	 * @return string
	 */
	abstract public function getVersionDescriptorUrl();

	/**
	 * This function retrieves all enabled product versions once
	 * from the database and caches the result for further
	 * access.
	 *
	 * @param $category string
	 * @param $mainContextId integer Optional ID of the top-level context
	 * (e.g. Journal, Conference, Press) to query for enabled products
	 * @return array
	 */
	public function &getEnabledProducts($category = null, $mainContextId = null) {
		$contextDepth = $this->getContextDepth();
		if (is_null($mainContextId)) {
			$request = $this->getRequest();
			$router = $request->getRouter();

			// Try to identify the main context (e.g. journal, conference, press),
			// will be null if none found.
			$mainContext = $router->getContext($request, 1);
			if ($mainContext) $mainContextId = $mainContext->getId();
			else $mainContextId = CONTEXT_SITE;
		}
		if (!isset($this->enabledProducts[$mainContextId])) {
			$settingContext = array();
			if ($contextDepth > 0) {
				// Create the context for the setting if found
				$settingContext[] = $mainContextId;
				$settingContext = array_pad($settingContext, $contextDepth, 0);
				$settingContext = array_combine($this->getContextList(), $settingContext);
			}

			$versionDao = DAORegistry::getDAO('VersionDAO'); /* @var $versionDao VersionDAO */
			$this->enabledProducts[$mainContextId] = $versionDao->getCurrentProducts($settingContext);
		}

		if (is_null($category)) {
			return $this->enabledProducts[$mainContextId];
		} elseif (isset($this->enabledProducts[$mainContextId][$category])) {
			return $this->enabledProducts[$mainContextId][$category];
		} else {
			$returner = array();
			return $returner;
		}
	}

	/**
	 * Get the list of plugin categories for this application.
	 * @return array
	 */
	abstract public function getPluginCategories();

	/**
	 * Return the current version of the application.
	 * @return Version
	 */
	public function &getCurrentVersion() {
		$currentVersion =& $this->getEnabledProducts('core');
		assert(count($currentVersion)) == 1;
		return $currentVersion[$this->getName()];
	}

	/**
	 * Get the map of DAOName => full.class.Path for this application.
	 * @return array
	 */
	public function getDAOMap() {
		return array(
			'AccessKeyDAO' => 'lib.pkp.classes.security.AccessKeyDAO',
			'AnnouncementDAO' => 'lib.pkp.classes.announcement.AnnouncementDAO',
			'AnnouncementTypeDAO' => 'lib.pkp.classes.announcement.AnnouncementTypeDAO',
			'AuthSourceDAO' => 'lib.pkp.classes.security.AuthSourceDAO',
			'CategoryDAO' => 'lib.pkp.classes.context.CategoryDAO',
			'CitationDAO' => 'lib.pkp.classes.citation.CitationDAO',
			'ControlledVocabDAO' => 'lib.pkp.classes.controlledVocab.ControlledVocabDAO',
			'ControlledVocabEntryDAO' => 'lib.pkp.classes.controlledVocab.ControlledVocabEntryDAO',
			'ControlledVocabEntrySettingsDAO' => 'lib.pkp.classes.controlledVocab.ControlledVocabEntrySettingsDAO',
			'CountryDAO' => 'lib.pkp.classes.i18n.CountryDAO', // DEPRECATED
			'CurrencyDAO' => 'lib.pkp.classes.currency.CurrencyDAO', // DEPRECATED
			'DataObjectTombstoneDAO' => 'lib.pkp.classes.tombstone.DataObjectTombstoneDAO',
			'DataObjectTombstoneSettingsDAO' => 'lib.pkp.classes.tombstone.DataObjectTombstoneSettingsDAO',
			'EditDecisionDAO' => 'lib.pkp.classes.submission.EditDecisionDAO',
			'EmailTemplateDAO' => 'lib.pkp.classes.mail.EmailTemplateDAO',
			'FilterDAO' => 'lib.pkp.classes.filter.FilterDAO',
			'FilterGroupDAO' => 'lib.pkp.classes.filter.FilterGroupDAO',
			'GenreDAO' => 'lib.pkp.classes.submission.GenreDAO',
			'InterestDAO' => 'lib.pkp.classes.user.InterestDAO',
			'InterestEntryDAO' => 'lib.pkp.classes.user.InterestEntryDAO',
			'LanguageDAO' => 'lib.pkp.classes.language.LanguageDAO', // DEPRECATED
			'LibraryFileDAO' => 'lib.pkp.classes.context.LibraryFileDAO',
			'NavigationMenuDAO' => 'lib.pkp.classes.navigationMenu.NavigationMenuDAO',
			'NavigationMenuItemDAO' => 'lib.pkp.classes.navigationMenu.NavigationMenuItemDAO',
			'NavigationMenuItemAssignmentDAO' => 'lib.pkp.classes.navigationMenu.NavigationMenuItemAssignmentDAO',
			'NoteDAO' => 'lib.pkp.classes.note.NoteDAO',
			'NotificationDAO' => 'lib.pkp.classes.notification.NotificationDAO',
			'NotificationSettingsDAO' => 'lib.pkp.classes.notification.NotificationSettingsDAO',
			'NotificationSubscriptionSettingsDAO' => 'lib.pkp.classes.notification.NotificationSubscriptionSettingsDAO',
			'PluginGalleryDAO' => 'lib.pkp.classes.plugins.PluginGalleryDAO',
			'PluginSettingsDAO' => 'lib.pkp.classes.plugins.PluginSettingsDAO',
			'PublicationDAO' => 'classes.publication.PublicationDAO',
			'QueuedPaymentDAO' => 'lib.pkp.classes.payment.QueuedPaymentDAO',
			'ReviewAssignmentDAO' => 'lib.pkp.classes.submission.reviewAssignment.ReviewAssignmentDAO',
			'ReviewFilesDAO' => 'lib.pkp.classes.submission.ReviewFilesDAO',
			'ReviewFormDAO' => 'lib.pkp.classes.reviewForm.ReviewFormDAO',
			'ReviewFormElementDAO' => 'lib.pkp.classes.reviewForm.ReviewFormElementDAO',
			'ReviewFormResponseDAO' => 'lib.pkp.classes.reviewForm.ReviewFormResponseDAO',
			'ReviewRoundDAO' => 'lib.pkp.classes.submission.reviewRound.ReviewRoundDAO',
			'RoleDAO' => 'lib.pkp.classes.security.RoleDAO',
			'ScheduledTaskDAO' => 'lib.pkp.classes.scheduledTask.ScheduledTaskDAO',
			'SessionDAO' => 'lib.pkp.classes.session.SessionDAO',
			'SiteDAO' => 'lib.pkp.classes.site.SiteDAO',
			'StageAssignmentDAO' => 'lib.pkp.classes.stageAssignment.StageAssignmentDAO',
			'SubEditorsDAO' => 'lib.pkp.classes.context.SubEditorsDAO',
			'SubmissionAgencyDAO' => 'lib.pkp.classes.submission.SubmissionAgencyDAO',
			'SubmissionAgencyEntryDAO' => 'lib.pkp.classes.submission.SubmissionAgencyEntryDAO',
			'SubmissionCommentDAO' => 'lib.pkp.classes.submission.SubmissionCommentDAO',
			'SubmissionDisciplineDAO' => 'lib.pkp.classes.submission.SubmissionDisciplineDAO',
			'SubmissionDisciplineEntryDAO' => 'lib.pkp.classes.submission.SubmissionDisciplineEntryDAO',
			'SubmissionEmailLogDAO' => 'lib.pkp.classes.log.SubmissionEmailLogDAO',
			'SubmissionEventLogDAO' => 'lib.pkp.classes.log.SubmissionEventLogDAO',
			'SubmissionFileDAO' => 'classes.submission.SubmissionFileDAO',
			'SubmissionFileEventLogDAO' => 'lib.pkp.classes.log.SubmissionFileEventLogDAO',
			'QueryDAO' => 'lib.pkp.classes.query.QueryDAO',
			'SubmissionLanguageDAO' => 'lib.pkp.classes.submission.SubmissionLanguageDAO',
			'SubmissionLanguageEntryDAO' => 'lib.pkp.classes.submission.SubmissionLanguageEntryDAO',
			'SubmissionKeywordDAO' => 'lib.pkp.classes.submission.SubmissionKeywordDAO',
			'SubmissionKeywordEntryDAO' => 'lib.pkp.classes.submission.SubmissionKeywordEntryDAO',
			'SubmissionSubjectDAO' => 'lib.pkp.classes.submission.SubmissionSubjectDAO',
			'SubmissionSubjectEntryDAO' => 'lib.pkp.classes.submission.SubmissionSubjectEntryDAO',
			'TimeZoneDAO' => 'lib.pkp.classes.i18n.TimeZoneDAO',
			'TemporaryFileDAO' => 'lib.pkp.classes.file.TemporaryFileDAO',
			'UserGroupAssignmentDAO' => 'lib.pkp.classes.security.UserGroupAssignmentDAO',
			'UserDAO' => 'lib.pkp.classes.user.UserDAO',
			'UserGroupDAO' => 'lib.pkp.classes.security.UserGroupDAO',
			'UserSettingsDAO' => 'lib.pkp.classes.user.UserSettingsDAO',
			'UserStageAssignmentDAO' => 'lib.pkp.classes.user.UserStageAssignmentDAO',
			'VersionDAO' => 'lib.pkp.classes.site.VersionDAO',
			'ViewsDAO' => 'lib.pkp.classes.views.ViewsDAO',
			'WorkflowStageDAO' => 'lib.pkp.classes.workflow.WorkflowStageDAO',
			'XMLDAO' => 'lib.pkp.classes.db.XMLDAO',
		);
	}

	/**
	 * Return the fully-qualified (e.g. page.name.ClassNameDAO) name of the
	 * given DAO.
	 * @param $name string
	 * @return string
	 */
	public function getQualifiedDAOName($name) {
		$map =& Registry::get('daoMap', true, $this->getDAOMap()); // Ref req'd
		if (isset($map[$name])) return $map[$name];
		return null;
	}


	//
	// Statistics API
	//
	/**
	 * Return all metric types supported by this application.
	 *
	 * @return array An array of strings of supported metric type identifiers.
	 */
	public function getMetricTypes($withDisplayNames = false) {
		// Retrieve site-level report plugins.
		$reportPlugins = PluginRegistry::loadCategory('reports', true, CONTEXT_SITE);
		if (empty($reportPlugins)) return array();

		// Run through all report plugins and retrieve all supported metrics.
		$metricTypes = array();
		foreach ($reportPlugins as $reportPlugin) {
			/* @var $reportPlugin ReportPlugin */
			$pluginMetricTypes = $reportPlugin->getMetricTypes();
			if ($withDisplayNames) {
				foreach ($pluginMetricTypes as $metricType) {
					$metricTypes[$metricType] = $reportPlugin->getMetricDisplayType($metricType);
				}
			} else {
				$metricTypes = array_merge($metricTypes, $pluginMetricTypes);
			}
		}

		return $metricTypes;
	}

	/**
	 * Returns the currently configured default metric type for this site.
	 * If no specific metric type has been set for this site then null will
	 * be returned.
	 *
	 * @return null|string A metric type identifier or null if no default metric
	 *   type could be identified.
	 */
	public function getDefaultMetricType() {
		$request = $this->getRequest();
		$site = $request->getSite();
		if (!is_a($site, 'Site')) return null;
		$defaultMetricType = $site->getData('defaultMetricType');

		// Check whether the selected metric type is valid.
		$availableMetrics = $this->getMetricTypes();
		if (empty($defaultMetricType)) {
			// If there is only a single available metric then use it.
			if (count($availableMetrics) === 1) {
				$defaultMetricType = $availableMetrics[0];
			} else {
				return null;
			}
		} else {
			if (!in_array($defaultMetricType, $availableMetrics)) return null;
		}
		return $defaultMetricType;
	}

	/**
	 * Main entry point for PKP statistics reports.
	 *
	 * @see <https://pkp.sfu.ca/wiki/index.php/OJSdeStatisticsConcept#Input_and_Output_Formats_.28Aggregation.2C_Filters.2C_Metrics_Data.29>
	 * for a full specification of the input and output format of this method.
	 *
	 * @param $metricType null|string|array metrics selection
	 *   NB: If you want to use the default metric on journal level then you must
	 *   set $metricType = null and add an explicit filter on a single journal ID.
	 *   Otherwise the default site-level metric will be used.
	 * @param $columns string|array column (aggregation level) selection
	 * @param $filters array report-level filter selection
	 * @param $orderBy array order criteria
	 * @param $range null|DBResultRange paging specification
	 *
	 * @return null|array The selected data as a simple tabular result set or
	 *   null if the given parameter combination is not supported.
	 */
	public function getMetrics($metricType = null, $columns = array(), $filter = array(), $orderBy = array(), $range = null) {
		import('classes.statistics.StatisticsHelper');
		$statsHelper = new StatisticsHelper();

		// Check the parameter format.
		if (!(is_array($filter) && is_array($orderBy))) return null;

		// Check whether which context we are.
		$context = $statsHelper->getContext($filter);

		// Identify and canonicalize filtered metric types.
		$defaultSiteMetricType = $this->getDefaultMetricType();
		$siteMetricTypes = $this->getMetricTypes();
		$metricType = $statsHelper->canonicalizeMetricTypes($metricType, $context, $defaultSiteMetricType, $siteMetricTypes);
		if (!is_array($metricType)) return null;
		$metricTypeCount = count($metricType);

		// Canonicalize columns.
		if (is_scalar($columns)) $columns = array($columns);

		// The metric type dimension is not additive. This imposes two important
		// restrictions on valid report descriptions:
		// 1) We need at least one metric Type to be specified.
		if ($metricTypeCount === 0) return null;
		// 2) If we have multiple metrics then we have to force inclusion of
		// the metric type column to avoid aggregation over several metric types.
		if ($metricTypeCount > 1) {
			if (!in_array(STATISTICS_DIMENSION_METRIC_TYPE, $columns)) {
				array_push($columns, STATISTICS_DIMENSION_METRIC_TYPE);
			}
		}

		// Retrieve report plugins.
		if (is_a($context, 'Context')) {
			$contextId = $context->getId();
		} else {
			$contextId = CONTEXT_SITE;
		}
		$reportPlugins = PluginRegistry::loadCategory('reports', true, $contextId);
		if (empty($reportPlugins)) return null;

		// Run through all report plugins and try to retrieve the requested metrics.
		$report = array();
		foreach ($reportPlugins as $reportPlugin) {
			// Check whether one (or more) of the selected metrics can be
			// provided by this plugin.
			$availableMetrics = $reportPlugin->getMetricTypes();
			$availableMetrics = array_intersect($availableMetrics, $metricType);
			if (count($availableMetrics) == 0) continue;

			// Retrieve a (partial) report.
			$partialReport = $reportPlugin->getMetrics($availableMetrics, $columns, $filter, $orderBy, $range);

			// Merge the partial report with the main report.
			$report = array_merge($report, (array) $partialReport);

			// Remove the found metric types from the metric type array.
			$metricType = array_diff($metricType, $availableMetrics);
		}

		// Check whether we found all requested metric types.
		if (count($metricType) > 0) return null;

		// Return the report.
		return $report;
	}

	/**
	 * Return metric in the primary metric type
	 * for the passed associated object.
	 * @param $assocType int
	 * @param $assocId int
	 * @return int
	 */
	public function getPrimaryMetricByAssoc($assocType, $assocId) {
		$filter = array(
			STATISTICS_DIMENSION_ASSOC_ID => $assocId,
			STATISTICS_DIMENSION_ASSOC_TYPE => $assocType);

		$request = $this->getRequest();
		$router = $request->getRouter();
		$context = $router->getContext($request);
		if ($context) {
			$filter[STATISTICS_DIMENSION_CONTEXT_ID] = $context->getId();
		}

		$metric = $this->getMetrics(null, array(), $filter);
		if (is_array($metric)) {
			if (!is_null($metric[0][STATISTICS_METRIC])) return $metric[0][STATISTICS_METRIC];
		}

		return 0;
	}

	/**
	 * Get a mapping of license URL to license locale key for common
	 * creative commons licenses.
	 * @return array
	 */
	public static function getCCLicenseOptions() {
		return array(
			'https://creativecommons.org/licenses/by-nc-nd/4.0' => 'submission.license.cc.by-nc-nd4',
			'https://creativecommons.org/licenses/by-nc/4.0' => 'submission.license.cc.by-nc4',
			'https://creativecommons.org/licenses/by-nc-sa/4.0' => 'submission.license.cc.by-nc-sa4',
			'https://creativecommons.org/licenses/by-nd/4.0' => 'submission.license.cc.by-nd4',
			'https://creativecommons.org/licenses/by/4.0' => 'submission.license.cc.by4',
			'https://creativecommons.org/licenses/by-sa/4.0' => 'submission.license.cc.by-sa4'
		);
	}

	/**
	 * Get the Creative Commons license badge associated with a given
	 * license URL.
	 * @param $ccLicenseURL URL to creative commons license
	 * @param $locale string Optional locale to return badge in
	 * @return string HTML code for CC license
	 */
	public function getCCLicenseBadge($ccLicenseURL, $locale = null) {
		$licenseKeyMap = array(
			'|http[s]?://(www\.)?creativecommons.org/licenses/by-nc-nd/4.0[/]?|' => 'submission.license.cc.by-nc-nd4.footer',
			'|http[s]?://(www\.)?creativecommons.org/licenses/by-nc/4.0[/]?|' => 'submission.license.cc.by-nc4.footer',
			'|http[s]?://(www\.)?creativecommons.org/licenses/by-nc-sa/4.0[/]?|' => 'submission.license.cc.by-nc-sa4.footer',
			'|http[s]?://(www\.)?creativecommons.org/licenses/by-nd/4.0[/]?|' => 'submission.license.cc.by-nd4.footer',
			'|http[s]?://(www\.)?creativecommons.org/licenses/by/4.0[/]?|' => 'submission.license.cc.by4.footer',
			'|http[s]?://(www\.)?creativecommons.org/licenses/by-sa/4.0[/]?|' => 'submission.license.cc.by-sa4.footer',
			'|http[s]?://(www\.)?creativecommons.org/licenses/by-nc-nd/3.0[/]?|' => 'submission.license.cc.by-nc-nd3.footer',
			'|http[s]?://(www\.)?creativecommons.org/licenses/by-nc/3.0[/]?|' => 'submission.license.cc.by-nc3.footer',
			'|http[s]?://(www\.)?creativecommons.org/licenses/by-nc-sa/3.0[/]?|' => 'submission.license.cc.by-nc-sa3.footer',
			'|http[s]?://(www\.)?creativecommons.org/licenses/by-nd/3.0[/]?|' => 'submission.license.cc.by-nd3.footer',
			'|http[s]?://(www\.)?creativecommons.org/licenses/by/3.0[/]?|' => 'submission.license.cc.by3.footer',
			'|http[s]?://(www\.)?creativecommons.org/licenses/by-sa/3.0[/]?|' => 'submission.license.cc.by-sa3.footer'
		);
		if ($locale === null) $locale = AppLocale::getLocale();

		foreach($licenseKeyMap as $pattern => $key) {
			if (preg_match($pattern, $ccLicenseURL ?? '')) {
				PKPLocale::requireComponents(LOCALE_COMPONENT_PKP_SUBMISSION, $locale);
				return __($key, array(), $locale);
			}
		}
		return null;
	}

	/**
	 * Get a mapping of role keys and i18n key names.
	 * @param boolean $contextOnly If false, also returns site-level roles (Site admin)
	 * @param array|null $roleIds Only return role names of these IDs
	 * @return array
	 */
	public static function getRoleNames($contextOnly = false, $roleIds = null) {
		$siteRoleNames = array(ROLE_ID_SITE_ADMIN => 'user.role.siteAdmin');
		$appRoleNames = array(
			ROLE_ID_MANAGER => 'user.role.manager',
			ROLE_ID_SUB_EDITOR => 'user.role.subEditor',
			ROLE_ID_ASSISTANT => 'user.role.assistant',
			ROLE_ID_AUTHOR => 'user.role.author',
			ROLE_ID_REVIEWER => 'user.role.reviewer',
			ROLE_ID_READER => 'user.role.reader',
		);
		$roleNames = $contextOnly ? $appRoleNames : $siteRoleNames + $appRoleNames;
		if (!empty($roleIds)) $roleNames = array_intersect_key($roleNames, array_flip($roleIds));

		return $roleNames;
	}

	/**
	 * Get a mapping of roles allowed to access particular workflows
	 * @return array
	 */
	public static function getWorkflowTypeRoles() {
		return array(
			WORKFLOW_TYPE_EDITORIAL => array(ROLE_ID_SITE_ADMIN, ROLE_ID_MANAGER, ROLE_ID_SUB_EDITOR, ROLE_ID_ASSISTANT),
			WORKFLOW_TYPE_AUTHOR => array(ROLE_ID_SITE_ADMIN, ROLE_ID_MANAGER, ROLE_ID_SUB_EDITOR, ROLE_ID_AUTHOR),
		);
	}

	/**
	 * Get the name of a workflow stage
	 *
	 * @param int $stageId One of the WORKFLOW_STAGE_* constants
	 * @return string
	 */
	public static function getWorkflowStageName($stageId) {
		AppLocale::requireComponents(LOCALE_COMPONENT_PKP_SUBMISSION, LOCALE_COMPONENT_APP_SUBMISSION);
		switch ($stageId) {
			case WORKFLOW_STAGE_ID_SUBMISSION: return 'submission.submission';
			case WORKFLOW_STAGE_ID_INTERNAL_REVIEW: return 'workflow.review.internalReview';
			case WORKFLOW_STAGE_ID_EXTERNAL_REVIEW: return 'workflow.review.externalReview';
			case WORKFLOW_STAGE_ID_EDITING: return 'submission.editorial';
			case WORKFLOW_STAGE_ID_PRODUCTION: return 'submission.production';
		}
		throw new Exception('Name requested for an unrecognized stage id.');
	}

	/**
	 * Get the hex color (#000000) of a workflow stage
	 *
	 * @param int $stageId One of the WORKFLOW_STAGE_* constants
	 * @return string
	 */
	public static function getWorkflowStageColor($stageId) {
		AppLocale::requireComponents(LOCALE_COMPONENT_PKP_SUBMISSION, LOCALE_COMPONENT_APP_SUBMISSION);
		switch ($stageId) {
			case WORKFLOW_STAGE_ID_SUBMISSION: return '#d00a0a';
			case WORKFLOW_STAGE_ID_INTERNAL_REVIEW: return '#e05c14';
			case WORKFLOW_STAGE_ID_EXTERNAL_REVIEW: return '#e08914';
			case WORKFLOW_STAGE_ID_EDITING: return '#006798';
			case WORKFLOW_STAGE_ID_PRODUCTION: return '#00b28d';
		}
		throw new Exception('Color requested for an unrecognized stage id.');
	}

	/**
	 * Get a human-readable version of the max file upload size
	 *
	 * @return string
	 */
	public static function getReadableMaxFileSize() {
		return strtoupper(UPLOAD_MAX_FILESIZE) . 'B';
	}

	/**
	 * Convert the max upload size to an integer in MBs
	 *
	 * @return int
	 */
	public static function getIntMaxFileMBs() {
		$size = (int) UPLOAD_MAX_FILESIZE;
		$unit = strtolower(substr(UPLOAD_MAX_FILESIZE, -1));
		// No letter suffix fallbacks to "byte"
		switch (ctype_alpha($unit) ? $unit : 'b') {
			case 'g':
				$size <<= 10;
				break;
			case 'm':
				break;
			case 'k':
				$size >>= 10;
				break;
			case 'b':
				$size >>= 20;
				break;
			default:
				error_log(sprintf('Invalid value for the PHP configuration upload_max_filesize: "%s"', UPLOAD_MAX_FILESIZE));
				break;
		}
		return floor($size);
	}

	/**
	 * Get the supported metadata setting names for this application
	 *
	 * @return array
	 */
	public static function getMetadataFields() {
		return [
			'coverage',
			'languages',
			'rights',
			'source',
			'subjects',
			'type',
			'disciplines',
			'keywords',
			'agencies',
			'citations',
		];
	}
}

define('REALLY_BIG_NUMBER', 10000);
define('UPLOAD_MAX_FILESIZE', trim(ini_get('upload_max_filesize')));

define('WORKFLOW_STAGE_ID_PUBLISHED', 0); // FIXME? See bug #6463.
define('WORKFLOW_STAGE_ID_SUBMISSION', 1);
define('WORKFLOW_STAGE_ID_INTERNAL_REVIEW', 2);
define('WORKFLOW_STAGE_ID_EXTERNAL_REVIEW', 3);
define('WORKFLOW_STAGE_ID_EDITING', 4);
define('WORKFLOW_STAGE_ID_PRODUCTION', 5);

/* TextArea insert tag variable types used to change their display when selected */
define('INSERT_TAG_VARIABLE_TYPE_PLAIN_TEXT', 'PLAIN_TEXT');
define('INSERT_TAG_VARIABLE_TYPE_SAFE_HTML', 'SAFE_HTML');

// To expose LISTBUILDER_SOURCE_TYPE_... constants via JS
import('lib.pkp.classes.controllers.listbuilder.ListbuilderHandler');

// To expose ORDER_CATEGORY_GRID_... constants via JS
import('lib.pkp.classes.controllers.grid.feature.OrderCategoryGridItemsFeature');
