<?php

/**
 * @file classes/core/PKPApplication.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.
 *
 */

namespace PKP\core;

use APP\core\Application;
use APP\core\Request;
use DateTime;
use DateTimeZone;
use Exception;
use GuzzleHttp\Client;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Database\MySqlConnection;
use Illuminate\Support\Facades\DB;
use PKP\config\Config;
use PKP\db\DAORegistry;
use PKP\facades\Locale;
use PKP\security\Role;
use PKP\session\SessionManager;
use PKP\site\VersionDAO;
use PKP\submission\RepresentationDAOInterface;

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

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

    /**
     * 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
{
    public const PHP_REQUIRED_VERSION = '8.0.2';

    // Constant used to distinguish between editorial and author workflows
    public const WORKFLOW_TYPE_EDITORIAL = 'editorial';
    public const WORKFLOW_TYPE_AUTHOR = 'author';

    public const API_VERSION = 'v1';

    public const ROUTE_COMPONENT = 'component';
    public const ROUTE_PAGE = 'page';
    public const ROUTE_API = 'api';

    public const CONTEXT_SITE = 0;
    public const CONTEXT_ID_NONE = 0;
    public const CONTEXT_ID_ALL = '_';
    public const REVIEW_ROUND_NONE = 0;

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

    // Constant used in UsageStats for submission files that are not full texts
    public const ASSOC_TYPE_SUBMISSION_FILE_COUNTER_OTHER = 0x0000213;

    public $enabledProducts = [];
    public $allProducts;

    /**
     * Constructor
     */
    public function __construct()
    {
        if (!defined('PKP_STRICT_MODE')) {
            define('PKP_STRICT_MODE', (bool) Config::getVar('general', 'strict'));
            class_alias('\PKP\config\Config', '\Config');
            class_alias('\PKP\core\Registry', '\Registry');
            class_alias('\PKP\core\Core', '\Core');
            class_alias('\PKP\cache\CacheManager', '\CacheManager');
            class_alias('\PKP\handler\PKPHandler', '\PKPHandler');
            class_alias('\PKP\payment\QueuedPayment', '\QueuedPayment'); // QueuedPayment instances may be serialized
        }

        // If not in strict mode, globally expose constants on this class.
        if (!PKP_STRICT_MODE) {
            foreach ([
                'WORKFLOW_TYPE_EDITORIAL', 'WORKFLOW_TYPE_AUTHOR', 'PHP_REQUIRED_VERSION',
                'API_VERSION',
                'ROUTE_COMPONENT', 'ROUTE_PAGE', 'ROUTE_API',
                'CONTEXT_SITE', 'CONTEXT_ID_NONE', 'CONTEXT_ID_ALL', 'REVIEW_ROUND_NONE',

                'ASSOC_TYPE_PRODUCTION_ASSIGNMENT',
                'ASSOC_TYPE_SUBMISSION_FILE',
                'ASSOC_TYPE_REVIEW_RESPONSE',
                'ASSOC_TYPE_REVIEW_ASSIGNMENT',
                'ASSOC_TYPE_SUBMISSION_EMAIL_LOG_ENTRY',
                'ASSOC_TYPE_WORKFLOW_STAGE',
                'ASSOC_TYPE_NOTE',
                'ASSOC_TYPE_REPRESENTATION',
                'ASSOC_TYPE_ANNOUNCEMENT',
                'ASSOC_TYPE_REVIEW_ROUND',
                'ASSOC_TYPE_SUBMISSION_FILES',
                'ASSOC_TYPE_PLUGIN',
                'ASSOC_TYPE_SECTION',
                'ASSOC_TYPE_CATEGORY',
                'ASSOC_TYPE_USER',
                'ASSOC_TYPE_USER_GROUP',
                'ASSOC_TYPE_CITATION',
                'ASSOC_TYPE_AUTHOR',
                'ASSOC_TYPE_EDITOR',
                'ASSOC_TYPE_USER_ROLES',
                'ASSOC_TYPE_ACCESSIBLE_WORKFLOW_STAGES',
                'ASSOC_TYPE_SUBMISSION',
                'ASSOC_TYPE_QUERY',
                'ASSOC_TYPE_QUEUED_PAYMENT',
                'ASSOC_TYPE_PUBLICATION',
                'ASSOC_TYPE_ACCESSIBLE_FILE_STAGES',
                'ASSOC_TYPE_SUBMISSION_FILE_COUNTER_OTHER',
            ] as $constantName) {
                if (!defined($constantName)) {
                    define($constantName, constant('self::' . $constantName));
                }
            }
            if (!class_exists('\PKPApplication')) {
                class_alias('\PKP\core\PKPApplication', '\PKPApplication');
            }
        }

        ini_set('display_errors', Config::getVar('debug', 'display_errors', ini_get('display_errors')));
        if (!static::isInstalled()) {
            SessionManager::disable();
        }

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

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

        $this->initializeLaravelContainer();
        PKPString::initialize();

        // Load default locale files
        Locale::registerPath(BASE_SYS_DIR . '/lib/pkp/locale');

        if (static::isInstalled() && !static::isUpgrading()) {
            $versionDao = DAORegistry::getDAO('VersionDAO'); /** @var VersionDAO $versionDao */
            $appVersion = $versionDao->getCurrentVersion()->getVersionString();
            Registry::set('appVersion', $appVersion);
        }
    }

    /**
     * Initialize Laravel container and register service providers
     */
    public function initializeLaravelContainer(): void
    {
        // Ensure multiple calls to this function don't cause trouble
        static $containerInitialized = false;
        if ($containerInitialized) {
            return;
        }

        $containerInitialized = true;

        // Initialize Laravel's container and set it globally
        $laravelContainer = new PKPContainer();
        $laravelContainer->registerConfiguredProviders();

        $this->initializeTimeZone();

        if (Config::getVar('database', 'debug')) {
            DB::listen(fn (QueryExecuted $query) => error_log("Database query\n{$query->sql}\n" . json_encode($query->bindings)));
        }
    }

    /**
     * Setup the internal time zone for the database and PHP.
     */
    protected function initializeTimeZone(): void
    {
        $timeZone = null;
        // Loads the time zone from the configuration file
        if ($setting = Config::getVar('general', 'time_zone')) {
            try {
                $timeZone = (new DateTimeZone($setting))->getName();
            } catch (Exception $e) {
                $setting = strtolower($setting);
                foreach (DateTimeZone::listIdentifiers() as $identifier) {
                    // Backward compatibility identification
                    if ($setting == strtolower(preg_replace(['/^\w+\//', '/_/'], ['', ' '], $identifier))) {
                        $timeZone = $identifier;
                        break;
                    }
                }
            }
        }
        // Set the default timezone
        date_default_timezone_set($timeZone ?: ini_get('date.timezone') ?: 'UTC');

        // Synchronize the database time zone
        if (Application::isInstalled()) {
            // Retrieve the current offset
            $offset = (new DateTime())->format('P');
            $statement = DB::connection() instanceof MySqlConnection
                ? "SET time_zone = '{$offset}'"
                : "SET TIME ZONE INTERVAL '{$offset}' HOUR TO MINUTE";
            DB::statement($statement);
        }
    }

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

    /**
     * Get the unique site ID
     */
    public function getUUID(): string
    {
        $site = $this->getRequest()->getSite();
        $uniqueSiteId = $site->getUniqueSiteID();
        if (!strlen((string) $uniqueSiteId)) {
            $uniqueSiteId = PKPString::generateUUID();
            $site->setUniqueSiteID($uniqueSiteId);
            /** @var SiteDAO */
            $siteDao = DAORegistry::getDAO('SiteDAO');
            $siteDao->updateObject($site);
        }
        return $uniqueSiteId;
    }

    /**
     * Return a HTTP client implementation.
     *
     * @return Client
     */
    public function getHttpClient()
    {
        if (PKPContainer::getInstance()->runningUnitTests()) {
            $client = Registry::get(\PKP\tests\PKPTestCase::MOCKED_GUZZLE_CLIENT_NAME);
            if ($client) {
                return $client;
            }
        }

        $application = Application::get();
        $userAgent = $application->getName() . '/';
        if (static::isInstalled() && !static::isUpgrading()) {
            /** @var \PKP\site\VersionDAO */
            $versionDao = DAORegistry::getDAO('VersionDAO');
            $currentVersion = $versionDao->getCurrentVersion();
            $userAgent .= $currentVersion->getVersionString();
        } else {
            $userAgent .= '?';
        }

        return new 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)) {
            // 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)) {
            // Implicitly set dispatcher by ref in the registry
            $dispatcher = new Dispatcher();

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

            // Inject router configuration
            $dispatcher->addRouterName('\PKP\core\APIRouter', self::ROUTE_API);
            $dispatcher->addRouterName('\PKP\core\PKPComponentRouter', self::ROUTE_COMPONENT);
            $dispatcher->addRouterName('\APP\core\PageRouter', self::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 name of the context for this application
     */
    abstract public function getContextName(): string;

    /**
     * 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 string $category
     * @param int $mainContextId 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)
    {
        if (is_null($mainContextId)) {
            $request = $this->getRequest();
            $router = $request->getRouter();

            $mainContextId = $router->getContext($request)?->getId() ?? self::CONTEXT_SITE;
        }
        if (!isset($this->enabledProducts[$mainContextId])) {
            $versionDao = DAORegistry::getDAO('VersionDAO'); /** @var \PKP\site\VersionDAO $versionDao */
            $this->enabledProducts[$mainContextId] = $versionDao->getCurrentProducts($mainContextId);
        }

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

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

    /**
     * Return the current version of the application.
     *
     * @return \PKP\site\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 [
            'AccessKeyDAO' => 'PKP\security\AccessKeyDAO',
            'AnnouncementDAO' => 'PKP\announcement\AnnouncementDAO',
            'AnnouncementTypeDAO' => 'PKP\announcement\AnnouncementTypeDAO',
            'CitationDAO' => 'PKP\citation\CitationDAO',
            'ControlledVocabDAO' => 'PKP\controlledVocab\ControlledVocabDAO',
            'ControlledVocabEntryDAO' => 'PKP\controlledVocab\ControlledVocabEntryDAO',
            'DataObjectTombstoneDAO' => 'PKP\tombstone\DataObjectTombstoneDAO',
            'DataObjectTombstoneSettingsDAO' => 'PKP\tombstone\DataObjectTombstoneSettingsDAO',
            'FilterDAO' => 'PKP\filter\FilterDAO',
            'FilterGroupDAO' => 'PKP\filter\FilterGroupDAO',
            'GenreDAO' => 'PKP\submission\GenreDAO',
            'InterestDAO' => 'PKP\user\InterestDAO',
            'InterestEntryDAO' => 'PKP\user\InterestEntryDAO',
            'LibraryFileDAO' => 'PKP\context\LibraryFileDAO',
            'NavigationMenuDAO' => 'PKP\navigationMenu\NavigationMenuDAO',
            'NavigationMenuItemDAO' => 'PKP\navigationMenu\NavigationMenuItemDAO',
            'NavigationMenuItemAssignmentDAO' => 'PKP\navigationMenu\NavigationMenuItemAssignmentDAO',
            'NoteDAO' => 'PKP\note\NoteDAO',
            'NotificationDAO' => 'PKP\notification\NotificationDAO',
            'NotificationSettingsDAO' => 'PKP\notification\NotificationSettingsDAO',
            'NotificationSubscriptionSettingsDAO' => 'PKP\notification\NotificationSubscriptionSettingsDAO',
            'PluginGalleryDAO' => 'PKP\plugins\PluginGalleryDAO',
            'PluginSettingsDAO' => 'PKP\plugins\PluginSettingsDAO',
            'PublicationDAO' => 'APP\publication\PublicationDAO',
            'QueuedPaymentDAO' => 'PKP\payment\QueuedPaymentDAO',
            'ReviewAssignmentDAO' => 'PKP\submission\reviewAssignment\ReviewAssignmentDAO',
            'ReviewFilesDAO' => 'PKP\submission\ReviewFilesDAO',
            'ReviewFormDAO' => 'PKP\reviewForm\ReviewFormDAO',
            'ReviewFormElementDAO' => 'PKP\reviewForm\ReviewFormElementDAO',
            'ReviewFormResponseDAO' => 'PKP\reviewForm\ReviewFormResponseDAO',
            'ReviewRoundDAO' => 'PKP\submission\reviewRound\ReviewRoundDAO',
            'RoleDAO' => 'PKP\security\RoleDAO',
            'ScheduledTaskDAO' => 'PKP\scheduledTask\ScheduledTaskDAO',
            'SessionDAO' => 'PKP\session\SessionDAO',
            'SiteDAO' => 'PKP\site\SiteDAO',
            'StageAssignmentDAO' => 'PKP\stageAssignment\StageAssignmentDAO',
            'SubEditorsDAO' => 'PKP\context\SubEditorsDAO',
            'SubmissionAgencyDAO' => 'PKP\submission\SubmissionAgencyDAO',
            'SubmissionAgencyEntryDAO' => 'PKP\submission\SubmissionAgencyEntryDAO',
            'SubmissionCommentDAO' => 'PKP\submission\SubmissionCommentDAO',
            'SubmissionDisciplineDAO' => 'PKP\submission\SubmissionDisciplineDAO',
            'SubmissionDisciplineEntryDAO' => 'PKP\submission\SubmissionDisciplineEntryDAO',
            'SubmissionEmailLogDAO' => 'PKP\log\SubmissionEmailLogDAO',
            'QueryDAO' => 'PKP\query\QueryDAO',
            'SubmissionLanguageDAO' => 'PKP\submission\SubmissionLanguageDAO',
            'SubmissionLanguageEntryDAO' => 'PKP\submission\SubmissionLanguageEntryDAO',
            'SubmissionKeywordDAO' => 'PKP\submission\SubmissionKeywordDAO',
            'SubmissionKeywordEntryDAO' => 'PKP\submission\SubmissionKeywordEntryDAO',
            'SubmissionSubjectDAO' => 'PKP\submission\SubmissionSubjectDAO',
            'SubmissionSubjectEntryDAO' => 'PKP\submission\SubmissionSubjectEntryDAO',
            'TemporaryFileDAO' => 'PKP\file\TemporaryFileDAO',
            'TemporaryInstitutionsDAO' => 'PKP\statistics\TemporaryInstitutionsDAO',
            'UserStageAssignmentDAO' => 'PKP\user\UserStageAssignmentDAO',
            'VersionDAO' => 'PKP\site\VersionDAO',
            'WorkflowStageDAO' => 'PKP\workflow\WorkflowStageDAO',
            'XMLDAO' => 'PKP\db\XMLDAO',
        ];
    }

    /**
     * Return the fully-qualified (e.g. page.name.ClassNameDAO) name of the
     * given DAO.
     *
     * @param string $name
     *
     * @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;
    }

    /**
     * Get a mapping of license URL to license locale key for common
     * creative commons licenses.
     *
     * @return array
     */
    public static function getCCLicenseOptions()
    {
        return [
            '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 ?string $ccLicenseURL URL to creative commons license
     * @param ?string $locale Optional locale to return badge in
     *
     * @return ?string HTML code for CC license
     */
    public function getCCLicenseBadge($ccLicenseURL, $locale = null)
    {
        if (!$ccLicenseURL) {
            return null;
        }

        $licenseKeyMap = [
            '|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'
        ];

        $locale ??= Locale::getLocale();
        foreach ($licenseKeyMap as $pattern => $key) {
            if (preg_match($pattern, $ccLicenseURL)) {
                return __($key, [], $locale);
            }
        }
        return null;
    }

    /**
     * Get a mapping of role keys and i18n key names.
     *
     * @param bool $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 = [Role::ROLE_ID_SITE_ADMIN => 'user.role.siteAdmin'];
        $appRoleNames = [
            Role::ROLE_ID_MANAGER => 'user.role.manager',
            Role::ROLE_ID_SUB_EDITOR => 'user.role.subEditor',
            Role::ROLE_ID_ASSISTANT => 'user.role.assistant',
            Role::ROLE_ID_AUTHOR => 'user.role.author',
            Role::ROLE_ID_REVIEWER => 'user.role.reviewer',
            Role::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 [
            self::WORKFLOW_TYPE_EDITORIAL => [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT],
            self::WORKFLOW_TYPE_AUTHOR => [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_AUTHOR],
        ];
    }

    /**
     * Get the name of a workflow stage
     *
     * @param int $stageId One of the WORKFLOW_STAGE_* constants
     */
    public static function getWorkflowStageName(int $stageId): string
    {
        return match ($stageId) {
            WORKFLOW_STAGE_ID_SUBMISSION => 'submission.submission',
            WORKFLOW_STAGE_ID_INTERNAL_REVIEW => 'workflow.review.internalReview',
            WORKFLOW_STAGE_ID_EXTERNAL_REVIEW => 'workflow.review.externalReview',
            WORKFLOW_STAGE_ID_EDITING => 'submission.editorial',
            WORKFLOW_STAGE_ID_PRODUCTION => 'submission.production',
            default => 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)
    {
        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
     */
    public static function getReadableMaxFileSize(): string
    {
        return strtoupper(UPLOAD_MAX_FILESIZE) . 'B';
    }

    /**
     * Convert the max upload size to an integer in MBs
     */
    public static function getIntMaxFileMBs(): int
    {
        $size = (int) UPLOAD_MAX_FILESIZE;
        $unit = strtolower(substr(UPLOAD_MAX_FILESIZE, -1));
        // No suffix fallbacks to "byte"
        match (ctype_alpha($unit) ? $unit : 'b') {
            'g' => $size <<= 10,
            'm' => null,
            'k' => $size >>= 10,
            'b' => $size >>= 20,
            default => error_log(sprintf('Invalid value for the PHP configuration upload_max_filesize "%s"', UPLOAD_MAX_FILESIZE))
        };
        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',
            'dataAvailability',
        ];
    }

    /**
     * Retrieves whether the application is installed
     */
    public static function isInstalled(): bool
    {
        return !!Config::getVar('general', 'installed');
    }

    /**
     * Retrieves whether the application is running an upgrade
     */
    public static function isUpgrading(): bool
    {
        return defined('RUNNING_UPGRADE');
    }

    /**
     * Retrieves whether the application is under maintenance (not installed or being upgraded)
     */
    public static function isUnderMaintenance(): bool
    {
        return !static::isInstalled() || static::isUpgrading();
    }

    /**
     * Signals the application is undergoing an upgrade
     */
    public static function upgrade(): void
    {
        // Constant kept for backwards compatibility
        if (!defined('RUNNING_UPGRADE')) {
            define('RUNNING_UPGRADE', true);
        }
    }

    /**
     * Get the property name for a section id
     *
     * In OMP, the section is referred to as a series and the
     * property name is different.
     */
    public static function getSectionIdPropName(): string
    {
        return 'sectionId';
    }
}

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');
