[ Mini Kiebo ]
Server: Windows NT DESKTOP-5B8S0D4 6.2 build 9200 (Windows 8 Professional Edition) i586
Path:
D:
/
Backup
/
14082024
/
Data
/
htdocs
/
htdocs
/
jurnal-kesmas
/
baru
/
lib
/
pkp
/
classes
/
plugins
/
[
Home
]
File: ThemePlugin.php
<?php /** * @file classes/plugins/ThemePlugin.php * * Copyright (c) 2014-2021 Simon Fraser University * Copyright (c) 2003-2021 John Willinsky * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING. * * @class ThemePlugin * * @ingroup plugins * * @brief Abstract class for theme plugins */ namespace PKP\plugins; use APP\core\Application; use APP\core\Request; use APP\core\Services; use APP\facades\Repo; use APP\statistics\StatisticsHelper; use APP\template\TemplateManager; use Exception; use PKP\cache\CacheManager; use PKP\cache\FileCache; use PKP\config\Config; use PKP\context\Context; use PKP\core\Core; use PKP\core\PKPApplication; use PKP\db\DAORegistry; use PKP\session\SessionManager; define('LESS_FILENAME_SUFFIX', '.less'); define('THEME_OPTION_PREFIX', 'themeOption_'); abstract class ThemePlugin extends LazyLoadPlugin { /** * Collection of styles * * @see self::_registerStyles * * @var array $styles */ public $styles = []; /** * Collection of scripts * * @see self::_registerScripts * * @var array $scripts */ public $scripts = []; /** * Theme-specific options * * @var array; $options */ public $options = []; /** * Theme-specific navigation menu areas * * @var array $menuAreas */ public $menuAreas = []; /** * Parent theme (optional) * * @var ThemePlugin $parent */ public $parent; /** * Stored reference to option values * * A null value indicates that no lookup has occurred. If no options are set, * the lookup will assign an empty array. * * @var null|array; $_optionValues */ protected $_optionValues = null; /** * @copydoc Plugin::register * * @param null|mixed $mainContextId */ public function register($category, $path, $mainContextId = null) { if (!parent::register($category, $path, $mainContextId)) { return false; } // Don't perform any further operations if theme is not currently active if (!$this->isActive()) { return true; } // Themes must initialize their functionality after all theme plugins // have been loaded in order to make use of parent/child theme // relationships Hook::add('PluginRegistry::categoryLoaded::themes', [$this, 'themeRegistered']); Hook::add('PluginRegistry::categoryLoaded::themes', [$this, 'initAfter']); // Allow themes to override plugin template files Hook::add('TemplateResource::getFilename', [$this, '_overridePluginTemplates']); return true; } /** * Fire the init() method when a theme is registered * * @param array $themes List of all loaded themes */ public function themeRegistered($themes) { // Don't fully initialize the theme until the application is installed, so that // there are no requests to the database before it exists if (SessionManager::isDisabled()) { return; } $this->init(); } /** * The primary method themes should use to add styles, scripts and fonts, * or register hooks. This method is only fired for the currently active * theme. * */ abstract public function init(); /** * Perform actions after the theme has been initialized * * Registers templates, styles and scripts that have been added by the * theme or any parent themes */ public function initAfter() { $this->_registerTemplates(); $this->_registerStyles(); $this->_registerScripts(); } /** * Determine whether or not this plugin is currently active * * This only returns true if the theme is currently the selected theme * in a given context. Use self::getEnabled() if you want to know if the * theme is available for use on the site. * * @return bool */ public function isActive() { if (SessionManager::isDisabled()) { return false; } $request = Application::get()->getRequest(); $context = $request->getContext(); if ($context instanceof Context) { $activeTheme = $context->getData('themePluginPath'); } else { $site = $request->getSite(); $activeTheme = $site->getData('themePluginPath'); } return $activeTheme == basename($this->getPluginPath()); } /** * Add a stylesheet to load with this theme * * Style paths with a .less extension will be compiled and redirected to * the compiled file. * * @param string $name A name for this stylesheet * @param string $style The stylesheet. Should be a path relative to the * theme directory or, if the `inline` argument is included, style data to * be output. * @param array $args Optional arguments hash. Supported args: * 'context': Whether to load this on the `frontend` or `backend`. * default: `frontend` * 'priority': Controls order in which styles are printed * 'addLess': Additional LESS files to process before compiling. Array * 'addLessVariables': A string containing additional LESS variables to * parse before compiling. Example: "@bg:#000;" * `inline` bool Whether the $style value should be output directly as * style data. */ public function addStyle($name, $style, $args = []) { // Pass a file path for LESS files if (substr($style, (strlen(LESS_FILENAME_SUFFIX) * -1)) === LESS_FILENAME_SUFFIX) { $args['style'] = $this->_getBaseDir($style); // Pass a URL for other files } elseif (empty($args['inline'])) { if (isset($args['baseUrl'])) { $args['style'] = $args['baseUrl'] . $style; } else { $args['style'] = $this->_getBaseUrl($style); } // Leave inlined styles alone } else { $args['style'] = $style; } // Generate file paths for any additional LESS files to compile with // this style if (isset($args['addLess'])) { foreach ($args['addLess'] as &$file) { $file = $this->_getBaseDir($file); } } $this->styles[$name] = $args; } /** * Modify the params of an existing stylesheet * * @param string $name The name of the stylesheet to modify * @param array $args Parameters to modify. * * @see self::addStyle() */ public function modifyStyle($name, $args = []) { $style = &$this->getStyle($name); if (empty($style)) { return; } if (isset($args['addLess'])) { foreach ($args['addLess'] as &$file) { $file = $this->_getBaseDir($file); } } if (isset($args['style']) && !isset($args['inline'])) { $args['style'] = substr($args['style'], (strlen(LESS_FILENAME_SUFFIX) * -1)) == LESS_FILENAME_SUFFIX ? $this->_getBaseDir($args['style']) : $this->_getBaseUrl($args['style']); } $style = array_merge_recursive($style, $args); } /** * Remove a registered stylesheet * * @param string $name The name of the stylesheet to remove * * @return bool Whether or not the stylesheet was found and removed. */ public function removeStyle($name) { if (isset($this->styles[$name])) { unset($this->styles[$name]); return true; } return $this->parent ? $this->parent->removeStyle($name) : false; } /** * Get a style from this theme or any parent theme * * @param string $name The name of the style to retrieve * * @return array|null Reference to the style or null if not found */ public function &getStyle($name) { // Search this theme if (isset($this->styles[$name])) { $style = &$this->styles[$name]; return $style; } // If no parent theme, no style was found if (!isset($this->parent)) { $style = null; return $style; } return $this->parent->getStyle($name); } /** * Add a script to load with this theme * * @param string $name A name for this script * @param string $script The script to be included. Should be path relative * to the theme or, if the `inline` argument is included, script data to * be output. * @param array $args Optional arguments hash. Supported args: * `context` string Whether to load this on the `frontend` or `backend`. * default: frontend * `priority` int Controls order in which scripts are printed * default: TemplateManager::STYLE_SEQUENCE_NORMAL * `inline` bool Whether the $script value should be output directly as * script data. Used to pass backend data to the scripts. */ public function addScript($name, $script, $args = []) { if (!empty($args['inline'])) { $args['script'] = $script; } elseif (isset($args['baseUrl'])) { $args['script'] = $args['baseUrl'] . $script; } else { $args['script'] = $this->_getBaseUrl($script); } $this->scripts[$name] = $args; } /** * Modify the params of an existing script * * @param string $name The name of the script to modify * @param array $args Parameters to modify. * * @see self::addScript() */ public function modifyScript($name, $args = []) { $script = &$this->getScript($name); if (empty($script)) { return; } if (isset($args['path'])) { $args['path'] = $this->_getBaseUrl($args['path']); } $script = array_merge($script, $args); } /** * Remove a registered script * * @param string $name The name of the script to remove * * @return bool Whether or not the stylesheet was found and removed. */ public function removeScript($name) { if (isset($this->scripts[$name])) { unset($this->scripts[$name]); return true; } return $this->parent ? $this->parent->removeScript($name) : false; } /** * Get a script from this theme or any parent theme * * @param string $name The name of the script to retrieve * * @return array|null Reference to the script or null if not found */ public function &getScript($name) { // Search this theme if (isset($this->scripts[$name])) { $style = &$this->scripts[$name]; return $style; } // If no parent theme, no script was found if (!isset($this->parent)) { return; } return $this->parent->getScript($name); } /** * Add a theme option * * Theme options are added programmatically to the Settings > Website > * Appearance form when this theme is activated. Common options are * colour and typography selectors. * * @param string $name Unique name for this setting * @param string $type One of the Field* class names * @param array $args Optional parameters defining this setting. Some setting * types may accept or require additional arguments. * `label` string Locale key for a label for this field. * `description` string Locale key for a description for this field. * `default` mixed A default value. Default: '' */ public function addOption($name, $type, $args = []) { if (!empty($this->options[$name])) { return; } // Convert theme option types from before v3.2 if (in_array($type, ['text', 'colour', 'radio'])) { if (isset($args['label'])) { $args['label'] = __($args['label']); } if (isset($args['description'])) { $args['description'] = __($args['description']); } switch ($type) { case 'text': $type = 'FieldText'; break; case 'colour': $type = 'FieldColor'; break; case 'radio': $type = 'FieldOptions'; $args['type'] = 'radio'; if (!empty($args['options'])) { $options = []; foreach ($args['options'] as $optionValue => $optionLabel) { $options[] = ['value' => $optionValue, 'label' => __($optionLabel)]; } $args['options'] = $options; } break; } } $class = 'PKP\components\forms\\' . $type; try { $this->options[$name] = new $class($name, $args); } catch (Exception $e) { $class = 'APP\components\forms\\' . $type; try { $this->options[$name] = new $class($name, $args); } catch (Exception $e) { throw new Exception(sprintf( 'The %s class was not found for the theme option, %s, defined by %s or one of its parent themes.', $type, $name, $this->getDisplayName() )); } } } /** * Get the value of an option or default if the option is not set * * @param string $name The name of the option value to retrieve * * @return mixed The value of the option. Will return a default if set in * the option config. False if no option exists. Null if no value or default * exists. */ public function getOption($name) { // Check if this is a valid option if (!isset($this->options[$name])) { return $this->parent ? $this->parent->getOption($name) : false; } // Retrieve option values if they haven't been loaded yet if (is_null($this->_optionValues)) { $context = Application::get()->getRequest()->getContext(); $contextId = $context ? $context->getId() : \PKP\core\PKPApplication::CONTEXT_ID_NONE; $this->_optionValues = $this->getOptionValues($contextId); } if (isset($this->_optionValues[$name])) { return $this->_optionValues[$name]; } // Return a default if no value is set if (isset($this->options[$name])) { $option = $this->options[$name]; } elseif ($this->parent) { $option = $this->parent->getOption($name); } return $option->default ?? null; } /** * Get an option's configuration settings * * This retrieves option settings for any option attached to this theme or * any parent theme. * * @param string $name The name of the option config to retrieve * * @return false|array The config array for this option. Or false if no * config is found. */ public function getOptionConfig($name) { if (isset($this->options[$name])) { return $this->options[$name]; } return $this->parent ? $this->parent->getOptionConfig($name) : false; } /** * Get all options' configuration settings. * * This retrieves a single array containing options settings for this * theme and any parent themes. * * @return array */ public function getOptionsConfig() { if (!$this->parent) { return $this->options; } return array_merge( $this->parent->getOptionsConfig(), $this->options ); } /** * Modify option configuration settings * * @deprecated Unnecessary since 3.2 because options are stored as objects, * so changes can be made directly (via reference) and args don't need to be * manually merged * * @param string $name The name of the option config to retrieve * @param array $args The new configuration settings for this option */ public function modifyOptionsConfig($name, $args = []) { $option = $this->getOption($name); foreach ($args as $key => $value) { if (property_exists($option, $key)) { $option->{$key} = $value; } } } /** * Remove an option * * @param string $name The name of the option to remove * * @return bool Whether the option was found and removed */ public function removeOption($name) { if (isset($this->options[$name])) { unset($this->options[$name]); return true; } return $this->parent ? $this->parent->removeOption($name) : false; } /** * Get all option values * * This retrieves a single array containing option values for this theme * and any parent themes. * * @param int $contextId * * @return array */ public function getOptionValues($contextId) { /** @var PluginSettingsDAO */ $pluginSettingsDAO = DAORegistry::getDAO('PluginSettingsDAO'); $return = []; $values = $pluginSettingsDAO->getPluginSettings($contextId, $this->getName()); foreach ($this->options as $optionName => $optionConfig) { $value = $values[$optionName] ?? null; // Convert values stored in the db to the type of the default value if (!is_null($optionConfig->default)) { switch (gettype($optionConfig->default)) { case 'boolean': $value = !$value || $value === 'false' ? false : true; break; case 'integer': $value = (int) $value; break; case 'array': try { $value = json_decode($value, true, flags: JSON_THROW_ON_ERROR); } catch (Exception) { // FIXME: pkp/pkp-lib#6250 Remove after 3.3.x upgrade code is removed (see also pkp/pkp-lib#5772) $value = unserialize($value); } $value = is_array($value) ? $value : []; break; } } $return[$optionName] = $value; } if (!$this->parent) { return $return; } return array_merge( $this->parent->getOptionValues($contextId), $return ); } /** * Overwrite this function to perform any validation on options before they * are saved * * If this is a child theme, you must call $this->parent->validateOptions() to * perform any validation defined on the parent theme. * * @param array $options Key/value list of options to validate * @param string $themePluginPath The theme these options are for * @param int $contextId The context these theme options are for, or * CONTEXT_ID_NONE for the site-wide settings. * @param Request $request * * @return array List of errors with option name as the key and the value as * an array of error messages. Example: * [ * 'color' => [ * 'This color is too dark for this area and some people will not be able to read it.', * ] * ] */ public function validateOptions($options, $themePluginPath, $contextId, $request) { return []; } /** * Sanitize and save a theme option * * @param string $name A unique id for the option to save * @param mixed $value The new value to save * @param int $contextId Optional context id. Defaults to the current * context */ public function saveOption($name, $value, $contextId = null) { $option = !empty($this->options[$name]) ? $this->options[$name] : null; if (is_null($option)) { return $this->parent ? $this->parent->saveOption($name, $value, $contextId) : false; } if (is_null($contextId)) { $context = Application::get()->getRequest()->getContext(); $contextId = $context->getId(); } $pluginSettingsDao = DAORegistry::getDAO('PluginSettingsDAO'); /** @var PluginSettingsDAO $pluginSettingsDao */ // Remove setting row for empty string values (but not all falsy values) if ($value === '') { $pluginSettingsDao->deleteSetting($contextId, $this->getName(), $name); } else { $type = $pluginSettingsDao->getType($value); $value = $pluginSettingsDao->convertToDb($value, $type); $this->updateSetting($contextId, $name, $value, $type); } } /** * Register a navigation menu area for this theme * * @param string|array $menuAreas One or more menu area names */ public function addMenuArea($menuAreas) { if (!is_array($menuAreas)) { $menuAreas = [$menuAreas]; } $this->menuAreas = array_merge($this->menuAreas, $menuAreas); } /** * Remove a registered navigation menu area * * @param string $menuArea The menu area to remove * * @return bool Whether or not the menuArea was found and removed. */ public function removeMenuArea($menuArea) { $index = array_search($menuArea, $this->menuAreas); if ($index !== false) { array_splice($this->menuAreas, $index, 1); return true; } return $this->parent ? $this->parent->removeMenuArea($menuArea) : false; } /** * Get all menu areas registered by this theme and any parents * * @param array $existingAreas Any existing menu areas from child themes * * @return array All menu areas */ public function getMenuAreas($existingAreas = []) { $existingAreas = array_unique(array_merge($this->menuAreas, $existingAreas)); return $this->parent ? $this->parent->getMenuAreas($existingAreas) : $existingAreas; } /** * Set a parent theme for this theme * * @param string $parent Key in the plugin registry for the parent theme */ public function setParent($parent) { $parent = PluginRegistry::getPlugin('themes', $parent); if (!($parent instanceof self)) { return; } $this->parent = $parent; $this->parent->init(); } /** * Register directories to search for template files * */ private function _registerTemplates() { // Register parent theme template directory if (isset($this->parent) && $this->parent instanceof self) { $this->parent->_registerTemplates(); } // Register this theme's template directory $request = Application::get()->getRequest(); $templateManager = TemplateManager::getManager($request); $templateManager->addTemplateDir($this->_getBaseDir('templates')); } /** * Register stylesheets and font assets * * Passes styles defined by the theme to the template manager for handling. * */ private function _registerStyles() { if (isset($this->parent)) { $this->parent->_registerStyles(); } $request = Application::get()->getRequest(); $dispatcher = $request->getDispatcher(); $templateManager = TemplateManager::getManager($request); foreach ($this->styles as $name => $data) { if (empty($data['style'])) { continue; } // Compile LESS files if ($dispatcher && substr($data['style'], (strlen(LESS_FILENAME_SUFFIX) * -1)) == LESS_FILENAME_SUFFIX) { $styles = $dispatcher->url( $request, PKPApplication::ROUTE_COMPONENT, null, 'page.PageHandler', 'css', null, [ 'name' => $name, ] ); } else { $styles = $data['style']; } unset($data['style']); $templateManager->addStylesheet($name, $styles, $data); } } /** * Register script assets * * Passes scripts defined by the theme to the template manager for handling. * */ public function _registerScripts() { if (isset($this->parent)) { $this->parent->_registerScripts(); } $request = Application::get()->getRequest(); $templateManager = TemplateManager::getManager($request); foreach ($this->scripts as $name => $data) { $script = $data['script']; unset($data['script']); $templateManager->addJavaScript($name, $script, $data); } } /** * Get the base URL to be used for file paths * * A base URL for loading LESS/CSS/JS files in <link> elements. It will * also be set to the @baseUrl variable before LESS files are compiled so * that images and fonts can be located. * * @param string $path An optional path to append to the base * * @return string */ public function _getBaseUrl($path = '') { $request = Application::get()->getRequest(); $path = empty($path) ? '' : "/{$path}"; return "{$request->getBaseUrl()}/{$this->getPluginPath()}$path"; } /** * Get the base path to be used for file references * * @param string $path An optional path to append to the base * * @return string */ public function _getBaseDir($path = '') { $path = empty($path) ? '' : "/{$path}"; return Core::getBaseDir() . "/{$this->getPluginPath()}$path"; } /** * Check if the passed colour is dark * * This is a utility function to determine the darkness of a hex colour. This * is designed to be used in theme colour options, so that text can be * adjusted to ensure it's readable on light or dark backgrounds. You can * specify the brightness threshold by passing in a $limit value. Higher * values are brighter. * * Based on: http://stackoverflow.com/a/8468448/1723499 * * @since 0.1 */ public function isColourDark(string $color, $limit = 130) { $color = str_replace('#', '', $color); $r = hexdec(substr($color, 0, 2)); $g = hexdec(substr($color, 2, 2)); $b = hexdec(substr($color, 4, 2)); $contrast = sqrt( $r * $r * .241 + $g * $g * .691 + $b * $b * .068 ); return $contrast < $limit; } /** * Add usage statistics graph to submission view page */ public function displayUsageStatsGraph(int $submissionId): void { $this->addUsageStatsJavascriptData($this->getAllDownloadsStats($submissionId), $submissionId); $this->loadChartJavascript(); } /** * Add submission's monthly statistics data to the script data output for graph display */ protected function addUsageStatsJavascriptData(array $statsByMonth, int $submissionId): void { // Initialize the name space $script_data = 'var pkpUsageStats = pkpUsageStats || {};'; $script_data .= 'pkpUsageStats.data = pkpUsageStats.data || {};'; $script_data .= 'pkpUsageStats.data.Submission = pkpUsageStats.data.Submission || {};'; $namespace = 'Submission[' . $submissionId . ']'; $script_data .= 'pkpUsageStats.data.' . $namespace . ' = ' . json_encode($statsByMonth) . ';'; $request = Application::get()->getRequest(); $templateMgr = TemplateManager::getManager($request); $templateMgr->addJavaScript( 'pkpUsageStatsData', $script_data, [ 'inline' => true, 'contexts' => $this->getSubmissionViewContext(), ] ); } /** * Load JavaScript assets for usage statistics display and pass data to the scripts */ protected function loadChartJavascript(): void { $request = Application::get()->getRequest(); $templateMgr = TemplateManager::getManager($request); // Register Chart.js on the frontend article view $min = Config::getVar('general', 'enable_minified') ? '.min' : ''; $templateMgr->addJavaScript( 'chartJS', $request->getBaseUrl() . '/lib/pkp/js/lib/Chart' . $min . '.js', [ 'contexts' => $this->getSubmissionViewContext(), ] ); // Add locale and configuration data $chartType = $this->getOption('displayStats'); $script_data = 'var pkpUsageStats = pkpUsageStats || {};'; $script_data .= 'pkpUsageStats.locale = pkpUsageStats.locale || {};'; $script_data .= 'pkpUsageStats.locale.months = ' . json_encode(explode(' ', __('plugins.themes.default.displayStats.monthInitials'))) . ';'; $script_data .= 'pkpUsageStats.config = pkpUsageStats.config || {};'; $script_data .= 'pkpUsageStats.config.chartType = ' . json_encode($chartType) . ';'; $templateMgr->addJavaScript( 'pkpUsageStatsConfig', $script_data, [ 'inline' => true, 'contexts' => $this->getSubmissionViewContext(), ] ); // Register the JS which initializes the chart $templateMgr->addJavaScript( 'usageStatsFrontend', $request->getBaseUrl() . '/lib/pkp/js/usage-stats-chart.js', [ 'contexts' => $this->getSubmissionViewContext(), ] ); } /** * Retrieve download metrics for the given submission */ protected function getAllDownloadsStats(int $submissionId): array { $cache = CacheManager::getManager()->getCache('downloadStats', $submissionId, [$this, 'downloadStatsCacheMiss']); if (time() - $cache->getCacheTime() > 60 * 60 * 24) { // Cache is older than one day, erase it. $cache->flush(); } $statsByMonth = []; $totalDownloads = 0; $data = $cache->get($submissionId); foreach ($data as $monthlyDownloadStats) { [$year, $month] = explode('-', $monthlyDownloadStats['date']); $month = ltrim($month, '0'); $statsByMonth[$year][$month] = $monthlyDownloadStats['value']; $totalDownloads += $monthlyDownloadStats['value']; } return [ 'data' => $statsByMonth, 'label' => __('common.allDownloads'), 'color' => $this->getUsageStatsDisplayColor(REALLY_BIG_NUMBER), 'total' => $totalDownloads ]; } /** * Callback to fill cache with submission's download usage statistics data. */ public function downloadStatsCacheMiss(FileCache $cache, int $submissionId): array { $request = Application::get()->getRequest(); $submission = Repo::submission()->get($submissionId); $params = [ 'contextIds' => [$request->getContext()->getId()], 'submissionIds' => [$submissionId], 'assocTypes' => [Application::ASSOC_TYPE_SUBMISSION_FILE], 'timelineInterval' => StatisticsHelper::STATISTICS_DIMENSION_MONTH, ]; $originalPublication = $submission->getOriginalPublication(); if (!$originalPublication) { return []; } if ($earliestDatePublished = $originalPublication->getData('datePublished')) { $params['dateStart'] = $earliestDatePublished; } $statsService = Services::get('publicationStats'); /** @var \App\services\StatsPublicationService $statsService */ $data = $statsService->getTimeline($params['timelineInterval'], $params); $cache->setEntireCache([$submissionId => $data]); return $data; } /** * Return a color RGB code to be used in the usage statistics display graph. */ protected function getUsageStatsDisplayColor(int $num): string { $hash = md5('color' . $num * 2); return hexdec(substr($hash, 0, 2)) . ',' . hexdec(substr($hash, 2, 2)) . ',' . hexdec(substr($hash, 4, 2)); } /** * Get the context for inclusion of usage stats display related JavaScripts in the submission view page */ protected function getSubmissionViewContext(): string { if (Application::get()->getName() == 'ojs2') { return 'frontend-article-view'; } elseif (Application::get()->getName() == 'omp') { return 'frontend-catalog-book'; } elseif (Application::get()->getName() == 'ops') { return 'frontend-preprint-view'; } } } if (!PKP_STRICT_MODE) { class_alias('\PKP\plugins\ThemePlugin', '\ThemePlugin'); }