first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-06-08 17:09:23 -04:00
commit df3a033196
17887 changed files with 8637778 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
<?php
/**
* @file classes/core/APIResponse.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 APIResponse
*
* @ingroup core
*
* @brief Extends the Response class in the Slim microframework.
*/
namespace PKP\core;
use Slim\Http\Response;
class APIResponse extends Response
{
public const RESPONSE_CSV = 'text/csv';
public const RESPONSE_TSV = 'text/tab-separated-values';
/**
* CSV Response
*
* @param integer $maxRows The total amount of rows, that is provided within the onw X-Total-Count header field
*/
public function withCSV(array $rows, array $columns, int $maxRows, string $mimeType = self::RESPONSE_CSV): self
{
$separator = ',' ;
if ($mimeType == self::RESPONSE_TSV) {
$separator = "\t";
}
$fp = fopen('php://output', 'wt');
//Add BOM (byte order mark) to fix UTF-8 in Excel
fprintf($fp, chr(0xEF) . chr(0xBB) . chr(0xBF));
if (!empty($columns)) {
fputcsv($fp, [''], $separator);
fputcsv($fp, $columns, $separator);
}
foreach ($rows as $row) {
fputcsv($fp, $row, $separator);
}
$csvData = stream_get_contents($fp);
fclose($fp);
$this->getBody()->rewind();
$this->getBody()->write($csvData);
$this->withStatus(200);
return $this->withHeader('X-Total-Count', $maxRows)->withHeader('Content-Type', $mimeType);
}
/**
* Response with an error message
*
* @param string $msg The message translation key
* @param array $params Optional parameters to pass to the translation
*
* @return APIResponse
*/
public function withJsonError($msg, $params = null)
{
return $this->withJson(
[
'error' => $msg,
'errorMessage' => __($msg, $params ?? []),
]
);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\core\APIResponse', '\APIResponse');
}
+209
View File
@@ -0,0 +1,209 @@
<?php
/**
* @file classes/core/APIRouter.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 APIRouter
*
* @ingroup core
*
* @brief Map HTTP requests to a REST API using the Slim microframework.
*
* Requests for [index.php]/api are intercepted for site-level API requests,
* and requests for [index.php]/{contextPath}/api are intercepted for
* context-level API requests.
*/
namespace PKP\core;
use APP\core\Application;
use Exception;
use PKP\handler\APIHandler;
use PKP\session\SessionManager;
class APIRouter extends PKPRouter
{
/**
* Determines path info parts
*
*/
protected function getPathInfoParts(): array
{
return explode('/', trim($_SERVER['PATH_INFO'] ?? '', '/'));
}
/**
* Determines whether this router can route the given request.
*
* @param PKPRequest $request
*
* @return bool true, if the router supports this request, otherwise false
*/
public function supports($request): bool
{
$pathInfoParts = $this->getPathInfoParts();
if (!is_null($pathInfoParts) && count($pathInfoParts) >= 2 && $pathInfoParts[1] == 'api') {
// Context-specific API requests: [index.php]/{contextPath}/api
return true;
}
return false;
}
/**
* Get the API version
*
* @return string
*/
public function getVersion()
{
$pathInfoParts = $this->getPathInfoParts();
return Core::cleanFileVar($pathInfoParts[2] ?? '');
}
/**
* Get the entity being requested
*
* @return string
*/
public function getEntity()
{
$pathInfoParts = $this->getPathInfoParts();
return Core::cleanFileVar($pathInfoParts[3] ?? '');
}
//
// Implement template methods from PKPRouter
//
/**
* @copydoc PKPRouter::route()
*/
public function route($request)
{
// Ensure slim library is available
require_once('lib/pkp/lib/vendor/autoload.php');
$sourceFile = sprintf('api/%s/%s/index.php', $this->getVersion(), $this->getEntity());
if (!file_exists($sourceFile)) {
http_response_code('404');
header('Content-Type: application/json');
echo json_encode([
'error' => 'api.404.endpointNotFound',
'errorMessage' => __('api.404.endpointNotFound'),
]);
exit;
}
if (!SessionManager::isDisabled()) {
// Initialize session
SessionManager::getManager();
}
$handler = require('./' . $sourceFile);
$this->setHandler($handler);
$handler->getApp()->run();
}
/**
* Get the requested operation
*
* @param PKPRequest $request
*
* @return string
*/
public function getRequestedOp($request)
{
/** @var APIHandler */
$handler = $this->getHandler();
$container = $handler->getApp()->getContainer();
$router = $container->get('router');
$slimRequest = $handler->getSlimRequest();
$routeInfo = $router->dispatch($slimRequest);
if (isset($routeInfo[1])) {
$route = $router->lookupRoute($routeInfo[1]);
$callable = $route->getCallable();
if (is_array($callable) && count($callable) == 2) {
return $callable[1];
}
}
return '';
}
/**
* @copydoc PKPRouter::handleAuthorizationFailure()
*/
public function handleAuthorizationFailure(
$request,
$authorizationMessage,
array $messageParams = []
) {
http_response_code('403');
header('Content-Type: application/json');
echo json_encode([
'error' => $authorizationMessage,
'errorMessage' => __($authorizationMessage, $messageParams),
]);
exit;
}
/**
* @copydoc PKPRouter::url()
*
* @param null|mixed $newContext
* @param null|mixed $endpoint
* @param null|mixed $op
* @param null|mixed $path
* @param null|mixed $params
* @param null|mixed $anchor
*/
public function url(
PKPRequest $request,
?string $newContext = null,
$endpoint = null,
$op = null,
$path = null,
$params = null,
$anchor = null,
$escape = false
) {
// APIHandlers do not understand $op, $path or $anchor. All routing is baked
// into the $endpoint string. It only accepts a string as the $newContext,
// since it relies on this when path info is disabled.
if (!is_null($op) || !is_null($path) || !is_null($anchor) || !is_scalar($newContext)) {
throw new Exception('APIRouter::url() should not be called with an op, path or anchor. If a new context is passed, the context path must be passed instead of the context object.');
}
//
// Base URL and Context
//
$baseUrlAndContext = $this->_urlGetBaseAndContext($request, $newContext);
$baseUrl = array_shift($baseUrlAndContext);
$context = array_shift($baseUrlAndContext);
//
// Additional query parameters
//
$additionalParameters = $this->_urlGetAdditionalParameters($request, $params, $escape);
//
// Assemble URL
//
$pathInfoArray = array_merge(
$context,
['api', Application::API_VERSION, $endpoint]
);
$queryParametersArray = $additionalParameters;
return $this->_urlFromParts($baseUrl, $pathInfoArray, $queryParametersArray, $anchor, $escape);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\core\APIRouter', '\APIRouter');
}
@@ -0,0 +1,53 @@
<?php
/**
* @file classes/core/AppServiceProvider.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 AppServiceProvider
*
* @ingroup core
*
* @brief Resolves requests for application classes such as the request handler
* to support dependency injection
*/
namespace PKP\core;
use APP\core\Application;
use APP\core\Services;
use Illuminate\Support\ServiceProvider;
use PKP\context\Context;
use PKP\services\PKPSchemaService;
class AppServiceProvider extends ServiceProvider
{
/**
* Register application services
*
* Services registered on the app container here can be automatically
* injected as dependencies to classes that are instantiated by the
* app container.
*
* @see https://laravel.com/docs/8.x/container#automatic-injection
* @see https://laravel.com/docs/8.x/providers#the-register-method
*/
public function register()
{
$this->app->singleton('maps', function ($app) {
return new MapContainer();
});
$this->app->singleton(PKPSchemaService::class, function ($app) {
return Services::get('schema');
});
$this->app->singleton(PKPRequest::class, function ($app) {
return Application::get()->getRequest();
});
$this->app->bind(Context::class, function ($app) {
return Application::get()->getRequest()->getContext();
});
}
}
+197
View File
@@ -0,0 +1,197 @@
<?php
/**
* @file classes/core/ArrayItemIterator.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 ArrayItemIterator
*
* @ingroup db
*
* @brief Provides paging and iteration for arrays.
*/
namespace PKP\core;
class ArrayItemIterator extends ItemIterator
{
/** @var array The array of contents of this iterator. */
public $theArray;
/** @var int Number of items to iterate through on this page */
public $itemsPerPage;
/** @var int The current page. */
public $page;
/** @var int The total number of items. */
public $count;
/** @var bool Whether or not the iterator was empty from the start */
public $wasEmpty;
/**
* Constructor.
*
* @param array $theArray The array of items to iterate through
* @param int $page the current page number
* @param int $itemsPerPage Number of items to display per page
*/
public function __construct(&$theArray, $page = -1, $itemsPerPage = -1)
{
parent::__construct();
if ($page >= 1 && $itemsPerPage >= 1) {
$this->theArray = array_slice($theArray, ($page - 1) * $itemsPerPage, $itemsPerPage, true);
$this->page = $page;
} else {
$this->theArray = & $theArray;
$this->page = 1;
$this->itemsPerPage = max(count($this->theArray), 1);
}
$this->count = count($theArray);
$this->itemsPerPage = $itemsPerPage;
$this->wasEmpty = count($this->theArray) == 0;
reset($this->theArray);
}
/**
* Static method: Generate an iterator from an array and rangeInfo object.
*
* @param array $theArray
* @param object $theRange
*/
public function &fromRangeInfo(&$theArray, &$theRange)
{
if ($theRange && $theRange->isValid()) {
$theIterator = new ArrayItemIterator($theArray, $theRange->getPage(), $theRange->getCount());
} else {
$theIterator = new ArrayItemIterator($theArray);
}
return $theIterator;
}
/**
* Return the next item in the iterator.
*
* @return ?object
*/
public function &next()
{
if (!is_array($this->theArray)) {
$value = null;
return $value;
}
$value = current($this->theArray);
if (next($this->theArray) === false) {
$this->theArray = null;
}
return $value;
}
/**
* Return the next item in the iterator, with key.
*
* @return array (key, value)
*/
public function nextWithKey()
{
$key = key($this->theArray);
$value = $this->next();
return [$key, $value];
}
/**
* Determine whether or not this iterator represents the first page
*
* @return bool
*/
public function atFirstPage()
{
return $this->page == 1;
}
/**
* Determine whether or not this iterator represents the last page
*
* @return bool
*/
public function atLastPage()
{
return ($this->page * $this->itemsPerPage) + 1 > $this->count;
}
/**
* Get the current page number
*
* @return int
*/
public function getPage()
{
return $this->page;
}
/**
* Get the total count of items
*
* @return int
*/
public function getCount()
{
return $this->count;
}
/**
* Get the number of pages
*
* @return int
*/
public function getPageCount()
{
return max(1, ceil($this->count / $this->itemsPerPage));
}
/**
* Return a boolean indicating whether or not we've reached the end of results
*
* @return bool
*/
public function eof()
{
return (($this->theArray == null) || (count($this->theArray) == 0));
}
/**
* Return a boolean indicating whether or not this iterator was empty from the beginning
*
* @return bool
*/
public function wasEmpty()
{
return $this->wasEmpty;
}
/**
* Convert this iterator to an array
*
* @return array
*/
public function toArray()
{
return $this->theArray;
}
/**
* Return this iterator as an associative array.
*/
public function toAssociativeArray()
{
return $this->theArray;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\core\ArrayItemIterator', '\ArrayItemIterator');
}
+513
View File
@@ -0,0 +1,513 @@
<?php
/**
* @defgroup core Core
* Core web application concerns such as routing, dispatching, etc.
*/
/**
* @file classes/core/Core.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 Core
*
* @ingroup core
*
* @brief Class containing system-wide functions.
*/
namespace PKP\core;
use Exception;
use PKP\cache\CacheManager;
use PKP\cache\FileCache;
use PKP\config\Config;
use SplFileInfo;
define('PKP_LIB_PATH', 'lib/pkp');
define('COUNTER_USER_AGENTS_FILE', Core::getBaseDir() . '/' . PKP_LIB_PATH . '/lib/counterBots/generated/COUNTER_Robots_list.txt');
class Core
{
/** @var array The regular expressions that will find a bot user agent */
public static $botRegexps = [];
/**
* Get the path to the base installation directory.
*
* @return string
*/
public static function getBaseDir()
{
static $baseDir;
return $baseDir ??= dirname(INDEX_FILE_LOCATION);
}
/**
* Sanitize a value to be used in a file path.
* Removes any characters except alphanumeric characters, underscores, and dashes.
*
* @param string $var
*
* @return string
*/
public static function cleanFileVar($var)
{
return cleanFileVar($var);
}
/**
* Return the current date in ISO (YYYY-MM-DD HH:MM:SS) format.
*
* @param int $ts optional, use specified timestamp instead of current time
*
* @return string
*/
public static function getCurrentDate($ts = null)
{
return date('Y-m-d H:i:s', $ts ?? time());
}
/**
* Return *nix timestamp with microseconds (in units of seconds).
*
* @return float
*/
public static function microtime()
{
[$usec, $sec] = explode(' ', microtime());
return (float)$sec + (float)$usec;
}
/**
* Check if the server platform is Windows.
*
* @return bool
*/
public static function isWindows()
{
return strtolower_codesafe(substr(PHP_OS, 0, 3)) == 'win';
}
/**
* Checks to see if a PHP module is enabled.
*
* @param string $moduleName
*
* @return bool
*/
public static function checkGeneralPHPModule($moduleName)
{
if (extension_loaded($moduleName)) {
return true;
}
return false;
}
/**
* Check the passed user agent for a bot.
*
* @param string $userAgent
* @param string $botRegexpsFile An alternative file with regular
* expressions to find bots inside user agent strings.
*
* @return bool
*/
public static function isUserAgentBot($userAgent, $botRegexpsFile = COUNTER_USER_AGENTS_FILE)
{
static $botRegexps;
Registry::set('currentUserAgentsFile', $botRegexpsFile);
if (!isset($botRegexps[$botRegexpsFile])) {
$botFileCacheId = md5($botRegexpsFile);
$cacheManager = CacheManager::getManager();
/** @var FileCache */
$cache = $cacheManager->getCache('core', $botFileCacheId, ['Core', '_botFileListCacheMiss'], CACHE_TYPE_FILE);
$botRegexps[$botRegexpsFile] = $cache->getContents();
}
foreach ($botRegexps[$botRegexpsFile] as $regexp) {
// make the search case insensitive
$regexp .= 'i';
if (PKPString::regexp_match($regexp, $userAgent)) {
return true;
}
}
return false;
}
/**
* Get context path present into the passed
* url information.
*
* @param string $urlInfo Full url or just path info.
*/
public static function getContextPath(string $urlInfo): string
{
$contextPaths = explode('/', trim($urlInfo, '/'), 2);
return self::cleanFileVar($contextPaths[0] ?: 'index');
}
/**
* Get the page present into
* the passed url information. It expects that urls
* were built using the system.
*
* @param string $urlInfo Full url or just path info.
* @param bool $isPathInfo Tell if the
* passed url info string is a path info or not.
* @param array $userVars (optional) Pass GET variables
* if needed (for testing only).
*
* @return string
*/
public static function getPage($urlInfo, $isPathInfo, $userVars = [])
{
$page = Core::_getUrlComponents($urlInfo, $isPathInfo, 0, 'page', $userVars);
return Core::cleanFileVar(is_null($page) ? '' : $page);
}
/**
* Get the operation present into
* the passed url information. It expects that urls
* were built using the system.
*
* @param string $urlInfo Full url or just path info.
* @param bool $isPathInfo Tell if the
* passed url info string is a path info or not.
* @param array $userVars (optional) Pass GET variables
* if needed (for testing only).
*
* @return string
*/
public static function getOp($urlInfo, $isPathInfo, $userVars = [])
{
$operation = Core::_getUrlComponents($urlInfo, $isPathInfo, 1, 'op', $userVars);
return Core::cleanFileVar(empty($operation) ? 'index' : $operation);
}
/**
* Get the arguments present into
* the passed url information (not GET/POST arguments,
* only arguments appended to the URL separated by "/").
* It expects that urls were built using the system.
*
* @param string $urlInfo Full url or just path info.
* @param bool $isPathInfo Tell if the
* passed url info string is a path info or not.
* @param array $userVars (optional) Pass GET variables
* if needed (for testing only).
*
* @return array
*/
public static function getArgs($urlInfo, $isPathInfo, $userVars = [])
{
return Core::_getUrlComponents($urlInfo, $isPathInfo, 2, 'path', $userVars);
}
/**
* Remove base url from the passed url, if any.
* Also, if true, checks for the context path in
* url and if it's missing, tries to add it.
*
* @param string $url
*
* @return string|bool The url without base url,
* false if it was not possible to remove it.
*/
public static function removeBaseUrl($url)
{
[$baseUrl, $contextPath] = Core::_getBaseUrlAndPath($url);
if (!$baseUrl) {
return false;
}
// Remove base url from url, if any.
$url = str_replace($baseUrl, '', $url);
// If url doesn't have the entire protocol and host part,
// remove any possible base url path from url.
$baseUrlPath = parse_url($baseUrl, PHP_URL_PATH);
if ($baseUrlPath == $url) {
// Access to the base url, no context, the entire
// url is part of the base url and we can return empty.
$url = '';
} else {
// Handle case where index.php was removed by rewrite rules,
// and we have base url followed by the args.
if (strpos($url, $baseUrlPath . '?') === 0) {
$replacement = '?'; // Url path replacement.
$baseSystemEscapedPath = preg_quote($baseUrlPath . '?', '/');
} else {
$replacement = '/'; // Url path replacement.
$baseSystemEscapedPath = preg_quote($baseUrlPath . '/', '/');
}
$url = preg_replace('/^' . $baseSystemEscapedPath . '/', $replacement, $url);
// Remove possible index.php page from url.
$url = str_replace('/index.php', '', $url);
}
if ($contextPath) {
// We found the contextPath using the base_url
// config file settings. Check if the url starts
// with the context path, if not, prepend it.
if (strpos($url, '/' . $contextPath . '/') !== 0) {
$url = '/' . $contextPath . $url;
}
}
// Remove any possible trailing slashes.
$url = rtrim($url, '/');
return $url;
}
/**
* Try to get the base url and, if configuration
* is set to use base url override, context
* path for the passed url.
*
* @param string $url
*
* @return array With two elements, base url and context path.
*/
protected static function _getBaseUrlAndPath($url)
{
$baseUrl = false;
$contextPath = false;
// Check for override base url settings.
$contextBaseUrls = Config::getContextBaseUrls();
if (empty($contextBaseUrls)) {
$baseUrl = Config::getVar('general', 'base_url');
} else {
// We are just interested in context base urls, remove the index one.
if (isset($contextBaseUrls['index'])) {
unset($contextBaseUrls['index']);
}
// Arrange them in length order, so we make sure
// we get the correct one, in case there's an overlaping
// of contexts, eg.:
// base_url[context1] = http://somesite.com/
// base_url[context2] = http://somesite.com/context2
$sortedBaseUrls = array_combine($contextBaseUrls, array_map('strlen', $contextBaseUrls));
arsort($sortedBaseUrls);
foreach (array_keys($sortedBaseUrls) as $workingBaseUrl) {
$urlHost = parse_url($url, PHP_URL_HOST);
if (is_null($urlHost)) {
// Check the base url without the host part.
$baseUrlHost = parse_url($workingBaseUrl, PHP_URL_HOST);
if (is_null($baseUrlHost)) {
break;
}
$baseUrlToSearch = substr($workingBaseUrl, strpos($workingBaseUrl, $baseUrlHost) + strlen($baseUrlHost));
// Base url with only host part, add trailing slash
// so it can be checked below.
if (!$baseUrlToSearch) {
$baseUrlToSearch = '/';
}
} else {
$baseUrlToSearch = $workingBaseUrl;
}
$baseUrlCheck = Core::_checkBaseUrl($baseUrlToSearch, $url);
if (is_null($baseUrlCheck)) {
// Can't decide. Stop searching.
break;
} elseif ($baseUrlCheck === true) {
$contextPath = array_search($workingBaseUrl, $contextBaseUrls);
$baseUrl = $workingBaseUrl;
break;
}
}
}
// If we still have no base URL, this may be a situation where we have an install with some customized URLs, and some not.
// Return the default base URL.
if (!$baseUrl) {
$baseUrl = Config::getVar('general', 'base_url');
}
return [$baseUrl, $contextPath];
}
/**
* Check if the passed base url is part of
* the passed url, based on the context base url
* configuration. Both parameters can represent
* full url (host plus path) or just the path,
* but they have to be consistent.
*
* @param string $baseUrl Full base url
* or just it's path info.
* @param string $url Full url or just it's
* path info.
*
* @return ?bool
*/
protected static function _checkBaseUrl($baseUrl, $url)
{
// Check if both base url and url have host
// component or not.
$baseUrlHasHost = (bool) parse_url($baseUrl, PHP_URL_HOST);
$urlHasHost = (bool) parse_url($url, PHP_URL_HOST);
if ($baseUrlHasHost !== $urlHasHost) {
return false;
}
$contextBaseUrls = & Config::getContextBaseUrls();
// If the base url is found inside the passed url,
// then we might found the right context path.
if (strpos($url, $baseUrl) === 0) {
if (strpos($url, '/index.php') == strlen($baseUrl) - 1) {
// index.php appears right after the base url,
// no more possible paths.
return true;
} else {
// Still have to check if there is no other context
// base url that combined with it's context path is
// equal to this base url. If it exists, we can't
// tell which base url is contained in url.
foreach ($contextBaseUrls as $contextPath => $workingBaseUrl) {
$urlToCheck = $workingBaseUrl . '/' . $contextPath;
if (!$baseUrlHasHost) {
$urlToCheck = parse_url($urlToCheck, PHP_URL_PATH);
}
if ($baseUrl == $urlToCheck) {
return null;
}
}
return true;
}
}
return false;
}
/**
* Bot list file cache miss fallback.
* (WARNING: This function appears to be used externally, hence public despite _ prefix.)
*
* @param FileCache $cache
*
* @return array
*/
public static function _botFileListCacheMiss($cache)
{
$id = $cache->getCacheId();
$filteredBotRegexps = array_filter(
file(Registry::get('currentUserAgentsFile')),
function ($regexp) {
$regexp = trim($regexp);
return !empty($regexp) && $regexp[0] != '#';
}
);
$botRegexps = array_map(
function ($regexp) {
$delimiter = '/';
$regexp = trim($regexp);
if (strpos($regexp, $delimiter) !== 0) {
// Make sure delimiters are in place.
$regexp = $delimiter . $regexp . $delimiter;
}
return $regexp;
},
$filteredBotRegexps
);
$cache->setEntireCache($botRegexps);
return $botRegexps;
}
/**
* Get passed variable value inside the passed url.
*
* @param string $url
* @param string $varName
* @param array $userVars
*
* @return string|null
*/
private static function _getUserVar($url, $varName, $userVars = [])
{
parse_str((string) parse_url($url, PHP_URL_QUERY), $userVarsFromUrl);
return $userVarsFromUrl[$varName] ?? $userVars[$varName] ?? null;
}
/**
* Get url components (page, operation and args)
* based on the passed offset.
*
* @param string $urlInfo
* @param string $isPathInfo
* @param int $offset
* @param string $varName
* @param array $userVars (optional) GET variables
* (only for testing).
*
* @return mixed array|string|null
*/
private static function _getUrlComponents($urlInfo, $isPathInfo, $offset, $varName = '', $userVars = [])
{
$component = null;
$isArrayComponent = false;
if ($varName == 'path') {
$isArrayComponent = true;
}
$vars = explode('/', trim($urlInfo ?? '', '/'));
if (count($vars) > $offset + 1) {
if ($isArrayComponent) {
$component = array_slice($vars, $offset + 1);
} else {
$component = $vars[$offset + 1];
}
}
if ($isArrayComponent) {
if (empty($component)) {
$component = [];
} elseif (!is_array($component)) {
$component = [$component];
}
}
return $component;
}
/**
* Extract the class name from the given file path.
*
* @param SplFileInfo $file info about a file extract class name from
*
* @return string fully qualified class name
*
* @see Finder
*/
public static function classFromFile(SplFileInfo $file): string
{
$libPath = realpath(base_path(PKP_LIB_PATH));
$isLib = str_starts_with($file->getRealPath(), $libPath);
$className = str_replace($isLib ? $libPath : realpath(base_path()), '', $file->getRealPath());
// Drop the "classes" from the path (we don't use it on the namespaces) and the extension
$className = preg_replace('#^[\\\\/]classes|\.php$#', '', $className);
// Include the base namespace and replace the directory separator by the namespace separator
$className = str_replace('/', '\\', '/' . ($isLib ? 'PKP' : 'APP') . $className);
return class_exists($className)
? $className
: throw new Exception("Failed to map the file \"{$file->getRealPath()}\" to a full qualified class name");
}
}
+530
View File
@@ -0,0 +1,530 @@
<?php
/**
* @file classes/core/DataObject.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 DataObject
*
* @ingroup core
*
* @see Core
*
* @brief Any class with an associated DAO should extend this class.
*/
namespace PKP\core;
use APP\core\Application;
use PKP\db\DAO;
use PKP\db\DAORegistry;
use \PKP\filter\FilterDAO;
use PKP\facades\Locale;
/**
* @template T of EntityDAO|DAO
*/
class DataObject
{
/** @var array Array of object data */
public $_data = [];
/** @var bool whether this objects loads meta-data adapters from the database */
public $_hasLoadableAdapters = false;
/** @var array an array of meta-data extraction adapters (one per supported schema) */
public $_metadataExtractionAdapters = [];
/** @var bool whether extraction adapters have already been loaded from the database */
public $_extractionAdaptersLoaded = false;
/** @var array an array of meta-data injection adapters (one per supported schema) */
public $_metadataInjectionAdapters = [];
/** @var bool whether injection adapters have already been loaded from the database */
public $_injectionAdaptersLoaded = false;
/**
* Constructor
*/
public function __construct()
{
}
//
// Getters and Setters
//
/**
* Get a piece of data for this object, localized to the current
* locale if possible.
*/
public function getLocalizedData(string $key, string $preferredLocale = null, string &$selectedLocale = null): mixed
{
foreach ($this->getLocalePrecedence($preferredLocale) as $locale) {
$value = & $this->getData($key, $locale);
if (!empty($value)) {
$selectedLocale = $locale;
return $value;
}
unset($value);
}
// Fallback: Get the first available piece of data.
$data = $this->getData($key, null);
foreach ((array) $data as $locale => $dataValue) {
if (!empty($dataValue)) {
$selectedLocale = $locale;
return $dataValue;
}
}
return null;
}
/**
* Get the locale precedence order for object in the following order
*
* 1. Preferred Locale if provided
* 2. User's current local
* 3. Object's default locale if set
* 4. Context's primary locale if context available
* 5. Site's primary locale
*/
public function getLocalePrecedence(string $preferredLocale = null): array
{
$request = Application::get()->getRequest();
return array_unique(
array_filter([
$preferredLocale ?? Locale::getLocale(),
$this->getDefaultLocale(),
$request->getContext()?->getPrimaryLocale(),
$request->getSite()->getPrimaryLocale(),
])
);
}
/**
* Get the default locale for object
*/
public function getDefaultLocale(): ?string
{
return null;
}
/**
* Get the value of a data variable.
*
* @param string $key
* @param string $locale (optional)
*
* @return mixed
*/
public function &getData($key, $locale = null)
{
if (is_null($locale)) {
if (array_key_exists($key, $this->_data)) {
return $this->_data[$key];
}
} elseif (array_key_exists($locale, (array) ($this->_data[$key] ?? []))) {
return $this->_data[$key][$locale];
}
$nullVar = null;
return $nullVar;
}
/**
* Set the value of a new or existing data variable.
*
* @param string $key
* @param mixed $value can be either a single value or
* an array of of localized values in the form:
* array(
* 'fr_FR' => 'en français',
* 'en' => 'in English',
* ...
* )
* @param string $locale (optional) non-null for a single
* localized value. Null for a non-localized value or
* when setting all locales at once (see comment for
* $value parameter)
*/
public function setData($key, $value, $locale = null)
{
if (is_null($locale)) {
// This is either a non-localized value or we're passing in all locales at once.
$this->_data[$key] = $value;
return;
}
// Set a single localized value.
if (!is_null($value)) {
if (isset($this->_data[$key]) && !is_array($this->_data[$key])) {
$this->_data[$key] = [];
}
$this->_data[$key][$locale] = $value;
return;
}
// If the value is null, remove the entry.
if (array_key_exists($key, $this->_data)) {
if (array_key_exists($locale, (array) $this->_data[$key])) {
unset($this->_data[$key][$locale]);
}
// Was this the last entry for the data variable?
if (empty($this->_data[$key])) {
unset($this->_data[$key]);
}
}
}
/**
* Unset an element of the data object.
*
* @param string $key
* @param string $locale (optional) non-null for a single
* localized value. Null for a non-localized value or
* when unsetting all locales at once.
*/
public function unsetData($key, $locale = null)
{
if (is_null($locale)) {
unset($this->_data[$key]);
} else {
unset($this->_data[$key][$locale]);
}
}
/**
* Check whether a value exists for a given data variable.
*
* @param string $key
* @param string $locale (optional)
*
* @return bool
*/
public function hasData($key, $locale = null)
{
return is_null($locale) ? array_key_exists($key, $this->_data) : array_key_exists($locale, (array) ($this->_data[$key] ?? []));
}
/**
* Return an array with all data variables.
*
* @return array
*/
public function &getAllData()
{
return $this->_data;
}
/**
* Set all data variables at once.
*
* @param array $data
*/
public function setAllData($data)
{
$this->_data = $data;
}
/**
* Get ID of object.
*
* @return int
*/
public function getId()
{
return $this->getData('id');
}
/**
* Set ID of object.
*
* @param int $id
*/
public function setId($id)
{
$this->setData('id', $id);
}
//
// MetadataProvider interface implementation
//
/**
* Set whether the object has loadable meta-data adapters
*
* @param bool $hasLoadableAdapters
*/
public function setHasLoadableAdapters($hasLoadableAdapters)
{
$this->_hasLoadableAdapters = $hasLoadableAdapters;
}
/**
* Get whether the object has loadable meta-data adapters
*
* @return bool
*/
public function getHasLoadableAdapters()
{
return $this->_hasLoadableAdapters;
}
/**
* Add a meta-data adapter that will be supported
* by this application entity. Only one adapter per schema
* can be added.
*
* @param \PKP\metadata\MetadataDataObjectAdapter $metadataAdapter
*/
public function addSupportedMetadataAdapter($metadataAdapter)
{
$metadataSchemaName = $metadataAdapter->getMetadataSchemaName();
assert(!empty($metadataSchemaName));
// NB: Some adapters are injectors and extractors at the same time,
// notably the meta-data description dummy adapter that converts
// from/to a meta-data description. That's why we have to check
// input and output type separately.
// Is this a meta-data extractor?
$inputType = $metadataAdapter->getInputType();
if ($inputType->checkType($this)) {
if (!isset($this->_metadataExtractionAdapters[$metadataSchemaName])) {
$this->_metadataExtractionAdapters[$metadataSchemaName] = $metadataAdapter;
}
}
// Is this a meta-data injector?
$outputType = $metadataAdapter->getOutputType();
if ($outputType->checkType($this)) {
if (!isset($this->_metadataInjectionAdapters[$metadataSchemaName])) {
$this->_metadataInjectionAdapters[$metadataSchemaName] = $metadataAdapter;
}
}
}
/**
* Remove all adapters for the given meta-data schema
* (if it exists).
*
* @param string $metadataSchemaName fully qualified class name
*
* @return bool true if an adapter was removed, otherwise false.
*/
public function removeSupportedMetadataAdapter($metadataSchemaName)
{
$result = false;
if (isset($this->_metadataExtractionAdapters[$metadataSchemaName])) {
unset($this->_metadataExtractionAdapters[$metadataSchemaName]);
$result = true;
}
if (isset($this->_metadataInjectionAdapters[$metadataSchemaName])) {
unset($this->_metadataInjectionAdapters[$metadataSchemaName]);
$result = true;
}
return $result;
}
/**
* Get all meta-data extraction adapters that
* support this data object. This includes adapters
* loaded from the database.
*
* @return array
*/
public function getSupportedExtractionAdapters()
{
// Load meta-data adapters from the database.
if ($this->getHasLoadableAdapters() && !$this->_extractionAdaptersLoaded) {
$filterDao = DAORegistry::getDAO('FilterDAO'); /** @var FilterDAO $filterDao */
$loadedAdapters = $filterDao->getObjectsByTypeDescription('class::%', 'metadata::%', $this);
foreach ($loadedAdapters as $loadedAdapter) {
$this->addSupportedMetadataAdapter($loadedAdapter);
}
$this->_extractionAdaptersLoaded = true;
}
return $this->_metadataExtractionAdapters;
}
/**
* Get all meta-data injection adapters that
* support this data object. This includes adapters
* loaded from the database.
*
* @return array
*/
public function getSupportedInjectionAdapters()
{
// Load meta-data adapters from the database.
if ($this->getHasLoadableAdapters() && !$this->_injectionAdaptersLoaded) {
$filterDao = DAORegistry::getDAO('FilterDAO'); /** @var FilterDAO $filterDao */
$loadedAdapters = $filterDao->getObjectsByTypeDescription('metadata::%', 'class::%', $this, false);
foreach ($loadedAdapters as $loadedAdapter) {
$this->addSupportedMetadataAdapter($loadedAdapter);
}
$this->_injectionAdaptersLoaded = true;
}
return $this->_metadataInjectionAdapters;
}
/**
* Returns all supported meta-data schemas
* which are supported by extractor adapters.
*
* @return array
*/
public function getSupportedMetadataSchemas()
{
$supportedMetadataSchemas = [];
$extractionAdapters = $this->getSupportedExtractionAdapters();
foreach ($extractionAdapters as $metadataAdapter) {
$supportedMetadataSchemas[] = $metadataAdapter->getMetadataSchema();
}
return $supportedMetadataSchemas;
}
/**
* Retrieve the names of meta-data
* properties of this data object.
*
* @param bool $translated if true, return localized field
* names, otherwise return additional field names.
*/
public function getMetadataFieldNames($translated = true)
{
// Create a list of all possible meta-data field names
$metadataFieldNames = [];
$extractionAdapters = $this->getSupportedExtractionAdapters();
foreach ($extractionAdapters as $metadataAdapter) {
// Add the field names from the current adapter
$metadataFieldNames = array_merge(
$metadataFieldNames,
$metadataAdapter->getDataObjectMetadataFieldNames($translated)
);
}
return array_unique($metadataFieldNames);
}
/**
* Retrieve the names of meta-data
* properties that need to be persisted
* (i.e. that have data).
*
* @param bool $translated if true, return localized field
* names, otherwise return additional field names.
*
* @return array an array of field names
*/
public function getSetMetadataFieldNames($translated = true)
{
// Retrieve a list of all possible meta-data field names
$metadataFieldNameCandidates = $this->getMetadataFieldNames($translated);
// Only retain those fields that have data
$metadataFieldNames = [];
foreach ($metadataFieldNameCandidates as $metadataFieldNameCandidate) {
if ($this->hasData($metadataFieldNameCandidate)) {
$metadataFieldNames[] = $metadataFieldNameCandidate;
}
}
return $metadataFieldNames;
}
/**
* Retrieve the names of translated meta-data
* properties that need to be persisted.
*
* @return array an array of field names
*/
public function getLocaleMetadataFieldNames()
{
return $this->getMetadataFieldNames(true);
}
/**
* Retrieve the names of additional meta-data
* properties that need to be persisted.
*
* @return array an array of field names
*/
public function getAdditionalMetadataFieldNames()
{
return $this->getMetadataFieldNames(false);
}
/**
* Inject a meta-data description into this
* data object.
*
* @param \PKP\metadata\MetadataDescription $metadataDescription
*
* @return bool true on success, otherwise false
*/
public function injectMetadata($metadataDescription)
{
$dataObject = null;
$metadataSchemaName = $metadataDescription->getMetadataSchemaName();
$injectionAdapters = $this->getSupportedInjectionAdapters();
if (isset($injectionAdapters[$metadataSchemaName])) {
// Get the meta-data adapter that supports the
// given meta-data description's schema.
$metadataAdapter = $injectionAdapters[$metadataSchemaName]; /** @var \PKP\metadata\MetadataDataObjectAdapter $metadataAdapter */
// Pass in a reference to the data object which
// the filter will use to update the current instance
// of the data object.
$metadataAdapter->setTargetDataObject($this);
// Use adapter filter to convert from a meta-data
// description to a data object.
$dataObject = $metadataAdapter->execute($metadataDescription);
}
return $dataObject;
}
/**
* Extract a meta-data description from this
* data object.
*
* @param \PKP\metadata\MetadataSchema $metadataSchema
*
* @return $metadataDescription MetadataDescription
*/
public function extractMetadata($metadataSchema)
{
$metadataDescription = null;
$metadataSchemaName = $metadataSchema->getClassName();
$extractionAdapters = $this->getSupportedExtractionAdapters();
if (isset($extractionAdapters[$metadataSchemaName])) {
// Get the meta-data adapter that supports the
// given meta-data description's schema.
$metadataAdapter = $extractionAdapters[$metadataSchemaName];
// Use adapter filter to convert from a data object
// to a meta-data description.
$metadataDescription = $metadataAdapter->execute($this);
}
return $metadataDescription;
}
/**
* Get DAO class for this object.
*
* @return T
*/
public function getDAO()
{
assert(false);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\core\DataObject', '\DataObject');
}
+298
View File
@@ -0,0 +1,298 @@
<?php
/**
* @file classes/core/Dispatcher.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 Dispatcher
*
* @ingroup core
*
* @brief Class dispatching HTTP requests to handlers.
*/
namespace PKP\core;
use APP\core\Services;
use PKP\config\Config;
use PKP\plugins\Hook;
use PKP\plugins\PluginRegistry;
use PKP\services\PKPSchemaService;
class Dispatcher
{
/** @var PKPApplication */
public $_application;
/** @var array an array of Router implementation class names */
public $_routerNames = [];
/** @var array an array of Router instances */
public $_routerInstances = [];
/** @var PKPRouter */
public $_router;
/** @var PKPRequest Used for a callback hack - NOT GENERALLY SET. */
public $_requestCallbackHack;
/**
* Get the application
*
* @return PKPApplication
*/
public function &getApplication()
{
return $this->_application;
}
/**
* Set the application
*
* @param PKPApplication $application
*/
public function setApplication($application)
{
$this->_application = $application;
}
/**
* Get the router names
*
* @return array an array of Router names
*/
public function &getRouterNames()
{
return $this->_routerNames;
}
/**
* Add a router name.
*
* NB: Routers will be called in the order that they
* have been added to the dispatcher. The first router
* that supports the request will be called. The last
* router should always be a "catch-all" router that
* supports all types of requests.
*
* NB: Routers must be part of the core package
* to be accepted by this dispatcher implementation.
*
* @param string $routerName a class name of a router
* to be given the chance to route the request.
* NB: These are class names and not instantiated objects. We'll
* use lazy instantiation to reduce the performance/memory impact
* to a minimum.
* @param string $shortcut a shortcut name for the router
* to be used for quick router instance retrieval.
*/
public function addRouterName($routerName, $shortcut)
{
assert(is_array($this->_routerNames) && is_string($routerName));
$this->_routerNames[$shortcut] = $routerName;
}
/**
* Determine the correct router for this request. Then
* let the router dispatch the request to the appropriate
* handler method.
*
* @param PKPRequest $request
*/
public function dispatch($request)
{
// Make sure that we have at least one router configured
$routerNames = $this->getRouterNames();
assert(count($routerNames) > 0);
// Go through all configured routers by priority
// and find out whether one supports the incoming request
/** @var PKPRouter */
$router = null;
foreach ($routerNames as $shortcut => $routerCandidateName) {
$routerCandidate = & $this->_instantiateRouter($routerCandidateName, $shortcut);
// Does this router support the current request?
if ($routerCandidate->supports($request)) {
// Inject router and dispatcher into request
$request->setRouter($routerCandidate);
$request->setDispatcher($this);
// We've found our router and can go on
// to handle the request.
$router = & $routerCandidate;
$this->_router = & $router;
break;
}
}
// None of the router handles this request? This is a development-time
// configuration error.
if (is_null($router)) {
fatalError('None of the configured routers supports this request.');
}
// Can we serve a cached response?
if ($router->isCacheable($request)) {
$this->_requestCallbackHack = & $request;
if (Config::getVar('cache', 'web_cache')) {
if ($this->_displayCached($router, $request)) {
exit;
} // Success
ob_start([$this, '_cacheContent']);
}
} else {
if (isset($_SERVER['HTTP_X_MOZ']) && $_SERVER['HTTP_X_MOZ'] == 'prefetch') {
header('HTTP/1.0 403 Forbidden');
echo '403: Forbidden<br><br>Pre-fetching not allowed.';
exit;
}
}
PluginRegistry::loadCategory('generic', true);
PluginRegistry::loadCategory('pubIds', true);
Hook::call('Dispatcher::dispatch', [$request]);
// Reload the context after generic plugins have loaded so that changes to
// the context schema can take place
$contextSchema = Services::get('schema')->get(PKPSchemaService::SCHEMA_CONTEXT, true);
$request->getRouter()->getContext($request, true);
$router->route($request);
}
/**
* Build a handler request URL into PKPApplication.
*
* @param PKPRequest $request the request to be routed
* @param string $shortcut the short name of the router that should be used to construct the URL
* @param mixed $newContext Optional contextual paths
* @param string $handler Optional name of the handler to invoke
* @param string $op Optional name of operation to invoke
* @param mixed $path Optional string or array of args to pass to handler
* @param array $params Optional set of name => value pairs to pass as user parameters
* @param string $anchor Optional name of anchor to add to URL
* @param bool $escape Whether or not to escape ampersands for this URL; default false.
*
* @return string the URL
*/
public function url(
PKPRequest $request,
string $shortcut,
?string $newContext = null,
$handler = null,
$op = null,
$path = null,
$params = null,
$anchor = null,
$escape = false
) {
// Instantiate the requested router
assert(isset($this->_routerNames[$shortcut]));
$routerName = $this->_routerNames[$shortcut];
$router = & $this->_instantiateRouter($routerName, $shortcut);
return $router->url($request, $newContext, $handler, $op, $path, $params, $anchor, $escape);
}
//
// Private helper methods
//
/**
* Instantiate a router
*
* @param string $routerName
* @param string $shortcut
*/
public function &_instantiateRouter($routerName, $shortcut)
{
if (!isset($this->_routerInstances[$shortcut])) {
// Instantiate the router
$router = new $routerName();
if (!$router instanceof \PKP\core\PKPRouter) {
throw new \Exception('Cannot instantiate requested router. Routers must belong to the core package and be of type "PKPRouter".');
}
$router->setApplication($this->_application);
$router->setDispatcher($this);
// Save the router instance for later re-use
$this->_routerInstances[$shortcut] = & $router;
}
return $this->_routerInstances[$shortcut];
}
/**
* Display the request contents from cache.
*
* @param PKPRouter $router
*/
public function _displayCached($router, $request)
{
$filename = $router->getCacheFilename($request);
if (!file_exists($filename)) {
return false;
}
// Allow a caching proxy to work its magic if possible
$ifModifiedSince = $request->getIfModifiedSince();
if ($ifModifiedSince !== null && $ifModifiedSince >= filemtime($filename)) {
header('HTTP/1.1 304 Not Modified');
exit;
}
$data = file_get_contents($filename);
$i = strpos($data, ':');
$time = substr($data, 0, $i);
$contents = substr($data, $i + 1);
if (time() > $time + Config::getVar('cache', 'web_cache_hours') * 60 * 60) {
return false;
}
header('Content-Type: text/html; charset=utf-8');
echo $contents;
return true;
}
/**
* Cache content as a local file.
*
* @param string $contents
*
* @return string
*/
public function _cacheContent($contents)
{
assert($this->_router instanceof \PKP\core\PKPRouter);
if ($contents == '') {
return $contents;
} // Do not cache empties
$filename = $this->_router->getCacheFilename($this->_requestCallbackHack);
$fp = fopen($filename, 'w');
if ($fp) {
fwrite($fp, time() . ':' . $contents);
fclose($fp);
}
return $contents;
}
/**
* Handle a 404 error (page not found).
*/
public static function handle404()
{
header('HTTP/1.0 404 Not Found');
fatalError('404 Not Found');
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\core\Dispatcher', '\Dispatcher');
}
+323
View File
@@ -0,0 +1,323 @@
<?php
/**
* @file classes/core/EntityDAO.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 EntityDAO
*
* @brief A base class for DAOs that read and write an entity to the database
*/
namespace PKP\core;
use Exception;
use Illuminate\Support\Facades\DB;
use PKP\db\DAO;
use PKP\services\PKPSchemaService;
/**
* @template T of DataObject
*/
abstract class EntityDAO
{
/** @var string One of the \PKP\services\PKPSchemaService::SCHEMA_... constants */
public $schema;
/** @var string The name of the primary table for this entity */
public $table;
/** @var string The name of the settings table for this entity */
public $settingsTable;
/** @var string The column name for the object id in primary and settings tables */
public $primaryKeyColumn;
/** @var array Maps schema properties for the primary table to their column names */
public $primaryTableColumns = [];
/**
* @var array Map schema properties to the primary table
*
* An array mapping the property names of an entity to the
* correct column in the database table.
*
* Example:
*
* ```
* [
* 'id' => 'announcement_id',
* 'datePosted' => 'date_posted',
* }
* ```
*
* Only include properties stored in self::$table. Properties
* stored in self::$settingsTable do not need to be mapped.
*/
public $primaryKeyColumns;
/**
* @var DAO An instance of PKP\db\DAO
*
* This provides access to a few methods that are
* still shared with the deprecated DAOs.
*
* @deprecated 3.4
*/
public $deprecatedDao;
/** @var PKPSchemaService<T> $schemaService */
protected $schemaService;
/**
* Constructor
*/
public function __construct(PKPSchemaService $schemaService)
{
$this->deprecatedDao = new DAO();
$this->schemaService = $schemaService;
}
/**
* Creates a new DataObject
*
* @return T
*/
public function newDataObject()
{
throw new Exception('Not implemented');
}
/**
* Convert a row from the database query into a DataObject
*
* @return T
*/
public function fromRow(object $row): DataObject
{
$schema = $this->schemaService->get($this->schema);
$object = $this->newDataObject();
foreach ($this->primaryTableColumns as $propName => $column) {
if (property_exists($row, $column)) {
$object->setData(
$propName,
$this->convertFromDB($row->{$column}, $schema->properties->{$propName}->type, true)
);
}
}
if ($this->settingsTable) {
$rows = DB::table($this->settingsTable)
->where($this->primaryKeyColumn, '=', $row->{$this->primaryKeyColumn})
->get();
$rows->each(function ($row) use ($object, $schema) {
if (!empty($schema->properties->{$row->setting_name})) {
$object->setData(
$row->setting_name,
$this->convertFromDB(
$row->setting_value,
$schema->properties->{$row->setting_name}->type
),
empty($row->locale) ? null : $row->locale
);
}
});
}
return $object;
}
/**
* Insert an object into the database
* @param T $object
*/
protected function _insert(DataObject $object): int
{
$schemaService = $this->schemaService;
$schema = $schemaService->get($this->schema);
$sanitizedProps = $schemaService->sanitize($this->schema, $object->_data);
$primaryDbProps = $this->getPrimaryDbProps($object);
if (empty($primaryDbProps)) {
throw new Exception('Tried to insert ' . get_class($object) . ' without any properties for the ' . $this->table . ' table.');
}
DB::table($this->table)->insert($primaryDbProps);
$object->setId((int) DB::getPdo()->lastInsertId());
// Add additional properties to settings table if they exist
if ($this->settingsTable && count($sanitizedProps) !== count($primaryDbProps)) {
foreach ($schema->properties as $propName => $propSchema) {
if (!isset($sanitizedProps[$propName]) || array_key_exists($propName, $this->primaryTableColumns)) {
continue;
}
if (!empty($propSchema->multilingual)) {
foreach ($sanitizedProps[$propName] as $localeKey => $localeValue) {
DB::table($this->settingsTable)->insert([
$this->primaryKeyColumn => $object->getId(),
'locale' => $localeKey,
'setting_name' => $propName,
'setting_value' => $this->convertToDB($localeValue, $schema->properties->{$propName}->type),
]);
}
} else {
DB::table($this->settingsTable)->insert([
$this->primaryKeyColumn => $object->getId(),
'setting_name' => $propName,
'setting_value' => $this->convertToDB($sanitizedProps[$propName], $schema->properties->{$propName}->type),
]);
}
}
}
return $object->getId();
}
/**
* Update an object in the database
* @param T $object
*/
protected function _update(DataObject $object)
{
$schemaService = $this->schemaService;
$schema = $schemaService->get($this->schema);
$sanitizedProps = $schemaService->sanitize($this->schema, $object->_data);
$primaryDbProps = $this->getPrimaryDbProps($object);
DB::table($this->table)
->where($this->primaryKeyColumn, '=', $object->getId())
->update($primaryDbProps);
if ($this->settingsTable) {
$deleteSettings = [];
foreach ($schema->properties as $propName => $propSchema) {
if (array_key_exists($propName, $this->primaryTableColumns)) {
continue;
} elseif (!isset($sanitizedProps[$propName])) {
$deleteSettings[] = $propName;
continue;
}
if (!empty($propSchema->multilingual)) {
foreach ($sanitizedProps[$propName] as $localeKey => $localeValue) {
// Delete rows with a null value
if (is_null($localeValue)) {
DB::table($this->settingsTable)
->where($this->primaryKeyColumn, '=', $object->getId())
->where('setting_name', '=', $propName)
->where('locale', '=', $localeKey)
->delete();
} else {
DB::table($this->settingsTable)
->updateOrInsert(
[
$this->primaryKeyColumn => $object->getId(),
'locale' => $localeKey,
'setting_name' => $propName,
],
[
'setting_value' => $this->convertToDB($localeValue, $schema->properties->{$propName}->type),
]
);
}
}
} else {
DB::table($this->settingsTable)
->updateOrInsert(
[
$this->primaryKeyColumn => $object->getId(),
'locale' => '',
'setting_name' => $propName,
],
[
'setting_value' => $this->convertToDB($sanitizedProps[$propName], $schema->properties->{$propName}->type),
]
);
}
}
if (count($deleteSettings)) {
DB::table($this->settingsTable)
->where($this->primaryKeyColumn, '=', $object->getId())
->whereIn('setting_name', $deleteSettings)
->delete();
}
}
}
/**
* Delete an object from the database
* @param T $object
*/
protected function _delete(DataObject $object)
{
$this->deleteById($object->getId());
}
/**
* Delete an object from the database by its id
*/
public function deleteById(int $id)
{
DB::table($this->table)
->where($this->primaryKeyColumn, '=', $id)
->delete();
}
/**
* Prepare data to be inserted into the primary table
*
* Compiles the properties of a DataObject into a key/value
* array that maps them to the primary table columns.
*
* @see $this->primaryTableColumns
* @param T $object
*/
protected function getPrimaryDbProps(DataObject $object): array
{
$schema = $this->schemaService->get($this->schema);
$sanitizedProps = $this->schemaService->sanitize($this->schema, $object->_data);
$primaryDbProps = [];
foreach ($this->primaryTableColumns as $propName => $columnName) {
if ($propName !== 'id' && array_key_exists($propName, $sanitizedProps)) {
$primaryDbProps[$columnName] = $this->convertToDB($sanitizedProps[$propName] ?? null, $schema->properties->{$propName}->type, true);
// Convert empty string values for DATETIME columns into null values
// because an empty string can not be saved to a DATETIME column
if ($primaryDbProps[$columnName] === ''
&& isset($schema->properties->{$propName}->validation)
&& (
in_array('date_format:Y-m-d H:i:s', $schema->properties->{$propName}->validation)
|| in_array('date_format:Y-m-d', $schema->properties->{$propName}->validation)
)
) {
$primaryDbProps[$columnName] = null;
}
}
}
return $primaryDbProps;
}
/**
* @copydoc DAO::convertFromDB()
*/
protected function convertFromDB($value, string $type, bool $nullable = false)
{
return $this->deprecatedDao->convertFromDB($value, $type, $nullable);
}
/**
* @copydoc DAO::convertToDB()
*/
protected function convertToDB($value, string $type, bool $nullable = false)
{
return $this->deprecatedDao->convertToDB($value, $type, $nullable);
}
}
@@ -0,0 +1,106 @@
<?php
/**
* @file classes/core/EventServiceProvider.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 EventServiceProvider
*
* @ingroup core
*
* @brief Registers Events Service Provider and boots data on events and their listeners
*/
namespace PKP\core;
use DateInterval;
use Illuminate\Foundation\Events\DiscoverEvents;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as LaravelEventServiceProvider;
use Illuminate\Support\Facades\Cache;
use SplFileInfo;
class EventServiceProvider extends LaravelEventServiceProvider
{
/** Max lifetime for the event discovery cache */
protected const MAX_CACHE_LIFETIME = '1 day';
/**
* @copydoc \Illuminate\Foundation\Support\Providers\EventServiceProvider::getEvents()
*/
public function getEvents()
{
$expiration = DateInterval::createFromDateString(static::MAX_CACHE_LIFETIME);
$events = Cache::remember(static::getCacheKey(), $expiration, fn () => $this->discoveredEvents());
return array_merge_recursive(
$events,
$this->listens()
);
}
/**
* @copydoc \Illuminate\Foundation\Support\Providers\EventServiceProvider::shouldDiscoverEvents()
*/
public function shouldDiscoverEvents()
{
return true;
}
/**
* @copydoc \Illuminate\Foundation\Support\Providers\EventServiceProvider::discoverEvents()
*/
public function discoverEvents()
{
// Adapt classes naming convention
$discoverEvents = new class () extends DiscoverEvents {
/**
* @copydoc \Illuminate\Foundation\Events\DiscoverEvents::classFromFile()
*/
protected static function classFromFile(SplFileInfo $file, $basePath): string
{
return Core::classFromFile($file);
}
};
return collect($this->discoverEventsWithin())
->reject(function ($directory) {
return !is_dir($directory);
})
->reduce(function ($discovered, $directory) use ($discoverEvents) {
return array_merge_recursive(
$discovered,
$discoverEvents::within($directory, base_path())
);
}, []);
}
/**
* @copydoc \Illuminate\Foundation\Support\Providers\EventServiceProvider::discoverEventsWithin()
*/
protected function discoverEventsWithin()
{
return [
$this->app->basePath('lib/pkp/classes/observers/listeners'),
$this->app->basePath('classes/observers/listeners'),
];
}
/**
* Clears the event cache
*/
public static function clearCache(): void
{
Cache::forget(static::getCacheKey());
}
/**
* Retrieves a unique and static key to store the event cache
*/
private static function getCacheKey(): string
{
return __METHOD__ . static::MAX_CACHE_LIFETIME;
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
/**
* @file classes/core/ExportableTrait.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 ExportableTrait
*
* @ingroup db
*
* @brief Implements the __set_state magic method, for classes that have a parameterless constructor, in order to recover classes exported with "var_export"
*/
namespace PKP\core;
trait ExportableTrait
{
/**
* Generic __set_state implementation
*/
public static function __set_state(array $data)
{
$object = new static();
foreach ($data as $key => $value) {
$object->$key = $value;
}
return $object;
}
}
+136
View File
@@ -0,0 +1,136 @@
<?php
/**
* @file classes/core/ItemIterator.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 ItemIterator
*
* @ingroup db
*
* @brief Generic iterator class; needs to be overloaded by subclasses
* providing specific implementations.
*/
namespace PKP\core;
/**
* @template TKey
* @template TValue
*/
class ItemIterator
{
/**
* Constructor
*/
public function __construct()
{
}
/**
* Return the next item in the iterator.
*
* @return TValue
*/
public function next()
{
return null;
}
/**
* Return the next item with key.
*
* @return array<Tkey,TValue>
*/
public function nextWithKey()
{
return [null, null];
}
/**
* Determine whether this iterator represents the first page of a set.
*
* @return bool
*/
public function atFirstPage()
{
return true;
}
/**
* Determine whether this iterator represents the last page of a set.
*
* @return bool
*/
public function atLastPage()
{
return true;
}
/**
* Get the page number of a set that this iterator represents.
*
* @return int
*/
public function getPage()
{
return 1;
}
/**
* Get the total number of items in the set.
*
* @return int
*/
public function getCount()
{
return 0;
}
/**
* Get the total number of pages in the set.
*
* @return int
*/
public function getPageCount()
{
return 0;
}
/**
* Return a boolean indicating whether or not we've reached the end of results
*
* @return bool
*/
public function eof()
{
return true;
}
/**
* Return a boolean indicating whether or not this iterator was empty from the beginning
*
* @return bool
*/
public function wasEmpty()
{
return true;
}
/**
* Convert this iterator to an array.
*
* @return TValue[]
*/
public function toArray()
{
return [];
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\core\ItemIterator', '\ItemIterator');
}
+217
View File
@@ -0,0 +1,217 @@
<?php
/**
* @file classes/core/JSONMessage.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 JSONMessage
*
* @ingroup core
*
* @brief Class to represent a JSON (Javascript Object Notation) message.
*
*/
namespace PKP\core;
class JSONMessage
{
/** @var string The status of an event (e.g. false if form validation fails). */
public $_status;
/** @var mixed The message to be delivered back to the calling script. */
public $_content;
/** @var string ID for DOM element that will be replaced. */
public $_elementId;
/** @var array List of JS events generated on the server side. */
public $_events;
/** @var array Set of additional attributes for special cases. */
public $_additionalAttributes;
/**
* Constructor.
*
* @param bool $status The status of an event (e.g. false if form validation fails).
* @param mixed $content The message to be delivered back to the calling script.
* @param string $elementId The DOM element to be replaced.
* @param array $additionalAttributes Additional data to be returned.
*/
public function __construct($status = true, $content = '', $elementId = '0', $additionalAttributes = null)
{
// Set internal state.
$this->setStatus($status);
$this->setContent($content);
$this->setElementId($elementId);
if (isset($additionalAttributes)) {
$this->setAdditionalAttributes($additionalAttributes);
}
}
/**
* Get the status string
*
* @return string
*/
public function getStatus()
{
return $this->_status;
}
/**
* Set the status string
*
* @param string $status
*/
public function setStatus($status)
{
assert(is_bool($status));
$this->_status = $status;
}
/**
* Get the content string
*/
public function getContent()
{
return $this->_content;
}
/**
* Set the content data
*
*/
public function setContent($content)
{
$this->_content = $content;
}
/**
* Get the elementId string
*
* @return string
*/
public function getElementId()
{
return $this->_elementId;
}
/**
* Set the elementId string
*
* @param string $elementId
*/
public function setElementId($elementId)
{
assert(is_string($elementId) || is_numeric($elementId));
$this->_elementId = $elementId;
}
/**
* Set the event to trigger with this JSON message
*
* @param string $eventName
* @param null|mixed $eventData
*/
public function setEvent($eventName, $eventData = null)
{
assert(is_string($eventName));
// Construct the even as an associative array.
$event = ['name' => $eventName];
if (!is_null($eventData)) {
$event['data'] = $eventData;
}
$this->_events[] = $event;
}
/**
* Set a global event to trigger with this JSON message
*
* This is a wrapper for the setEvent method.
*
* Global events are triggered on the global event router instead of being
* triggered directly on the handler. They are intended for broadcasting
* updates from one handler to other handlers.
*
* @param string $eventName
* @param array $eventData Global event data must be an assoc array
*/
public function setGlobalEvent($eventName, $eventData = [])
{
assert(is_array($eventData));
$eventData['isGlobalEvent'] = true;
$this->setEvent($eventName, $eventData);
}
/**
* Get the events to trigger with this JSON message
*
* @return array
*/
public function getEvents()
{
return $this->_events;
}
/**
* Get the additionalAttributes array
*
* @return array
*/
public function getAdditionalAttributes()
{
return $this->_additionalAttributes;
}
/**
* Set the additionalAttributes array
*
* @param array $additionalAttributes
*/
public function setAdditionalAttributes($additionalAttributes)
{
assert(is_array($additionalAttributes));
$this->_additionalAttributes = $additionalAttributes;
}
/**
* Construct a JSON string to use for AJAX communication
*
* @return string
*/
public function getString()
{
// Construct an associative array that contains all information we require.
$jsonObject = [
'status' => $this->getStatus(),
'content' => $this->getContent(),
'elementId' => $this->getElementId(),
'events' => $this->getEvents(),
];
if (is_array($this->getAdditionalAttributes())) {
foreach ($this->getAdditionalAttributes() as $key => $value) {
$jsonObject[$key] = $value;
}
}
// Encode the object.
$json = json_encode($jsonObject);
if ($json === false) {
error_log(json_last_error_msg());
}
return $json;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\core\JSONMessage', '\JSONMessage');
}
@@ -0,0 +1,111 @@
<?php
/**
* @file classes/core/MailServiceProvider.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 MailServiceProvider
*
* @ingroup core
*
* @brief Registers Laravel's Mailer service without support for markup rendering, such as blade or markdown templates
*/
namespace PKP\core;
use Illuminate\Mail\MailManager;
use Illuminate\Mail\MailServiceProvider as IlluminateMailService;
use InvalidArgumentException;
use PKP\mail\Mailer;
use PKP\mail\transport\PHPMailerTransport;
use Symfony\Component\Mailer\Transport\SendmailTransport;
class MailServiceProvider extends IlluminateMailService
{
/**
* Register mailer excluding markdown renderer
*/
public function register(): void
{
$this->registerIlluminateMailer();
}
/**
* @copydoc \Illuminate\Mail\MailServiceProvider::registerIlluminateMailer()
*/
public function registerIlluminateMailer(): void
{
$this->app->singleton('mail.manager', function ($app) {
return new class ($app) extends MailManager {
/**
* @see MailManager::resolve()
*
* @param string $name
*
* @throws InvalidArgumentException
*/
protected function resolve($name): Mailer
{
$config = $this->getConfig($name);
if (is_null($config)) {
throw new InvalidArgumentException("Mailer [{$name}] is not defined.");
}
// Override Illuminate mailer construction to remove unsupported view
$mailer = new Mailer(
$name,
$this->createSymfonyTransport($config),
$this->app['events']
);
if ($this->app->bound('queue')) {
$mailer->setQueue($this->app['queue']);
}
return $mailer;
}
/*
* Override sendmail transport construction to allow default path
*/
protected function createSendmailTransport(array $config): SendmailTransport
{
$path = $config['path'] ?? $this->app['config']->get('mail.sendmail');
return $path ? new SendmailTransport($path) : new SendmailTransport();
}
/**
* Transport to send with mail() function by PHPMailer
*/
protected function createPHPMailerTransport(): PHPMailerTransport
{
return new PHPMailerTransport();
}
};
});
$this->app->bind('mailer', function ($app) {
return $app->make('mail.manager')->mailer();
});
}
/**
* @copydoc \Illuminate\Mail\MailServiceProvider::provides()
*/
public function provides(): array
{
return
[
'mail.manager',
'mailer',
];
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\core\MailServiceProvider', '\MailServiceProvider');
}
+52
View File
@@ -0,0 +1,52 @@
<?php
/**
* @file classes/core/MapsContainer.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 MapContainer
*
* @ingroup core
*
* @brief This service provider allows Map classes to be instantiated and
* applies any extensions to the map registered by plugins.
*/
namespace PKP\core;
use PKP\core\maps\Base;
class MapContainer
{
/** @var array Key/value map that stores extensions to maps registered by plugins */
protected array $extensions = [];
public function extend(string $map, callable $callback)
{
if (isset($this->extensions[$map])) {
$this->extensions[$map][] = $callback;
}
$this->extensions[$map] = [$callback];
}
public function getMap(string $class, array $dependencies = []): Base
{
return app($class, $dependencies);
}
public function withExtensions(string $class, array $dependencies = []): Base
{
$map = $this->getMap($class, $dependencies);
foreach ($this->extensions as $name => $extensions) {
if (is_a($map, $name)) {
foreach ($extensions as $extension) {
$map->extend($extension);
}
}
}
return $map;
}
}
+796
View File
@@ -0,0 +1,796 @@
<?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()
{
$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');
+534
View File
@@ -0,0 +1,534 @@
<?php
/**
* @file classes/core/PKPComponentRouter.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 PKPComponentRouter
*
* @ingroup core
*
* @brief Class mapping an HTTP request to a component handler operation.
*
* We are using an RPC style URL-to-endpoint mapping. Our approach follows
* a simple "convention-over-configuration" paradigm. If necessary the
* router can be subclassed to implement more complex URL-to-endpoint mappings.
*
* For servers with path info enabled the component URL has the following elements:
*
* .../index.php/context1/context2/$$$call$$$/path/to/handler-class/operation-name?arg1=...&arg2=...
*
* where "$$$call$$$" is a non-mutable literal string and "path/to" is
* by convention the directory path below the "controllers" folder leading to the
* component. The next element ("handler-class" in this example) will be mapped to a
* component class file by "camelizing" the string to "HandlerClassHandler" and adding
* ".php" to the end. The "operation-name" is transformed to "operationName"
* and represents the name of the handler method to be called. Finally "arg1", "arg2",
* etc. are parameters to be passed along to the handler method.
*
* For servers with path info disabled the component URL looks like this:
*
* .../index.php?component=path.to.handler-class&op=operation-name&arg1=...&arg2=...
*
* The router will sanitize the request URL to a certain amount to make sure that
* random code inclusions are prevented. User authorization and parameter validation
* are however not the router's concern. These must be implemented on handler level.
*
* NB: Component and operation names may only contain a-z, 0-9 and hyphens. Numbers
* are not allowed at the beginning of a name or after a hyphen.
*
* NB: Component handlers must implement an initialize() method that will be called
* before the request is routed. The initialization method must enforce authorization
* and request validation.
*/
namespace PKP\core;
use Exception;
use PKP\config\Config;
use PKP\plugins\Hook;
// The string to be found in the URL to mark this request as a component request
define('COMPONENT_ROUTER_PATHINFO_MARKER', '$$$call$$$');
// The parameter to be found in the query string for servers with path info disabled
define('COMPONENT_ROUTER_PARAMETER_MARKER', 'component');
// This is the maximum directory depth allowed within the component directory. Set
// it to something reasonable to avoid DoS or overflow attacks
define('COMPONENT_ROUTER_PARTS_MAXDEPTH', 9);
// This is the maximum/minimum length of the name of a sub-directory or
// handler class name.
define('COMPONENT_ROUTER_PARTS_MAXLENGTH', 50);
define('COMPONENT_ROUTER_PARTS_MINLENGTH', 2);
class PKPComponentRouter extends PKPRouter
{
//
// Internal state cache variables
// NB: Please do not access directly but
// only via their respective getters/setters
//
/** @var string the requested component handler */
public $_component;
/** @var string the requested operation */
public $_op;
/** @var array the rpc service endpoint parts from the request */
public $_rpcServiceEndpointParts = false;
/** @var callable the rpc service endpoint the request was routed to */
public $_rpcServiceEndpoint = false;
/**
* Determines whether this router can route the given request.
*
* @param PKPRequest $request
*
* @return bool true, if the router supports this request, otherwise false
*/
public function supports($request): bool
{
// See whether this looks like a component router request.
// NOTE: this is prone to false positives i.e. when a class
// name cannot be matched, but this laxity permits plugins to
// extend the system by registering against the
// LoadComponentHandler hook.
return $this->_retrieveServiceEndpointParts($request) !== null;
}
/**
* Retrieve the requested component from the request.
*
* NB: This can be a component that not actually exists
* in the code base.
*
* @param PKPRequest $request
*
* @return string the requested component or an empty string
* if none can be found.
*/
public function getRequestedComponent($request)
{
if (is_null($this->_component)) {
$this->_component = '';
// Retrieve the service endpoint parts from the request.
if (is_null($rpcServiceEndpointParts = $this->_getValidatedServiceEndpointParts($request))) {
// Endpoint parts cannot be found in the request
return '';
}
// Pop off the operation part
array_pop($rpcServiceEndpointParts);
// Construct the fully qualified component class name from the rest of it.
$handlerClassName = PKPString::camelize(array_pop($rpcServiceEndpointParts), PKPString::CAMEL_CASE_HEAD_UP) . 'Handler';
// camelize remaining endpoint parts
$camelizedRpcServiceEndpointParts = [];
foreach ($rpcServiceEndpointParts as $part) {
$camelizedRpcServiceEndpointParts[] = PKPString::camelize($part, PKPString::CAMEL_CASE_HEAD_DOWN);
}
$handlerPackage = implode('.', $camelizedRpcServiceEndpointParts);
$this->_component = $handlerPackage . '.' . $handlerClassName;
}
return $this->_component;
}
/**
* Retrieve the requested operation from the request
*
* NB: This can be an operation that not actually
* exists in the requested component.
*
* @param PKPRequest $request
*
* @return string the requested operation or an empty string
* if none can be found.
*/
public function getRequestedOp($request)
{
if (is_null($this->_op)) {
$this->_op = '';
// Retrieve the service endpoint parts from the request.
if (is_null($rpcServiceEndpointParts = $this->_getValidatedServiceEndpointParts($request))) {
// Endpoint parts cannot be found in the request
return '';
}
// Pop off the operation part
$this->_op = PKPString::camelize(array_pop($rpcServiceEndpointParts), PKPString::CAMEL_CASE_HEAD_DOWN);
}
return $this->_op;
}
/**
* Get the (validated) RPC service endpoint from the request.
* If no such RPC service endpoint can be constructed then the method
* returns null.
*
* @param PKPRequest $request the request to be routed
*
* @return callable|array|null an array with the handler instance
* and the handler operation to be called by call_user_func().
*/
public function &getRpcServiceEndpoint($request)
{
if ($this->_rpcServiceEndpoint === false) {
// We have not yet resolved this request. Mark the
// state variable so that we don't try again next
// time.
$this->_rpcServiceEndpoint = $nullVar = null;
// Retrieve requested component operation
$op = $this->getRequestedOp($request);
assert(!empty($op));
//
// Component Handler
//
// Retrieve requested component handler
$component = $this->getRequestedComponent($request);
$componentInstance = null;
$allowedPackages = null;
// Give plugins a chance to intervene
if (!Hook::call('LoadComponentHandler', [&$component, &$op, &$componentInstance])) {
if (empty($component)) {
return $nullVar;
}
// Construct the component handler file name and test its existence.
$component = 'controllers.' . $component;
$componentFileNamePart = str_replace('.', '/', $component);
switch (true) {
case file_exists("{$componentFileNamePart}.php"):
$className = 'APP\\' . strtr($componentFileNamePart, '/', '\\');
$componentInstance = new $className();
break;
case file_exists("{$componentFileNamePart}.inc.php"):
// This behaviour is DEPRECATED as of 3.4.0.
break;
case file_exists(PKP_LIB_PATH . "/{$componentFileNamePart}.php"):
$className = 'PKP\\' . strtr($componentFileNamePart, '/', '\\');
$componentInstance = new $className();
break;
case file_exists(PKP_LIB_PATH . "/{$componentFileNamePart}.inc.php"):
// This behaviour is DEPRECATED as of 3.4.0.
$component = 'lib.pkp.' . $component;
break;
default:
// Request to non-existent handler
return $nullVar;
}
// We expect the handler to be part of one
// of the following packages:
$allowedPackages = [
'controllers',
'lib.pkp.controllers'
];
}
// A handler at least needs to implement the
// following methods:
$requiredMethods = [
$op, 'authorize', 'validate', 'initialize'
];
if (!$componentInstance) {
$componentInstance = instantiate($component, 'PKPHandler', $allowedPackages, $requiredMethods);
}
if (!is_object($componentInstance)) {
return $nullVar;
}
$this->setHandler($componentInstance);
//
// Callable service endpoint
//
// Construct the callable array
$this->_rpcServiceEndpoint = [$componentInstance, $op];
}
return $this->_rpcServiceEndpoint;
}
//
// Implement template methods from PKPRouter
//
/**
* @copydoc PKPRouter::route()
*/
public function route($request)
{
// Determine the requested service endpoint.
$rpcServiceEndpoint = & $this->getRpcServiceEndpoint($request);
// Retrieve RPC arguments from the request.
$args = $request->getUserVars();
assert(is_array($args));
// Remove the caller-parameter (if present)
if (isset($args[COMPONENT_ROUTER_PARAMETER_MARKER])) {
unset($args[COMPONENT_ROUTER_PARAMETER_MARKER]);
}
// Authorize, validate and initialize the request
$this->_authorizeInitializeAndCallRequest($rpcServiceEndpoint, $request, $args);
}
/**
* @copydoc PKPRouter::url()
*
* @param null|mixed $newContext
* @param null|mixed $component
* @param null|mixed $op
* @param null|mixed $path
* @param null|mixed $params
* @param null|mixed $anchor
*/
public function url(
$request,
$newContext = null,
$component = null,
$op = null,
$path = null,
$params = null,
$anchor = null,
$escape = false
) {
if (!is_null($path)) {
throw new Exception('Path must be null when calling PKPComponentRouter::url()');
}
//
// Base URL and Context
//
[$baseUrl, $context] = $this->_urlGetBaseAndContext($request, $newContext);
//
// Component and Operation
//
// We only support component/op retrieval from the request
// if this request is a component request.
$currentRequestIsAComponentRequest = $request->getRouter() instanceof self;
if ($currentRequestIsAComponentRequest) {
if (empty($component)) {
$component = $this->getRequestedComponent($request);
}
if (empty($op)) {
$op = $this->getRequestedOp($request);
}
}
assert(!empty($component) && !empty($op));
// Encode the component and operation
$componentParts = explode('.', $component);
$componentName = array_pop($componentParts);
assert(substr($componentName, -7) == 'Handler');
$componentName = PKPString::uncamelize(substr($componentName, 0, -7));
// uncamelize the component parts
$uncamelizedComponentParts = [];
foreach ($componentParts as $part) {
$uncamelizedComponentParts[] = PKPString::uncamelize($part);
}
array_push($uncamelizedComponentParts, $componentName);
$opName = PKPString::uncamelize($op);
//
// Additional query parameters
//
$additionalParameters = $this->_urlGetAdditionalParameters($request, $params, $escape);
//
// Anchor
//
$anchor = (empty($anchor) ? '' : '#' . rawurlencode($anchor));
//
// Assemble URL
//
// Context, page, operation and additional path go into the path info.
$pathInfoArray = array_merge(
$context,
[COMPONENT_ROUTER_PATHINFO_MARKER],
$uncamelizedComponentParts,
[$opName]
);
// Query parameters
$queryParametersArray = $additionalParameters;
return $this->_urlFromParts($baseUrl, $pathInfoArray, $queryParametersArray, $anchor, $escape);
}
/**
* @copydoc PKPRouter::handleAuthorizationFailure()
*/
public function handleAuthorizationFailure(
$request,
$authorizationMessage,
array $messageParams = []
) {
$translatedAuthorizationMessage = __($authorizationMessage, $messageParams);
// Add the router name and operation if show_stacktrace is enabled.
if (Config::getVar('debug', 'show_stacktrace')) {
$url = $request->getRequestUrl();
$queryString = $request->getQueryString();
if ($queryString) {
$queryString = '?' . $queryString;
}
$translatedAuthorizationMessage .= ' [' . $url . $queryString . ']';
}
// Return a JSON error message.
return new JSONMessage(false, $translatedAuthorizationMessage);
}
//
// Private helper methods
//
/**
* Get the (validated) RPC service endpoint parts from the request.
* If no such RPC service endpoint parts can be retrieved
* then the method returns null.
*
* @param PKPRequest $request the request to be routed
*
* @return ?array a string array with the RPC service endpoint
* parts as values.
*/
public function _getValidatedServiceEndpointParts($request)
{
if ($this->_rpcServiceEndpointParts === false) {
// Mark the internal state variable so this
// will not be called again.
$this->_rpcServiceEndpointParts = null;
// Retrieve service endpoint parts from the request.
if (is_null($rpcServiceEndpointParts = $this->_retrieveServiceEndpointParts($request))) {
// This is not an RPC request
return null;
}
// Validate the service endpoint parts.
if (is_null($rpcServiceEndpointParts = $this->_validateServiceEndpointParts($rpcServiceEndpointParts))) {
// Invalid request
return null;
}
// Assign the validated service endpoint parts
$this->_rpcServiceEndpointParts = $rpcServiceEndpointParts;
}
return $this->_rpcServiceEndpointParts;
}
/**
* Try to retrieve a (non-validated) array with the service
* endpoint parts from the request. See the classdoc for the
* URL patterns supported here.
*
* @param PKPRequest $request the request to be routed
*
* @return ?array an array of (non-validated) service endpoint
* parts or null if the request is not an RPC request.
*/
public function _retrieveServiceEndpointParts($request)
{
if (!isset($_SERVER['PATH_INFO'])) {
return null;
}
$pathInfoParts = explode('/', trim($_SERVER['PATH_INFO'], '/'));
// We expect at least the context + the component
// router marker + 3 component parts (path, handler, operation)
$application = $this->getApplication();
if (count($pathInfoParts) < 5) {
// This path info is too short to be an RPC request
return null;
}
// Check the component router marker
if ($pathInfoParts[1] != COMPONENT_ROUTER_PATHINFO_MARKER) {
// This is not an RPC request
return null;
}
// Remove context and component marker from the array
$rpcServiceEndpointParts = array_slice($pathInfoParts, 2);
return $rpcServiceEndpointParts;
}
/**
* This method pre-validates the service endpoint parts before
* we try to convert them to a file/method name. This also
* converts all parts to lower case.
*
* @param array $rpcServiceEndpointParts
*
* @return ?array the validated service endpoint parts or null if validation
* does not succeed.
*/
public function _validateServiceEndpointParts($rpcServiceEndpointParts)
{
// Do we have data at all?
if (is_null($rpcServiceEndpointParts) || empty($rpcServiceEndpointParts)
|| !is_array($rpcServiceEndpointParts)) {
return null;
}
// We require at least three parts: component directory, handler
// and method name.
if (count($rpcServiceEndpointParts) < 3) {
return null;
}
// Check that the array dimensions remain within sane limits.
if (count($rpcServiceEndpointParts) > COMPONENT_ROUTER_PARTS_MAXDEPTH) {
return null;
}
// Validate the individual endpoint parts.
foreach ($rpcServiceEndpointParts as $key => $rpcServiceEndpointPart) {
// Make sure that none of the elements exceeds the length limit.
$partLen = strlen($rpcServiceEndpointPart);
if ($partLen > COMPONENT_ROUTER_PARTS_MAXLENGTH
|| $partLen < COMPONENT_ROUTER_PARTS_MINLENGTH) {
return null;
}
// Service endpoint URLs are case insensitive.
$rpcServiceEndpointParts[$key] = strtolower_codesafe($rpcServiceEndpointPart);
// We only allow letters, numbers and the hyphen.
if (!PKPString::regexp_match('/^[a-z0-9-]*$/', $rpcServiceEndpointPart)) {
return null;
}
}
return $rpcServiceEndpointParts;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\core\PKPComponentRouter', '\PKPComponentRouter');
}
+390
View File
@@ -0,0 +1,390 @@
<?php
/**
* @file classes/core/PKPContainer.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 PKPContainer
*
* @ingroup core
*
* @brief Bootstraps Laravel services, application-level parts and creates bindings
*/
namespace PKP\core;
use APP\core\AppServiceProvider;
use Exception;
use Illuminate\Config\Repository;
use Illuminate\Container\Container;
use Illuminate\Contracts\Console\Kernel as KernelContract;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Events\EventServiceProvider as LaravelEventServiceProvider;
use Illuminate\Foundation\Console\Kernel;
use Illuminate\Log\LogServiceProvider;
use Illuminate\Queue\Failed\DatabaseFailedJobProvider;
use Illuminate\Support\Facades\Facade;
use PKP\config\Config;
use PKP\i18n\LocaleServiceProvider;
use PKP\proxy\ProxyParser;
use Throwable;
class PKPContainer extends Container
{
/**
* @var string
*
* @brief the base path of the application, needed for base_path helper
*/
protected $basePath;
/**
* @brief Create own container instance, initialize bindings
*/
public function __construct()
{
$this->basePath = BASE_SYS_DIR;
$this->settingProxyForStreamContext();
$this->registerBaseBindings();
$this->registerCoreContainerAliases();
}
/**
* @brief Bind the current container and set it globally
* let helpers, facades and services know to which container refer to
*/
protected function registerBaseBindings()
{
static::setInstance($this);
$this->instance('app', $this);
$this->instance(Container::class, $this);
$this->instance('path', $this->basePath);
$this->singleton(ExceptionHandler::class, function () {
return new class () implements ExceptionHandler {
public function shouldReport(Throwable $e)
{
return true;
}
public function report(Throwable $e)
{
error_log((string) $e->getTraceAsString());
}
public function render($request, Throwable $e)
{
return null;
}
public function renderForConsole($output, Throwable $e)
{
echo (string) $e;
}
};
});
$this->singleton(
KernelContract::class,
Kernel::class
);
$this->singleton('pkpJobQueue', function ($app) {
return new PKPQueueProvider($app);
});
$this->singleton(
'queue.failer',
function ($app) {
return new DatabaseFailedJobProvider(
$app['db'],
config('queue.failed.database'),
config('queue.failed.table')
);
}
);
Facade::setFacadeApplication($this);
}
/**
* @brief Register used service providers within the container
*/
public function registerConfiguredProviders()
{
// Load main settings, this should be done before registering services, e.g., it's used by Database Service
$this->loadConfiguration();
$this->register(new \Illuminate\Cache\CacheServiceProvider($this));
$this->register(new \Illuminate\Filesystem\FilesystemServiceProvider($this));
$this->register(new \ElcoBvg\Opcache\ServiceProvider($this));
$this->register(new LaravelEventServiceProvider($this));
$this->register(new EventServiceProvider($this));
$this->register(new LogServiceProvider($this));
$this->register(new \Illuminate\Database\DatabaseServiceProvider($this));
$this->register(new \Illuminate\Bus\BusServiceProvider($this));
$this->register(new PKPQueueProvider($this));
$this->register(new MailServiceProvider($this));
$this->register(new AppServiceProvider($this));
$this->register(new LocaleServiceProvider($this));
}
/**
* @param \Illuminate\Support\ServiceProvider $provider
*
* @brief Simplified service registration
*/
public function register($provider)
{
$provider->register();
$provider->callBootingCallbacks();
if (method_exists($provider, 'boot')) {
$this->call([$provider, 'boot']);
}
// If there are bindings / singletons set as properties on the provider we
// will spin through them and register them with the application, which
// serves as a convenience layer while registering a lot of bindings.
if (property_exists($provider, 'bindings')) {
foreach ($provider->bindings as $key => $value) {
$this->bind($key, $value);
}
}
if (property_exists($provider, 'singletons')) {
foreach ($provider->singletons as $key => $value) {
$key = is_int($key) ? $value : $key;
$this->singleton($key, $value);
}
}
$provider->callBootedCallbacks();
$this->app->bind('request', fn () => PKPApplication::get()->getRequest());
}
/**
* @brief Bind aliases with contracts
*/
public function registerCoreContainerAliases()
{
foreach ([
'app' => [self::class, \Illuminate\Contracts\Container\Container::class, \Psr\Container\ContainerInterface::class],
'config' => [\Illuminate\Config\Repository::class, \Illuminate\Contracts\Config\Repository::class],
'cache' => [\Illuminate\Cache\CacheManager::class, \Illuminate\Contracts\Cache\Factory::class],
'cache.store' => [\Illuminate\Cache\Repository::class, \Illuminate\Contracts\Cache\Repository::class, \Psr\SimpleCache\CacheInterface::class],
'cache.psr6' => [\Psr\Cache\CacheItemPoolInterface::class],
'db' => [\Illuminate\Database\DatabaseManager::class, \Illuminate\Database\ConnectionResolverInterface::class],
'db.connection' => [\Illuminate\Database\Connection::class, \Illuminate\Database\ConnectionInterface::class],
'files' => [\Illuminate\Filesystem\Filesystem::class],
'filesystem' => [\Illuminate\Filesystem\FilesystemManager::class, \Illuminate\Contracts\Filesystem\Factory::class],
'filesystem.disk' => [\Illuminate\Contracts\Filesystem\Filesystem::class],
'filesystem.cloud' => [\Illuminate\Contracts\Filesystem\Cloud::class],
'maps' => [MapContainer::class, MapContainer::class],
'events' => [\Illuminate\Events\Dispatcher::class, \Illuminate\Contracts\Events\Dispatcher::class],
'queue' => [\Illuminate\Queue\QueueManager::class, \Illuminate\Contracts\Queue\Factory::class, \Illuminate\Contracts\Queue\Monitor::class],
'queue.connection' => [\Illuminate\Contracts\Queue\Queue::class],
'queue.failer' => [\Illuminate\Queue\Failed\FailedJobProviderInterface::class],
'log' => [\Illuminate\Log\LogManager::class, \Psr\Log\LoggerInterface::class],
] as $key => $aliases) {
foreach ($aliases as $alias) {
$this->alias($key, $alias);
}
}
}
/**
* @brief Bind and load container configurations
* usage from Facade, see Illuminate\Support\Facades\Config
*/
protected function loadConfiguration()
{
$items = [];
// Database connection
$driver = 'mysql';
if (substr(strtolower(Config::getVar('database', 'driver')), 0, 8) === 'postgres') {
$driver = 'pgsql';
}
$items['database']['default'] = $driver;
$items['database']['connections'][$driver] = [
'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'),
];
// Queue connection
$items['queue']['default'] = 'database';
$items['queue']['connections']['sync']['driver'] = 'sync';
$items['queue']['connections']['database'] = [
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
'retry_after' => 240,
'after_commit' => true,
];
$items['queue']['failed'] = [
'driver' => 'database',
'database' => $driver,
'table' => 'failed_jobs',
];
// Logging
$items['logging']['channels']['errorlog'] = [
'driver' => 'errorlog',
'level' => 'debug',
];
// Mail Service
$items['mail']['mailers']['sendmail'] = [
'transport' => 'sendmail',
'path' => Config::getVar('email', 'sendmail_path'),
];
$items['mail']['mailers']['smtp'] = [
'transport' => 'smtp',
'host' => Config::getVar('email', 'smtp_server'),
'port' => Config::getVar('email', 'smtp_port'),
'encryption' => Config::getVar('email', 'smtp_auth'),
'username' => Config::getVar('email', 'smtp_username'),
'password' => Config::getVar('email', 'smtp_password'),
'verify_peer' => !Config::getVar('email', 'smtp_suppress_cert_check'),
];
$items['mail']['mailers']['log'] = [
'transport' => 'log',
'channel' => 'errorlog',
];
$items['mail']['mailers']['phpmailer'] = [
'transport' => 'phpmailer',
];
$items['mail']['default'] = static::getDefaultMailer();
// Cache configuration
$items['cache'] = [
'default' => 'opcache',
'stores' => [
'opcache' => [
'driver' => 'opcache',
'path' => Core::getBaseDir() . '/cache/opcache'
]
]
];
// Create instance and bind to use globally
$this->instance('config', new Repository($items));
}
/**
* @param string $path appended to the base path
*
* @brief see Illuminate\Foundation\Application::basePath
*/
public function basePath($path = '')
{
return $this->basePath . ($path ? "/{$path}" : $path);
}
/**
* @param string $path appended to the path
*
* @brief alias of basePath(), Laravel app path differs from installation path
*/
public function path($path = '')
{
return $this->basePath($path);
}
/**
* Retrieves default mailer driver depending on the configuration
*
* @throws Exception
*/
protected static function getDefaultMailer(): string
{
$default = Config::getVar('general', 'sandbox', false)
? 'log'
: Config::getVar('email', 'default');
if (!$default) {
throw new Exception('Mailer driver isn\'t specified in the application\'s config');
}
return $default;
}
/**
* Setting a proxy on the stream_context_set_default when configuration [proxy] is filled
*/
protected function settingProxyForStreamContext(): void
{
$proxy = new ProxyParser();
if ($httpProxy = Config::getVar('proxy', 'http_proxy')) {
$proxy->parseFQDN($httpProxy);
}
if ($httpsProxy = Config::getVar('proxy', 'https_proxy')) {
$proxy->parseFQDN($httpsProxy);
}
if ($proxy->isEmpty()) {
return;
}
/**
* `Connection close` here its to avoid slowness. More info at https://www.php.net/manual/en/context.http.php#114867
* `request_fulluri` its related to avoid proxy errors. More info at https://www.php.net/manual/en/context.http.php#110449
*/
$opts = [
'http' => [
'protocol_version' => 1.1,
'header' => [
'Connection: close',
],
'proxy' => $proxy->getProxy(),
'request_fulluri' => true,
],
];
if ($proxy->getAuth()) {
$opts['http']['header'][] = 'Proxy-Authorization: Basic ' . $proxy->getAuth();
}
$context = stream_context_create($opts);
stream_context_set_default($opts);
libxml_set_streams_context($context);
}
/**
* Override Laravel method; always false.
* Prevents the undefined method error when the Log Manager tries to determine the driver
*
* @return bool
*/
public function runningUnitTests()
{
return false;
}
/**
* Determine if the application is currently down for maintenance.
*
* @return bool
*/
public function isDownForMaintenance()
{
return PKPApplication::isUnderMaintenance();
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\core\PKPContainer', '\PKPContainer');
}
+506
View File
@@ -0,0 +1,506 @@
<?php
/**
* @file classes/core/PKPPageRouter.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 PKPPageRouter
*
* @ingroup core
*
* @brief Class mapping an HTTP request to a handler or context.
*/
namespace PKP\core;
use APP\core\Application;
use APP\facades\Repo;
use PKP\facades\Locale;
use PKP\plugins\Hook;
use PKP\security\Role;
use PKP\security\Validation;
use PKP\session\SessionManager;
define('ROUTER_DEFAULT_PAGE', './pages/index/index.php');
define('ROUTER_DEFAULT_OP', 'index');
class PKPPageRouter extends PKPRouter
{
/** @var array pages that don't need an installed system to be displayed */
public $_installationPages = ['install', 'help', 'header', 'sidebar'];
//
// Internal state cache variables
// NB: Please do not access directly but
// only via their respective getters/setters
//
/** @var string the requested page */
public $_page;
/** @var string the requested operation */
public $_op;
/** @var string cache filename */
public $_cacheFilename;
/**
* get the installation pages
*
* @return array
*/
public function getInstallationPages()
{
return $this->_installationPages;
}
/**
* get the cacheable pages
*
* @return array
*/
public function getCacheablePages()
{
// Can be overridden by sub-classes.
return [];
}
/**
* Determine whether or not the request is cacheable.
*
* @param PKPRequest $request
* @param bool $testOnly required for unit test to
* bypass session check.
*
*/
public function isCacheable($request, $testOnly = false): bool
{
if (SessionManager::isDisabled() && !$testOnly) {
return false;
}
if (Application::isUnderMaintenance()) {
return false;
}
if (!empty($_POST) || Validation::isLoggedIn()) {
return false;
}
if (!empty($_GET)) {
return false;
}
if (in_array($this->getRequestedPage($request), $this->getCacheablePages())) {
return true;
}
return false;
}
/**
* Get the page requested in the URL.
*
* @param PKPRequest $request the request to be routed
*
* @return string the page path (under the "pages" directory)
*/
public function getRequestedPage($request)
{
if (!isset($this->_page)) {
$this->_page = $this->_getRequestedUrlParts(['Core', 'getPage'], $request);
}
return $this->_page;
}
/**
* Get the operation requested in the URL (assumed to exist in the requested page handler).
*
* @param PKPRequest $request the request to be routed
*
* @return string
*/
public function getRequestedOp($request)
{
if (!isset($this->_op)) {
$this->_op = $this->_getRequestedUrlParts(['Core', 'getOp'], $request);
}
return $this->_op;
}
/**
* Get the arguments requested in the URL.
*
* @param PKPRequest $request the request to be routed
*
* @return array
*/
public function getRequestedArgs($request)
{
return $this->_getRequestedUrlParts(['Core', 'getArgs'], $request);
}
/**
* Get the anchor (#anchor) requested in the URL
*
* @para $request PKPRequest the request to be routed
*
* @return string
*/
public function getRequestedAnchor($request)
{
$url = $request->getRequestUrl();
$parts = explode('#', $url);
if (count($parts) < 2) {
return '';
}
return $parts[1];
}
//
// Implement template methods from PKPRouter
//
/**
* @copydoc PKPRouter::getCacheFilename()
*/
public function getCacheFilename($request)
{
if (!isset($this->_cacheFilename)) {
$id = $_SERVER['PATH_INFO'] ?? 'index';
$id .= '-' . Locale::getLocale();
$path = Core::getBaseDir();
$this->_cacheFilename = $path . '/cache/wc-' . md5($id) . '.html';
}
return $this->_cacheFilename;
}
/**
* @copydoc PKPRouter::route()
*/
public function route($request)
{
// Determine the requested page and operation
$page = $this->getRequestedPage($request);
$op = $this->getRequestedOp($request);
// If the application has not yet been installed we only
// allow installer pages to be displayed.
if (!Application::isInstalled()) {
if (!in_array($page, $this->getInstallationPages())) {
// A non-installation page was called although
// the system is not yet installed. Redirect to
// the installation page.
$request->redirect('index', 'install');
}
}
// Redirect requests from logged-out users to a context which is not
// publicly enabled
if (!SessionManager::isDisabled()) {
$user = $request->getUser();
$currentContext = $request->getContext();
if ($currentContext && !$currentContext->getEnabled() && !$user instanceof \PKP\user\User) {
if ($page != 'login') {
$request->redirect(null, 'login');
}
}
}
// Determine the page index file. This file contains the
// logic to resolve a page to a specific handler class.
$sourceFile = sprintf('pages/%s/index.php', $page);
// If a hook has been registered to handle this page, give it the
// opportunity to load required resources and set the handler.
$handler = null;
if (!Hook::call('LoadHandler', [&$page, &$op, &$sourceFile, &$handler])) {
if (file_exists($sourceFile)) {
$result = require('./' . $sourceFile);
if (is_object($result)) {
$handler = $result;
}
} elseif (file_exists(PKP_LIB_PATH . "/{$sourceFile}")) {
$result = require('./' . PKP_LIB_PATH . "/{$sourceFile}");
if (is_object($result)) {
$handler = $result;
}
} elseif (empty($page)) {
require(ROUTER_DEFAULT_PAGE);
} else {
$dispatcher = $this->getDispatcher();
$dispatcher->handle404();
}
}
if (!SessionManager::isDisabled()) {
// Initialize session
SessionManager::getManager();
}
// Call the selected handler's index operation if
// no operation was defined in the request.
if (empty($op)) {
$op = ROUTER_DEFAULT_OP;
}
// Redirect to 404 if the operation doesn't exist
// for the handler.
$methods = [];
if ($handler) {
$methods = get_class_methods($handler);
} elseif (defined('HANDLER_CLASS')) {
// The use of HANDLER_CLASS is DEPRECATED with 3.4.0 pkp/pkp-lib#6019
$methods = get_class_methods(HANDLER_CLASS);
}
if (!in_array($op, $methods)) {
$dispatcher = $this->getDispatcher();
$dispatcher->handle404();
}
// Instantiate the handler class
if (!$handler) {
// The use of HANDLER_CLASS is DEPRECATED with 3.4.0 pkp/pkp-lib#6019
$handlerClass = HANDLER_CLASS;
$handler = new $handlerClass($request);
}
$this->setHandler($handler);
// Authorize and initialize the request but don't call the
// validate() method on page handlers.
// FIXME: We should call the validate() method for page
// requests also (last param = true in the below method
// call) once we've made sure that all validate() calls can
// be removed from handler operations without damage (i.e.
// they don't depend on actions being performed before the
// call to validate().
$args = $this->getRequestedArgs($request);
$serviceEndpoint = [$handler, $op];
$this->_authorizeInitializeAndCallRequest($serviceEndpoint, $request, $args, false);
}
/**
* @copydoc PKPRouter::url()
*
* @param null|mixed $newContext
* @param null|mixed $page
* @param null|mixed $op
* @param null|mixed $path
* @param null|mixed $params
* @param null|mixed $anchor
*/
public function url(
PKPRequest $request,
?string $newContext = null,
$page = null,
$op = null,
$path = null,
$params = null,
$anchor = null,
$escape = false
) {
//
// Base URL and Context
//
$baseUrlAndContext = $this->_urlGetBaseAndContext($request, $newContext);
$baseUrl = array_shift($baseUrlAndContext);
$context = array_shift($baseUrlAndContext);
//
// Additional path info
//
if (empty($path)) {
$additionalPath = [];
} else {
if (is_array($path)) {
$additionalPath = array_map('rawurlencode', $path);
} else {
$additionalPath = [rawurlencode($path)];
}
}
//
// Page and Operation
//
// Are we in a page request?
$currentRequestIsAPageRequest = $request->getRouter() instanceof \PKP\core\PKPPageRouter;
// Determine the operation
if ($op) {
// If an operation has been explicitly set then use it.
$op = rawurlencode($op);
} else {
// No operation has been explicitly set so let's determine a sensible
// default operation.
if (empty($newContext) && empty($page) && $currentRequestIsAPageRequest) {
// If we remain in the existing context and on the existing page then
// we will default to the current operation. We can only determine a
// current operation if the current request is a page request.
$op = $this->getRequestedOp($request);
} else {
// If a new context (or page) has been set then we'll default to the
// index operation within the new context (or on the new page).
if (empty($additionalPath)) {
// If no additional path is set we can simply leave the operation
// undefined which automatically defaults to the index operation
// but gives shorter (=nicer) URLs.
$op = null;
} else {
// If an additional path is set then we have to explicitly set the
// index operation to disambiguate the path info.
$op = 'index';
}
}
}
// Determine the page
if ($page) {
// If a page has been explicitly set then use it.
$page = rawurlencode($page);
} else {
// No page has been explicitly set so let's determine a sensible default page.
if (empty($newContext) && $currentRequestIsAPageRequest) {
// If we remain in the existing context then we will default to the current
// page. We can only determine a current page if the current request is a
// page request.
$page = $this->getRequestedPage($request);
} else {
// If a new context has been set then we'll default to the index page
// within the new context.
if (empty($op)) {
// If no explicit operation is set we can simply leave the page
// undefined which automatically defaults to the index page but gives
// shorter (=nicer) URLs.
$page = null;
} else {
// If an operation is set then we have to explicitly set the index
// page to disambiguate the path info.
$page = 'index';
}
}
}
//
// Additional query parameters
//
$additionalParameters = $this->_urlGetAdditionalParameters($request, $params, $escape);
//
// Anchor
//
$anchor = (empty($anchor) ? '' : '#' . preg_replace("/[^a-zA-Z0-9\-\_\/\.\~]/", '', $anchor));
//
// Assemble URL
//
// Context, page, operation and additional path go into the path info.
$pathInfoArray = $context;
if (!empty($page)) {
$pathInfoArray[] = $page;
if (!empty($op)) {
$pathInfoArray[] = $op;
}
}
$pathInfoArray = array_merge($pathInfoArray, $additionalPath);
// Query parameters
$queryParametersArray = $additionalParameters;
return $this->_urlFromParts($baseUrl, $pathInfoArray, $queryParametersArray, $anchor, $escape);
}
/**
* @copydoc PKPRouter::handleAuthorizationFailure()
*/
public function handleAuthorizationFailure(
$request,
$authorizationMessage,
array $messageParams = []
) {
// Redirect to the authorization denied page.
if (!$request->getUser()) {
Validation::redirectLogin();
}
$request->redirect(null, 'user', 'authorizationDenied', null, ['message' => $authorizationMessage]);
}
/**
* Redirect to user home page (or the user group home page if the user has one user group).
*/
public function redirectHome(PKPRequest $request)
{
$request->redirectUrl($this->getHomeUrl($request));
}
/**
* Get the user's "home" page URL (e.g. where they are sent after login).
*
* @param PKPRequest $request the request to be routed
*/
public function getHomeUrl($request)
{
$user = $request->getUser();
$userId = $user->getId();
if ($context = $this->getContext($request)) {
// If the user has no roles, or only one role and this is reader, go to "Index" page.
// Else go to "submissions" page
$userGroups = Repo::userGroup()->userUserGroups($userId, $context->getId());
if ($userGroups->isEmpty()
|| ($userGroups->count() == 1 && $userGroups->first()->getRoleId() == Role::ROLE_ID_READER)
) {
return $request->url(null, 'index');
}
return $request->url(null, 'submissions');
} else {
// The user is at the site context, check to see if they are
// only registered in one place w/ one role
$userGroups = Repo::userGroup()->userUserGroups($userId, \PKP\core\PKPApplication::CONTEXT_ID_NONE);
if ($userGroups->count() == 1) {
$firstUserGroup = $userGroups->first();
$contextDao = Application::getContextDAO();
$context = $contextDao->getById($firstUserGroup->getContextId());
if (!isset($context)) {
$request->redirect('index', 'index');
}
if ($firstUserGroup->getRoleId() == Role::ROLE_ID_READER) {
$request->redirect(null, 'index');
}
}
return $request->url('index', 'index');
}
}
//
// Private helper methods.
//
/**
* Retrieve part of the current requested
* url using the passed callback method.
*
* @param array $callback Core method to retrieve
* page, operation or arguments from url.
* @param PKPRequest $request
*
* @return array|string|null
*/
private function _getRequestedUrlParts($callback, &$request)
{
$url = null;
assert($request->getRouter() instanceof \PKP\core\PKPPageRouter);
if (isset($_SERVER['PATH_INFO'])) {
$url = $_SERVER['PATH_INFO'];
}
$userVars = $request->getUserVars();
return call_user_func_array($callback, [$url, true, $userVars]);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\core\PKPPageRouter', '\PKPPageRouter');
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/**
* @file classes/core/PKPQueueDatabaseConnector.php
*
* Copyright (c) 2014-2023 Simon Fraser University
* Copyright (c) 2000-2023 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPQueueDatabaseConnector
*
* @ingroup core
*
* @brief Registers the database queue connector
*/
namespace PKP\core;
use Illuminate\Queue\Connectors\DatabaseConnector as IlluminateQueueDatabaseConnector;
use Illuminate\Queue\DatabaseQueue;
use PKP\config\Config;
class PKPQueueDatabaseConnector extends IlluminateQueueDatabaseConnector
{
/**
* Establish a queue connection.
*
* @return \Illuminate\Contracts\Queue\Queue
*/
public function connect(array $config)
{
return new DatabaseQueue(
$this->connections->connection($config['connection'] ?? null),
$config['table'],
Config::getVar('queues', 'default_queue', 'queue'),
$config['retry_after'] ?? 60,
$config['after_commit'] ?? null
);
}
}
+202
View File
@@ -0,0 +1,202 @@
<?php
declare(strict_types=1);
/**
* @file classes/core/PKPQueueProvider.php
*
* Copyright (c) 2014-2023 Simon Fraser University
* Copyright (c) 2000-2023 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPQueueProvider
*
* @ingroup core
*
* @brief Registers Events Service Provider and boots data on events and their listeners
*/
namespace PKP\core;
use APP\core\Application;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Queue\QueueServiceProvider as IlluminateQueueServiceProvider;
use Illuminate\Queue\Worker;
use Illuminate\Queue\WorkerOptions;
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\Facades\Queue;
use PKP\config\Config;
use PKP\job\models\Job as PKPJobModel;
use PKP\queue\JobRunner;
use PKP\queue\WorkerConfiguration;
class PKPQueueProvider extends IlluminateQueueServiceProvider
{
/**
* Specific queue to target to run the associated jobs
*/
protected ?string $queue = null;
/**
* Set a specific queue to target to run the associated jobs
*/
public function forQueue(string $queue): self
{
$this->queue = $queue;
return $this;
}
/**
* Get a job model builder instance to query the jobs table
*/
public function getJobModelBuilder(): Builder
{
return PKPJobModel::isAvailable()
->nonEmptyQueue()
->when($this->queue, fn ($query) => $query->onQueue($this->queue))
->when(is_null($this->queue), fn ($query) => $query->notQueue(PKPJobModel::TESTING_QUEUE))
->notExceededAttempts();
}
/**
* Get the worker options object
*/
public function getWorkerOptions(array $options = []): WorkerOptions
{
return new WorkerOptions(
...array_values(WorkerConfiguration::withOptions($options)->getWorkerOptions())
);
}
/**
* Run the queue worker via an infinite loop daemon
*/
public function runJobsViaDaemon(string $connection, string $queue, array $workerOptions = []): void
{
$worker = PKPContainer::getInstance()['queue.worker']; /** @var \Illuminate\Queue\Worker $worker */
$worker
->setCache(app()->get('cache.store'))
->daemon(
$connection,
$queue,
$this->getWorkerOptions($workerOptions)
);
}
/**
* Run the queue worker to process queue the jobs
*/
public function runJobInQueue(): void
{
$job = $this->getJobModelBuilder()->limit(1)->first();
if ($job === null) {
return;
}
$laravelContainer = PKPContainer::getInstance();
$laravelContainer['queue.worker']->runNextJob(
'database',
$job->queue ?? Config::getVar('queues', 'default_queue', 'queue'),
$this->getWorkerOptions()
);
}
/**
* Bootstrap any application services.
*
*/
public function boot()
{
if (Config::getVar('queues', 'job_runner', true)) {
register_shutdown_function(function () {
// As this runs at the current request's end but the 'register_shutdown_function' registered
// at the service provider's registration time at application initial bootstrapping,
// need to check the maintenance status within the 'register_shutdown_function'
if (Application::get()->isUnderMaintenance()) {
return;
}
if (Config::getVar('general', 'sandbox', false)) {
error_log(__('admin.cli.tool.jobs.sandbox.message'));
return;
}
(new JobRunner($this))
->withMaxExecutionTimeConstrain()
->withMaxJobsConstrain()
->withMaxMemoryConstrain()
->withEstimatedTimeToProcessNextJobConstrain()
->processJobs();
});
}
Queue::failing(function (JobFailed $event) {
error_log($event->exception->__toString());
app('queue.failer')->log(
$event->connectionName,
$event->job->getQueue(),
$event->job->getRawBody(),
json_encode([
'message' => $event->exception->getMessage(),
'code' => $event->exception->getCode(),
'file' => $event->exception->getFile(),
'line' => $event->exception->getLine(),
'trace' => $event->exception->getTrace(),
])
);
});
}
/**
* Register the database queue connector.
*
* @param \Illuminate\Queue\QueueManager $manager
*/
protected function registerDatabaseConnector($manager)
{
$manager->addConnector('database', function () {
return new PKPQueueDatabaseConnector($this->app['db']);
});
}
/**
* Register the queue worker.
*
*/
protected function registerWorker()
{
$this->app->singleton('queue.worker', function ($app) {
$isDownForMaintenance = function () {
return $this->app->isDownForMaintenance();
};
$resetScope = function () use ($app) {
if (method_exists($app['db'], 'getConnections')) {
foreach ($app['db']->getConnections() as $connection) {
$connection->resetTotalQueryDuration();
$connection->allowQueryDurationHandlersToRunAgain();
}
}
$app->forgetScopedInstances();
return Facade::clearResolvedInstances();
};
return new Worker(
$app['queue'],
$app['events'],
$app[ExceptionHandler::class],
$isDownForMaintenance,
$resetScope
);
});
}
}
+884
View File
@@ -0,0 +1,884 @@
<?php
/**
* @file classes/core/PKPRequest.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 PKPRequest
*
* @ingroup core
*
* @brief Class providing operations associated with HTTP requests.
*/
namespace PKP\core;
use APP\core\Application;
use APP\facades\Repo;
use APP\file\PublicFileManager;
use PKP\config\Config;
use PKP\context\Context;
use PKP\db\DAORegistry;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\Validation;
use PKP\session\Session;
use PKP\session\SessionManager;
use PKP\site\Site;
use PKP\site\SiteDAO;
use PKP\user\User;
class PKPRequest
{
//
// Internal state - please do not reference directly
//
/** @var PKPRouter router instance used to route this request */
public $_router = null;
/** @var Dispatcher dispatcher instance used to dispatch this request */
public $_dispatcher = null;
/** @var array the request variables cache (GET/POST) */
public $_requestVars = null;
/** @var string request base path */
public $_basePath;
/** @var string request path */
public $_requestPath;
/** @var bool true if restful URLs are enabled in the config */
public $_isRestfulUrlsEnabled;
/** @var string server host */
public $_serverHost;
/** @var string request protocol */
public $_protocol;
/** @var bool bot flag */
public $_isBot;
/** @var string user agent */
public $_userAgent;
/**
* get the router instance
*
* @return PKPRouter
*/
public function &getRouter()
{
return $this->_router;
}
/**
* set the router instance
*
* @param PKPRouter $router
*/
public function setRouter($router)
{
$this->_router = $router;
}
/**
* Set the dispatcher
*
* @param Dispatcher $dispatcher
*/
public function setDispatcher($dispatcher)
{
$this->_dispatcher = $dispatcher;
}
/**
* Get the dispatcher
*
* @return Dispatcher
*/
public function &getDispatcher()
{
if (! $this->_dispatcher) {
$application = Application::get();
$this->setDispatcher($application->getDispatcher());
}
return $this->_dispatcher;
}
/**
* Perform an HTTP redirect to an absolute or relative (to base system URL) URL.
*
* @param string $url (exclude protocol for local redirects)
*/
public function redirectUrl($url)
{
if (Hook::call('Request::redirect', [&$url])) {
return;
}
header("Location: {$url}");
exit;
}
/**
* Request an HTTP redirect via JSON to be used from components.
*
* @param string $url
*
* @return JSONMessage
*/
public function redirectUrlJson($url)
{
$json = new JSONMessage(true);
$json->setEvent('redirectRequested', $url);
return $json;
}
/**
* Redirect to the current URL, forcing the HTTPS protocol to be used.
*/
public function redirectSSL()
{
// Note that we are intentionally skipping PKP processing of REQUEST_URI and QUERY_STRING for a protocol redirect
// This processing is deferred to the redirected (target) URI
$url = 'https://' . $this->getServerHost() . $_SERVER['REQUEST_URI'];
$queryString = $_SERVER['QUERY_STRING'];
if (!empty($queryString)) {
$url .= "?{$queryString}";
}
$this->redirectUrl($url);
}
/**
* Redirect to the current URL, forcing the HTTP protocol to be used.
*/
public function redirectNonSSL()
{
// Note that we are intentionally skipping PKP processing of REQUEST_URI and QUERY_STRING for a protocol redirect
// This processing is deferred to the redirected (target) URI
$url = 'http://' . $this->getServerHost() . $_SERVER['REQUEST_URI'];
$queryString = $_SERVER['QUERY_STRING'];
if (!empty($queryString)) {
$url .= "?{$queryString}";
}
$this->redirectUrl($url);
}
/**
* Get the IF_MODIFIED_SINCE date (as a numerical timestamp) if available
*
* @return ?int
*/
public function getIfModifiedSince()
{
if (!isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
return null;
}
return strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
}
/**
* Get the base URL of the request (excluding script).
*
* @param bool $allowProtocolRelative True iff protocol-relative URLs are allowed
*
* @return string
*/
public function getBaseUrl($allowProtocolRelative = false)
{
$serverHost = $this->getServerHost(false);
if ($serverHost !== false) {
// Auto-detection worked.
if ($allowProtocolRelative) {
$baseUrl = '//' . $this->getServerHost() . $this->getBasePath();
} else {
$baseUrl = $this->getProtocol() . '://' . $this->getServerHost() . $this->getBasePath();
}
} else {
// Auto-detection didn't work (e.g. this is a command-line call); use configuration param
$baseUrl = Config::getVar('general', 'base_url');
}
Hook::call('Request::getBaseUrl', [&$baseUrl]);
return $baseUrl;
}
/**
* Get the base path of the request (excluding trailing slash).
*
* @return string
*/
public function getBasePath()
{
if (!isset($this->_basePath)) {
// Strip the PHP filename off of the script's executed path
// We expect the SCRIPT_NAME to look like /path/to/file.php
// If the SCRIPT_NAME ends in /, assume this is the directory and the script's actual name
// is masked as the DirectoryIndex
// If the SCRIPT_NAME ends in neither / or .php, assume the the script's actual name is masked
// and we need to avoid stripping the terminal directory
$path = preg_replace('#/[^/]*$#', '', $_SERVER['SCRIPT_NAME'] . (substr($_SERVER['SCRIPT_NAME'], -1) == '/' || preg_match('#.php$#i', $_SERVER['SCRIPT_NAME']) ? '' : '/'));
// Encode characters which need to be encoded in a URL.
// Simply using rawurlencode() doesn't work because it
// also encodes characters which are valid in a URL (i.e. @, $).
$parts = explode('/', $path);
foreach ($parts as $i => $part) {
$pieces = array_map([$this, 'encodeBasePathFragment'], str_split($part));
$parts[$i] = implode('', $pieces);
}
$this->_basePath = implode('/', $parts);
if ($this->_basePath == '/' || $this->_basePath == '\\') {
$this->_basePath = '';
}
Hook::call('Request::getBasePath', [&$this->_basePath]);
}
return $this->_basePath;
}
/**
* Callback function for getBasePath() to correctly encode (or not encode)
* a basepath fragment.
*
* @param string $fragment
*
* @return string
*/
public function encodeBasePathFragment($fragment)
{
if (!preg_match('/[A-Za-z0-9-._~!$&\'()*+,;=:@]/', $fragment)) {
return rawurlencode($fragment);
}
return $fragment;
}
/**
* Deprecated
*
* @see PKPPageRouter::getIndexUrl()
*/
public function getIndexUrl()
{
static $indexUrl;
if (!isset($indexUrl)) {
$indexUrl = $this->_delegateToRouter('getIndexUrl');
// Call legacy hook
Hook::call('Request::getIndexUrl', [&$indexUrl]);
}
return $indexUrl;
}
/**
* Get the complete URL to this page, including parameters.
*
* @return string
*/
public function getCompleteUrl()
{
static $completeUrl;
if (!isset($completeUrl)) {
$completeUrl = $this->getRequestUrl();
$queryString = $this->getQueryString();
if (!empty($queryString)) {
$completeUrl .= "?{$queryString}";
}
Hook::call('Request::getCompleteUrl', [&$completeUrl]);
}
return $completeUrl;
}
/**
* Get the complete URL of the request.
*
* @return string
*/
public function getRequestUrl()
{
static $requestUrl;
if (!isset($requestUrl)) {
$requestUrl = $this->getProtocol() . '://' . $this->getServerHost() . $this->getRequestPath();
Hook::call('Request::getRequestUrl', [&$requestUrl]);
}
return $requestUrl;
}
/**
* Get the complete set of URL parameters to the current request.
*
* @return string
*/
public function getQueryString()
{
static $queryString;
if (!isset($queryString)) {
$queryString = $_SERVER['QUERY_STRING'] ?? '';
Hook::call('Request::getQueryString', [&$queryString]);
}
return $queryString;
}
/**
* Get the complete set of URL parameters to the current request as an
* associative array.
*
* @return array
*/
public function getQueryArray()
{
$queryString = $this->getQueryString();
$queryArray = [];
if (isset($queryString)) {
parse_str($queryString, $queryArray);
}
return $queryArray;
}
/**
* Get the completed path of the request.
*
* @return string
*/
public function getRequestPath()
{
if (!isset($this->_requestPath)) {
if ($this->isRestfulUrlsEnabled()) {
$this->_requestPath = $this->getBasePath();
} else {
$this->_requestPath = $_SERVER['SCRIPT_NAME'] ?? '';
}
$this->_requestPath .= $_SERVER['PATH_INFO'] ?? '';
Hook::call('Request::getRequestPath', [&$this->_requestPath]);
}
return $this->_requestPath;
}
/**
* Get the server hostname in the request.
*
* @param string $default Default hostname (defaults to localhost)
* @param bool $includePort Whether to include non-standard port number; default true
*
* @return string
*/
public function getServerHost($default = null, $includePort = true)
{
if ($default === null) {
$default = 'localhost';
}
if (!isset($this->_serverHost)) {
$this->_serverHost = $_SERVER['HTTP_X_FORWARDED_HOST']
?? ($_SERVER['HTTP_HOST']
?? ($_SERVER['SERVER_NAME']
?? $default));
// in case of multiple host entries in the header (e.g. multiple reverse proxies) take the first entry
$this->_serverHost = strtok($this->_serverHost, ',');
Hook::call('Request::getServerHost', [&$this->_serverHost, &$default, &$includePort]);
}
if (!$includePort) {
// Strip the port number, if one is included. (#3912)
return preg_replace("/:\d*$/", '', $this->_serverHost);
}
return $this->_serverHost;
}
/**
* Get the protocol used for the request (HTTP or HTTPS).
*
* @return string
*/
public function getProtocol()
{
if (!isset($this->_protocol)) {
$this->_protocol = (!isset($_SERVER['HTTPS']) || strtolower_codesafe($_SERVER['HTTPS']) != 'on') ? 'http' : 'https';
Hook::call('Request::getProtocol', [&$this->_protocol]);
}
return $this->_protocol;
}
/**
* Get the request method
*
* @return string
*/
public function getRequestMethod()
{
return ($_SERVER['REQUEST_METHOD'] ?? '');
}
/**
* Determine whether the request is a POST request
*
* @return bool
*/
public function isPost()
{
return ($this->getRequestMethod() == 'POST');
}
/**
* Determine whether the request is a GET request
*
* @return bool
*/
public function isGet()
{
return ($this->getRequestMethod() == 'GET');
}
/**
* Determine whether a CSRF token is present and correct.
*
* @return bool
*/
public function checkCSRF()
{
$session = $this->getSession();
return $this->getUserVar('csrfToken') == $session->getCSRFToken();
}
/**
* Get the remote IP address of the current request.
*
* @return string
*/
public function getRemoteAddr()
{
$ipaddr = & Registry::get('remoteIpAddr'); // Reference required.
if (is_null($ipaddr)) {
if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) &&
Config::getVar('general', 'trust_x_forwarded_for', true) &&
preg_match_all('/([0-9.a-fA-F:]+)/', $_SERVER['HTTP_X_FORWARDED_FOR'], $matches)) {
} elseif (isset($_SERVER['REMOTE_ADDR']) &&
preg_match_all('/([0-9.a-fA-F:]+)/', $_SERVER['REMOTE_ADDR'], $matches)) {
} elseif (preg_match_all('/([0-9.a-fA-F:]+)/', getenv('REMOTE_ADDR'), $matches)) {
} else {
$ipaddr = '';
}
if (!isset($ipaddr)) {
// If multiple addresses are listed, take the last. (Supports ipv6.)
$ipaddr = $matches[0][count($matches[0]) - 1];
}
Hook::call('Request::getRemoteAddr', [&$ipaddr]);
}
return $ipaddr;
}
/**
* Get the remote domain of the current request
*
* @return string
*/
public function getRemoteDomain()
{
static $remoteDomain;
if (!isset($remoteDomain)) {
$remoteDomain = null;
$remoteDomain = @getHostByAddr($this->getRemoteAddr());
Hook::call('Request::getRemoteDomain', [&$remoteDomain]);
}
return $remoteDomain;
}
/**
* Get the user agent of the current request.
*
* @return string
*/
public function getUserAgent()
{
if (!isset($this->_userAgent)) {
if (isset($_SERVER['HTTP_USER_AGENT'])) {
$this->_userAgent = $_SERVER['HTTP_USER_AGENT'];
}
if (!isset($this->_userAgent) || empty($this->_userAgent)) {
$this->_userAgent = getenv('HTTP_USER_AGENT');
}
if (!isset($this->_userAgent) || $this->_userAgent == false) {
$this->_userAgent = '';
}
Hook::call('Request::getUserAgent', [&$this->_userAgent]);
}
return $this->_userAgent;
}
/**
* Determine whether the user agent is a bot or not.
*
* @return bool
*/
public function isBot()
{
if (!isset($this->_isBot)) {
$userAgent = $this->getUserAgent();
$this->_isBot = Core::isUserAgentBot($userAgent);
}
return $this->_isBot;
}
/**
* Check if the HTTP_DNT (Do Not Track) is set
*/
public function getDoNotTrack(): bool
{
return (array_key_exists('HTTP_DNT', $_SERVER) && ((int) $_SERVER['HTTP_DNT'] === 1));
}
/**
* Return true if RESTFUL_URLS is enabled.
*/
public function isRestfulUrlsEnabled()
{
if (!isset($this->_isRestfulUrlsEnabled)) {
$this->_isRestfulUrlsEnabled = Config::getVar('general', 'restful_urls') ? true : false;
}
return $this->_isRestfulUrlsEnabled;
}
/**
* Get site data.
*
*/
public function getSite(): ?Site
{
$site = & Registry::get('site', true, null);
/** @var SiteDAO */
$siteDao = DAORegistry::getDAO('SiteDAO');
return $site ??= $siteDao->getSite();
}
/**
* Get the user session associated with the current request.
*/
public function getSession(): Session
{
$session = & Registry::get('session', true, null);
return $session ??= SessionManager::getManager()->getUserSession();
}
/**
* Get the user associated with the current request.
*/
public function getUser(): ?User
{
$user = & Registry::get('user', true, null);
if ($user) {
return $user;
}
// Attempt to load user from API token
if (($handler = $this->getRouter()->getHandler())
&& ($token = $handler->getApiToken())
&& ($apiUser = Repo::user()->getByApiKey($token))
&& $apiUser->getData('apiKeyEnabled')
) {
return $user = $apiUser;
}
// Attempts to retrieve a logged user
if (Validation::isLoggedIn()) {
$user = SessionManager::getManager()->getUserSession()->getUser();
}
return $user;
}
/**
* Get the value of a GET/POST variable.
*/
public function getUserVar($key)
{
// special treatment for APIRouter. APIHandler gets to fetch parameter first
$router = $this->getRouter();
if ($router instanceof \PKP\core\APIRouter && (!is_null($handler = $router->getHandler()))) {
/** @var APIHandler */
$handler = $router->getHandler();
$value = $handler->getParameter($key);
if (!is_null($value)) {
return $value;
}
}
// Get all vars (already cleaned)
$vars = $this->getUserVars();
return $vars[$key] ?? null;
}
/**
* Get all GET/POST variables as an array
*
* @return array
*/
public function &getUserVars()
{
$this->_requestVars ??= array_map(fn ($s) => is_string($s) ? trim($s) : $s, array_merge($_GET, $_POST));
return $this->_requestVars;
}
/**
* Get the value of a GET/POST variable generated using the Smarty
* html_select_date and/or html_select_time function.
*
* @param string $prefix
* @param int $defaultDay
* @param int $defaultMonth
* @param int $defaultYear
* @param int $defaultHour
* @param int $defaultMinute
* @param int $defaultSecond
*
* @return ?int Linux timestamp
*/
public function getUserDateVar($prefix, $defaultDay = null, $defaultMonth = null, $defaultYear = null, $defaultHour = 0, $defaultMinute = 0, $defaultSecond = 0)
{
$monthPart = $this->getUserVar($prefix . 'Month');
$dayPart = $this->getUserVar($prefix . 'Day');
$yearPart = $this->getUserVar($prefix . 'Year');
$hourPart = $this->getUserVar($prefix . 'Hour');
$minutePart = $this->getUserVar($prefix . 'Minute');
$secondPart = $this->getUserVar($prefix . 'Second');
switch ($this->getUserVar($prefix . 'Meridian')) {
case 'pm':
if (is_numeric($hourPart) && $hourPart != 12) {
$hourPart += 12;
}
break;
case 'am':
default:
// Do nothing.
break;
}
if (empty($dayPart)) {
$dayPart = $defaultDay;
}
if (empty($monthPart)) {
$monthPart = $defaultMonth;
}
if (empty($yearPart)) {
$yearPart = $defaultYear;
}
if (empty($hourPart)) {
$hourPart = $defaultHour;
}
if (empty($minutePart)) {
$minutePart = $defaultMinute;
}
if (empty($secondPart)) {
$secondPart = $defaultSecond;
}
if (empty($monthPart) || empty($dayPart) || empty($yearPart)) {
return null;
}
return mktime($hourPart, $minutePart, $secondPart, $monthPart, $dayPart, $yearPart);
}
/**
* Get the value of a cookie variable.
*/
public function getCookieVar($key)
{
if (isset($_COOKIE[$key])) {
return $_COOKIE[$key];
} else {
return null;
}
}
/**
* Set a cookie variable.
*
* @param string $key
* @param int $expire (optional)
*/
public function setCookieVar($key, $value, $expire = 0)
{
$basePath = $this->getBasePath();
if (!$basePath) {
$basePath = '/';
}
setcookie($key, $value, $expire, $basePath);
$_COOKIE[$key] = $value;
}
/**
* Redirect to the specified page within a PKP Application.
* Shorthand for a common call to $request->redirect($dispatcher->url($request, PKPApplication::ROUTE_PAGE, ...)).
*
* @param mixed $context The optional contextual paths
* @param string $page The name of the op to redirect to.
* @param string $op optional The name of the op to redirect to.
* @param mixed $path string or array containing path info for redirect.
* @param array $params Map of name => value pairs for additional parameters
* @param string $anchor Name of desired anchor on the target page
*/
public function redirect($context = null, $page = null, $op = null, $path = null, $params = null, $anchor = null)
{
$dispatcher = $this->getDispatcher();
$this->redirectUrl($dispatcher->url($this, PKPApplication::ROUTE_PAGE, $context, $page, $op, $path, $params, $anchor));
}
/**
* Get the current "context" (press/journal/etc) object.
*
* @see PKPPageRouter::getContext()
*/
public function getContext(): ?Context
{
return $this->_delegateToRouter('getContext');
}
/**
* Deprecated
*
* @see PKPPageRouter::getRequestedPage()
*/
public function getRequestedPage()
{
return $this->_delegateToRouter('getRequestedPage');
}
/**
* Deprecated
*
* @see PKPPageRouter::getRequestedOp()
*/
public function getRequestedOp()
{
return $this->_delegateToRouter('getRequestedOp');
}
/**
* Deprecated
*
* @see PKPPageRouter::getRequestedArgs()
*/
public function getRequestedArgs()
{
return $this->_delegateToRouter('getRequestedArgs');
}
/**
* Deprecated
*
* @see PKPPageRouter::url()
*
* @param null|mixed $context
* @param null|mixed $page
* @param null|mixed $op
* @param null|mixed $path
* @param null|mixed $params
* @param null|mixed $anchor
*/
public function url(
$context = null,
$page = null,
$op = null,
$path = null,
$params = null,
$anchor = null,
$escape = false
) {
return $this->_delegateToRouter(
'url',
$context,
$page,
$op,
$path,
$params,
$anchor,
$escape
);
}
/**
* Get the URL to the public file uploads directory
*/
public function getPublicFilesUrl(?Context $context = null): string
{
$publicFileManager = new PublicFileManager();
return join('/', [
$this->getBaseUrl(),
$context
? $publicFileManager->getContextFilesPath($context->getId())
: $publicFileManager->getSiteFilesPath()
]);
}
/**
* This method exists to maintain backwards compatibility
* with calls to methods that have been factored into the
* Router implementations.
*
* It delegates the call to the router and returns the result.
*
* NB: This method is protected and may not be used by
* external classes. It should also only be used in legacy
* methods.
*
* @return mixed depends on the called method
*/
public function &_delegateToRouter($method)
{
// This call is deprecated. We don't trigger a
// deprecation error, though, as there are so
// many instances of this error that it has a
// performance impact and renders the error
// log virtually useless when deprecation
// warnings are switched on.
// FIXME: Fix enough instances of this error so that
// we can put a deprecation warning in here.
$router = $this->getRouter();
if (is_null($router)) {
assert(false);
$nullValue = null;
return $nullValue;
}
// Construct the method call
$callable = [$router, $method];
// Get additional parameters but replace
// the first parameter (currently the
// method to be called) with the request
// as all router methods required the request
// as their first parameter.
$parameters = func_get_args();
$parameters[0] = & $this;
$returner = call_user_func_array($callable, $parameters);
return $returner;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\core\PKPRequest', '\PKPRequest');
}
+482
View File
@@ -0,0 +1,482 @@
<?php
/**
* @file classes/core/PKPRouter.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 PKPRouter
*
* @see PKPPageRouter
* @see PKPComponentRouter
*
* @ingroup core
*
* @brief Basic router class that has functionality common to all routers.
*
* NB: All handlers provide the common basic workflow. The router
* calls the following methods in the given order.
* 1) constructor:
* Handlers should establish a mapping of remote
* operations to roles that may access them. They do
* so by calling PKPHandler::addRoleAssignment().
* 2) authorize():
* Authorizes the request, among other things based
* on the result of the role assignment created
* during object instantiation. If authorization fails
* then die with a fatal error or execute the "call-
* on-deny" advice if one has been defined in the
* authorization policy that denied access.
* 3) validate():
* Let the handler execute non-fatal data integrity
* checks (FIXME: currently only for component handlers).
* Please make sure that data integrity checks that can
* lead to denial of access are being executed in the
* authorize() step via authorization policies and not
* here.
* 4) initialize():
* Let the handler initialize its internal state based
* on authorized and valid data. Authorization and integrity
* checks should be kept out of here to get a clear separation
* of concerns.
* 5) execution:
* Executes the requested handler operation. The mapping
* of requests to operations depends on the router
* implementation (see the class doc of specific router
* implementations for more details).
* 6) client response:
* Handlers should return a string value that will then be
* returned to the client as a response. Handler operations
* should not output the response directly to the client so
* that we can run filter operations on the output if required.
* Outputting text from handler operations to the client
* is possible but deprecated.
*/
namespace PKP\core;
use APP\core\Application;
use Exception;
use PKP\config\Config;
use PKP\context\Context;
use PKP\context\ContextDAO;
use PKP\db\DAORegistry;
use PKP\handler\PKPHandler;
use PKP\plugins\Hook;
abstract class PKPRouter
{
//
// Internal state cache variables
// NB: Please do not access directly but
// only via their respective getters/setters
//
protected Application $_application;
protected Dispatcher $_dispatcher;
protected ?string $_contextPath = null;
public ?Context $_context = null;
public ?PKPHandler $_handler = null;
/** @var string */
public $_indexUrl;
/**
* Constructor
*/
public function __construct()
{
}
/**
* get the application
*/
public function getApplication(): Application
{
return $this->_application;
}
/**
* set the application
*/
public function setApplication(Application $application)
{
$this->_application = $application;
}
/**
* get the dispatcher
*/
public function getDispatcher(): \PKP\core\Dispatcher
{
return $this->_dispatcher;
}
/**
* set the dispatcher
*/
public function setDispatcher(\PKP\core\Dispatcher $dispatcher)
{
$this->_dispatcher = $dispatcher;
}
/**
* Set the handler object for later retrieval.
*/
public function setHandler(PKPHandler $handler)
{
$this->_handler = $handler;
}
/**
* Get the handler object.
*/
public function getHandler(): ?PKPHandler
{
return $this->_handler;
}
/**
* Determines whether this router can route the given request.
*/
public function supports(PKPRequest $request): bool
{
// Default implementation returns always true
return true;
}
/**
* Determine whether or not this request is cacheable
*/
public function isCacheable(PKPRequest $request): bool
{
// Default implementation returns always false
return false;
}
/**
* A generic method to return a context path (e.g. a Press or a Journal path)
*/
public function getRequestedContextPath(PKPRequest $request): string
{
// Determine the context path
if ($this->_contextPath === null) {
$this->_contextPath = Core::getContextPath($_SERVER['PATH_INFO'] ?? '');
Hook::call('Router::getRequestedContextPath', [&$this->_contextPath]);
}
return $this->_contextPath;
}
/**
* A Generic call to a context defining object (e.g. a Journal, Press, or Server)
*
* @param PKPRequest $request the request to be routed
* @param bool $forceReload (optional) Reset a context even if it's already been loaded
*
* @return Context
*/
public function getContext($request, $forceReload = false)
{
if ($forceReload || !isset($this->_context)) {
// Retrieve the requested context path (this validates the path)
$path = $this->getRequestedContextPath($request);
// Resolve the path to the context
if ($path === 'index' || $path === '' || $path === Application::CONTEXT_ID_ALL) {
$this->_context = null;
} else {
// FIXME: Can't just use Application::get()->getContextDAO() without test breakage
/** @var ContextDAO */
$contextDao = DAORegistry::getDAO(ucfirst(Application::get()->getContextName()) . 'DAO');
// Retrieve the context from the DAO (by path)
$this->_context = $contextDao->getByPath($path);
// If the context couldn't be retrieved, it's a 404 error.
if (!$this->_context) {
$this->getDispatcher()?->handle404();
}
}
}
return $this->_context;
}
/**
* Get the URL to the index script.
*
* @param PKPRequest $request the request to be routed
*
* @return string
*/
public function getIndexUrl($request)
{
if (!isset($this->_indexUrl)) {
if ($request->isRestfulUrlsEnabled()) {
$this->_indexUrl = $request->getBaseUrl();
} else {
$this->_indexUrl = $request->getBaseUrl() . '/index.php';
}
Hook::call('Router::getIndexUrl', [&$this->_indexUrl]);
}
return $this->_indexUrl;
}
//
// Protected template methods to be implemented by sub-classes.
//
/**
* Determine the filename to use for a local cache file.
*
* @param PKPRequest $request
*
* @return string
*/
public function getCacheFilename($request)
{
throw new Exception('Unimplemented');
}
/**
* Routes a given request to a handler operation
*
* @param PKPRequest $request
*/
abstract public function route($request);
/**
* Build a handler request URL into PKPApplication.
*
* @param PKPRequest $request the request to be routed
* @param mixed $newContext Optional contextual paths
* @param string $handler Optional name of the handler to invoke
* @param string $op Optional name of operation to invoke
* @param mixed $path Optional string or array of args to pass to handler
* @param array $params Optional set of name => value pairs to pass as user parameters
* @param string $anchor Optional name of anchor to add to URL
* @param bool $escape Whether or not to escape ampersands, square brackets, etc. for this URL; default false.
*
* @return string the URL
*/
abstract public function url(
PKPRequest $request,
?string $newContext = null,
$handler = null,
$op = null,
$path = null,
$params = null,
$anchor = null,
$escape = false
);
/**
* Handle an authorization failure.
*
* @param PKPRequest $request
* @param string $authorizationMessage a translation key with the authorization
* failure message.
*/
abstract public function handleAuthorizationFailure(
$request,
$authorizationMessage,
array $messageParams = []
);
//
// Private helper methods
//
/**
* This is the method that implements the basic
* life-cycle of a handler request:
* 1) authorization
* 2) validation
* 3) initialization
* 4) execution
* 5) client response
*
* @param callable|array $serviceEndpoint the handler operation
* @param PKPRequest $request
* @param array $args
* @param bool $validate whether or not to execute the
* validation step.
*/
public function _authorizeInitializeAndCallRequest(&$serviceEndpoint, $request, &$args, $validate = true)
{
$dispatcher = $this->getDispatcher();
// It's conceivable that a call has gotten this far without
// actually being callable, e.g. a component has been named
// that does not exist and that no plugin has registered.
if (!is_callable($serviceEndpoint)) {
$dispatcher->handle404();
}
// Pass the dispatcher to the handler.
$serviceEndpoint[0]->setDispatcher($dispatcher);
// Authorize the request.
$roleAssignments = $serviceEndpoint[0]->getRoleAssignments();
assert(is_array($roleAssignments));
if ($serviceEndpoint[0]->authorize($request, $args, $roleAssignments)) {
// Execute class-wide data integrity checks.
if ($validate) {
$serviceEndpoint[0]->validate($request, $args);
}
// Let the handler initialize itself.
$serviceEndpoint[0]->initialize($request, $args);
// Call the service endpoint.
$result = call_user_func($serviceEndpoint, $args, $request);
} else {
// Authorization failed - try to retrieve a user
// message.
$authorizationMessage = $serviceEndpoint[0]->getLastAuthorizationMessage();
// Set a generic authorization message if no
// specific authorization message was set.
if ($authorizationMessage == '') {
$authorizationMessage = 'user.authorization.accessDenied';
}
// Handle the authorization failure.
$result = $this->handleAuthorizationFailure($request, $authorizationMessage);
}
// Return the result of the operation to the client.
if (is_string($result)) {
echo $result;
} elseif ($result instanceof \PKP\core\JSONMessage) {
header('Content-Type: application/json');
echo $result->getString();
}
}
/**
* Build the base URL and add the context part of the URL.
*
* The new URL will be based on the current request's context
* if no new context is given.
*
* The base URL for a given primary context can be overridden
* in the config file using the 'base_url[context]' syntax in the
* config file's 'general' section.
*
* @param PKPRequest $request the request to be routed
* @param mixed $newContext (optional) context that differs from
* the current request's context
*
* @return array An array consisting of the base url as the first
* entry and the context as the remaining entries.
*/
public function _urlGetBaseAndContext($request, ?string $newContext = null)
{
if (isset($newContext)) {
// A new context has been set so use it.
$contextValue = rawurlencode($newContext);
} else {
// No new context has been set so determine
// the current request's context
$contextObject = $this->getContext($request);
$contextValue = $contextObject?->getPath() ?? 'index';
}
// Check whether the base URL is overridden.
if ($overriddenBaseUrl = Config::getVar('general', "base_url[{$contextValue}]")) {
return [$overriddenBaseUrl, []];
}
return [$this->getIndexUrl($request), [$contextValue]];
}
/**
* Build the additional parameters part of the URL.
*
* @param PKPRequest $request the request to be routed
* @param array $params (optional) the parameter list to be
* transformed to a url part.
* @param bool $escape (optional) Whether or not to escape structural elements
*
* @return array the encoded parameters or an empty array
* if no parameters were given.
*/
public function _urlGetAdditionalParameters($request, $params = null, $escape = true)
{
$additionalParameters = [];
if (!empty($params)) {
assert(is_array($params));
foreach ($params as $key => $value) {
if (is_array($value)) {
foreach ($value as $element) {
$additionalParameters[] = $key . ($escape ? '%5B%5D=' : '[]=') . rawurlencode($element);
}
} else {
$additionalParameters[] = $key . '=' . rawurlencode($value ?? '');
}
}
}
return $additionalParameters;
}
/**
* Creates a valid URL from parts.
*
* @param string $baseUrl the protocol, domain and initial path/parameters, no anchors allowed here
* @param array $pathInfoArray strings to be concatenated as path info
* @param array $queryParametersArray strings to be concatenated as query string
* @param ?string $anchor an additional anchor
* @param bool $escape whether to escape ampersands
*
* @return string the URL
*/
public function _urlFromParts(string $baseUrl, array $pathInfoArray = [], array $queryParametersArray = [], ?string $anchor = '', bool $escape = false)
{
// Parse the base url
$baseUrlParts = parse_url($baseUrl);
assert(isset($baseUrlParts['host']) && !isset($baseUrlParts['fragment']));
// Reconstruct the base url without path and query
$baseUrl = (isset($baseUrlParts['scheme']) ? $baseUrlParts['scheme'] . ':' : '') . '//';
if (isset($baseUrlParts['user'])) {
$baseUrl .= $baseUrlParts['user'];
if (isset($baseUrlParts['pass'])) {
$baseUrl .= ':' . $baseUrlParts['pass'];
}
$baseUrl .= '@';
}
$baseUrl .= $baseUrlParts['host'];
if (isset($baseUrlParts['port'])) {
$baseUrl .= ':' . $baseUrlParts['port'];
}
$baseUrl .= '/';
// Add path info from the base URL to the path info array (if any).
if (isset($baseUrlParts['path'])) {
$pathInfoArray = array_merge(explode('/', trim($baseUrlParts['path'], '/')), $pathInfoArray);
}
// Add query parameters from the base URL to the query parameter array (if any).
if (isset($baseUrlParts['query'])) {
$queryParametersArray = array_merge(explode('&', $baseUrlParts['query']), $queryParametersArray);
}
// Expand path info
$pathInfo = implode('/', $pathInfoArray);
// Expand query parameters
$amp = $escape ? '&amp;' : '&';
$queryParameters = implode($amp, $queryParametersArray);
$queryParameters = empty($queryParameters) ? '' : '?' . $queryParameters;
// Assemble and return the final URL
return $baseUrl . $pathInfo . $queryParameters . $anchor;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\core\PKPRouter', '\PKPRouter');
}
+90
View File
@@ -0,0 +1,90 @@
<?php
/**
* @file classes/core/PKPServices.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 PKPServices
*
* @ingroup core
*
* @see Core
*
* @brief Pimple Dependency Injection Container.
*/
namespace PKP\core;
use APP\core\Services;
use Pimple\Container;
abstract class PKPServices
{
/** @var Container Pimple Dependency Injection Container */
private static $instance = null;
protected $container = null;
/**
* private constructor
*/
private function __construct()
{
$this->container = new Container();
$this->init();
}
/**
* container initialization
*/
abstract protected function init();
/**
* A static method to register a service
*/
public static function register(\Pimple\ServiceProviderInterface $service)
{
self::_instance()->container->register($service);
}
/**
* A static method to get a service
*
* @param string $service
*/
public static function get($service)
{
return self::_instance()->_getFromContainer($service);
}
/**
* Returns the instance of the container
*
* @return static
*/
private static function _instance()
{
if (is_null(self::$instance)) {
self::$instance = new Services();
}
return self::$instance;
}
/**
* Gets the service from an instanced container.
*
* @param string $service
*/
private function _getFromContainer($service)
{
return $this->container[$service];
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\core\PKPServices', '\PKPServices');
}
+599
View File
@@ -0,0 +1,599 @@
<?php
/**
* @file classes/core/PKPString.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 PKPString
*
* @ingroup core
*
* @brief String manipulation wrapper class.
*
*/
namespace PKP\core;
use HTMLPurifier;
use HTMLPurifier_Config;
use PKP\config\Config;
use Stringy\Stringy;
class PKPString
{
/** @var int Camel case for class names */
public const CAMEL_CASE_HEAD_UP = 1;
/** @var int Camel case for method names */
public const CAMEL_CASE_HEAD_DOWN = 2;
/**
* Perform initialization required for the string wrapper library.
*/
public static function initialize()
{
static $isInitialized;
if (!$isInitialized) {
if (self::hasMBString()) {
// Set up default encoding
mb_internal_encoding('utf-8');
ini_set('default_charset', 'utf-8');
}
$isInitialized = true;
}
}
/**
* Check if server has the mbstring library.
*
* @return bool Returns true iff the server supports mbstring functions.
*/
public static function hasMBString()
{
static $hasMBString;
if (isset($hasMBString)) {
return $hasMBString;
}
// If string overloading is active, it will break many of the
// native implementations. mbstring.func_overload must be set
// to 0, 1 or 4 in php.ini (string overloading disabled).
// Note: Overloading has been deprecated on PHP 7.2
if (ini_get('mbstring.func_overload') && defined('MB_OVERLOAD_STRING')) {
$hasMBString = false;
} else {
$hasMBString = extension_loaded('mbstring') &&
function_exists('mb_strlen') &&
function_exists('mb_strpos') &&
function_exists('mb_strrpos') &&
function_exists('mb_substr') &&
function_exists('mb_strtolower') &&
function_exists('mb_strtoupper') &&
function_exists('mb_substr_count') &&
function_exists('mb_send_mail');
}
return $hasMBString;
}
//
// Wrappers for basic string manipulation routines.
//
/**
* @see https://www.php.net/strlen
*
* @param string $string Input string
*
* @return int String length
*/
public static function strlen($string)
{
return Stringy::create($string)->length();
}
/**
* @see https://www.php.net/strpos
*
* @param string $haystack Input haystack to search
* @param string $needle Input needle to search for
* @param int $offset Offset at which to begin searching
*
* @return int Position of needle within haystack
*/
public static function strpos($haystack, $needle, $offset = 0)
{
return Stringy::create($haystack)->indexOf($needle, $offset);
}
/**
* @see https://www.php.net/strrpos
*
* @param string $haystack Haystack to search
* @param string $needle Needle to search haystack for
*
* @return int Last index of Needle in Haystack
*/
public static function strrpos($haystack, $needle)
{
return Stringy::create($haystack)->indexOfLast($needle);
}
/**
* @see https://www.php.net/substr
*
* @param string $string Subject to extract substring from
* @param int $start Position to start from
* @param int $length Length to extract, or false for entire string from start position
*
* @return string Substring of $string
*/
public static function substr($string, $start, $length = null)
{
return (string) Stringy::create($string)->substr($start, $length);
}
/**
* @see https://www.php.net/strtolower
*
* @param string $string Input string
*
* @return string Lower case version of input string
*/
public static function strtolower($string)
{
return (string) Stringy::create($string)->toLowerCase();
}
/**
* @see https://www.php.net/strtoupper
*
* @param string $string Input string
*
* @return string Upper case version of input string
*/
public static function strtoupper($string)
{
return (string) Stringy::create($string)->toUpperCase();
}
/**
* @see https://www.php.net/ucfirst
*
* @param string $string Input string
*
* @return string ucfirst version of input string
*/
public static function ucfirst($string)
{
return (string) Stringy::create($string)->upperCaseFirst();
}
/**
* @see https://www.php.net/substr_count
*
* @param string $haystack Input string to search
* @param string $needle String to search within $haystack for
*
* @return int Count of number of times $needle appeared in $haystack
*/
public static function substr_count($haystack, $needle)
{
return Stringy::create($haystack)->countSubstr($needle);
}
/**
* @see https://www.php.net/encode_mime_header
*
* @param string $string Input MIME header to encode.
*
* @return string Encoded MIME header.
*/
public static function encode_mime_header($string)
{
static::initialize();
return static::hasMBString()
? mb_encode_mimeheader($string, mb_internal_encoding(), 'B', Core::isWindows() ? "\r\n" : "\n")
: $string;
}
//
// Wrappers for PCRE-compatible regular expression routines.
// See the php.net documentation for usage.
//
/**
* @see https://www.php.net/preg_quote
*
* @param string $string String to quote
* @param string $delimiter Delimiter for regular expression
*
* @return string Quoted equivalent of $string
*/
public static function regexp_quote($string, $delimiter = '/')
{
return preg_quote($string, $delimiter);
}
/**
* @see https://www.php.net/preg_grep
*
* @param string $pattern Regular expression
* @param array $input Input
*
* @return array
*/
public static function regexp_grep($pattern, $input)
{
return preg_grep($pattern . 'u', $input);
}
/**
* @see https://www.php.net/preg_match
*
* @param string $pattern Regular expression
* @param string $subject String to apply regular expression to
*
* @return int
*/
public static function regexp_match($pattern, $subject)
{
return preg_match($pattern . 'u', $subject);
}
/**
* @see https://www.php.net/preg_match_get
*
* @param string $pattern Regular expression
* @param string $subject String to apply regular expression to
* @param array $matches Reference to receive matches
*
* @return int|boolean Returns 1 if the pattern matches given subject, 0 if it does not, or FALSE if an error occurred.
*/
public static function regexp_match_get($pattern, $subject, &$matches)
{
return preg_match($pattern . 'u', $subject, $matches);
}
/**
* @see https://www.php.net/preg_match_all
*
* @param string $pattern Regular expression
* @param string $subject String to apply regular expression to
* @param array $matches Reference to receive matches
*
* @return int|boolean Returns number of full matches of given subject, or FALSE if an error occurred.
*/
public static function regexp_match_all($pattern, $subject, &$matches)
{
return preg_match_all($pattern . 'u', $subject, $matches);
}
/**
* @see https://www.php.net/preg_replace
*
* @param string $pattern Regular expression
* @param string $replacement String to replace matches in $subject with
* @param string $subject String to apply regular expression to
* @param int $limit Number of replacements to perform, maximum, or -1 for no limit.
*/
public static function regexp_replace($pattern, $replacement, $subject, $limit = -1)
{
return preg_replace($pattern . 'u', (string) $replacement, (string) $subject, $limit);
}
/**
* @see https://www.php.net/preg_replace_callback
*
* @param string $pattern Regular expression
* @param callable $callback PHP callback to generate content to replace matches with
* @param string $subject String to apply regular expression to
* @param int $limit Number of replacements to perform, maximum, or -1 for no limit.
*/
public static function regexp_replace_callback($pattern, $callback, $subject, $limit = -1)
{
return preg_replace_callback($pattern . 'u', $callback, $subject, $limit);
}
/**
* @see https://www.php.net/preg_split
*
* @param string $pattern Regular expression
* @param string $subject String to apply regular expression to
* @param int $limit Number of times to match; -1 for unlimited
*
* @return array Resulting string segments
*/
public static function regexp_split($pattern, $subject, $limit = -1)
{
return preg_split($pattern . 'u', $subject, $limit);
}
/**
* @see https://www.php.net/mime_content_type
*
* @param string $filename Filename to test.
* @param string $suggestedExtension Suggested file extension (used for common misconfigurations)
*
* @return string Detected MIME type
*/
public static function mime_content_type($filename, $suggestedExtension = '')
{
$result = null;
if (function_exists('finfo_open')) {
$fi = & Registry::get('fileInfo', true, null);
if ($fi === null) {
$fi = finfo_open(FILEINFO_MIME, Config::getVar('finfo', 'mime_database_path'));
}
if ($fi !== false) {
$result = strtok(finfo_file($fi, $filename), ' ;');
}
}
if (!$result && function_exists('mime_content_type')) {
$result = mime_content_type($filename);
// mime_content_type appears to return a charset
// (erroneously?) in recent versions of PHP5
if (($i = strpos($result, ';')) !== false) {
$result = trim(substr($result, 0, $i));
}
}
if (!$result) {
// Fall back on an external "file" tool
$f = escapeshellarg($filename);
$result = trim(`file --brief --mime $f`);
// Make sure we just return the mime type.
if (($i = strpos($result, ';')) !== false) {
$result = trim(substr($result, 0, $i));
}
}
// Check ambiguous mimetypes against extension
$exploded = explode('.', $filename);
$ext = array_pop($exploded);
if ($suggestedExtension) {
$ext = $suggestedExtension;
}
$ambiguities = self::getAmbiguousExtensionsMap();
if (isset($ambiguities[strtolower($ext . ':' . $result)])) {
$result = $ambiguities[strtolower($ext . ':' . $result)];
}
return $result;
}
/**
* @return string[]
*
* @brief overrides for ambiguous mime types returned by finfo
* SUGGESTED_EXTENSION:DETECTED_MIME_TYPE => OVERRIDE_MIME_TYPE
*/
public static function getAmbiguousExtensionsMap()
{
return [
'html:text/xml' => 'text/html',
'css:text/x-c' => 'text/css',
'css:text/plain' => 'text/css',
'csv:text/plain' => 'text/csv',
'js:text/plain' => 'text/javascript',
'xlsx:application/zip' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'xltx:application/zip' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
'potx:application/zip' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
'ppsx:application/zip' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
'pptx:application/zip' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'sldx:application/zip' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
'docm:application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'application/vnd.ms-word.document.macroEnabled.12',
'docx:application/zip' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'dotx:application/zip' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
'wma:video/x-ms-asf' => 'audio/x-ms-wma',
'wmv:video/x-ms-asf' => 'video/x-ms-wmv',
];
}
/**
* Strip unsafe HTML from the input text. Covers XSS attacks like scripts,
* onclick(...) attributes, javascript: urls, and special characters.
*
* @param string $input input string
* @param string $configKey The config section key['allowed_html', 'allowed_title_html']
*
* @return string
*/
public static function stripUnsafeHtml($input, $configKey = 'allowed_html')
{
static $purifier;
if (!isset($purifier)) {
$config = HTMLPurifier_Config::createDefault();
$config->set('Core.Encoding', 'utf-8');
$config->set('HTML.Doctype', 'HTML 4.01 Transitional');
$config->set('HTML.Allowed', Config::getVar('security', $configKey));
$config->set('Cache.SerializerPath', 'cache');
$purifier = new HTMLPurifier($config);
}
return $purifier->purify((string) $input);
}
/**
* Convert limited HTML into a string.
*
* @param string $html
*
* @return string
*/
public static function html2text($html)
{
$html = self::regexp_replace('/<[\/]?p>/', "\n", $html);
$html = self::regexp_replace('/<li>/', '&bull; ', $html);
$html = self::regexp_replace('/<\/li>/', "\n", $html);
$html = self::regexp_replace('/<br[ ]?[\/]?>/', "\n", $html);
$html = html_entity_decode(strip_tags($html), ENT_COMPAT, 'UTF-8');
return $html;
}
/**
* Joins two title string fragments (in $fields) either with a
* space or a colon.
*
* @param array $fields
*
* @return string the joined string
*/
public static function concatTitleFields($fields)
{
// Set the characters that will avoid the use of
// a semicolon between title and subtitle.
$avoidColonChars = ['?', '!', '/', '&'];
// if the first field ends in a character in $avoidColonChars,
// concat with a space, otherwise use a colon.
// Check for any of these characters in
// the last position of current full title value.
if (in_array(substr($fields[0], -1, 1), $avoidColonChars)) {
$fullTitle = join(' ', $fields);
} else {
$fullTitle = join(': ', $fields);
}
return $fullTitle;
}
/**
* Transform "handler-class" to "HandlerClass"
* and "my-op" to "myOp".
*
* @param string $string input string
* @param int $type which kind of camel case?
*
* @return string the string in camel case
*/
public static function camelize($string, $type = self::CAMEL_CASE_HEAD_UP)
{
assert($type == static::CAMEL_CASE_HEAD_UP || $type == static::CAMEL_CASE_HEAD_DOWN);
// Transform "handler-class" to "HandlerClass" and "my-op" to "MyOp"
$string = implode(array_map('ucfirst_codesafe', explode('-', $string)));
// Transform "MyOp" to "myOp"
if ($type == static::CAMEL_CASE_HEAD_DOWN) {
$string = strtolower_codesafe(substr($string, 0, 1)) . substr($string, 1);
}
return $string;
}
/**
* Transform "HandlerClass" to "handler-class"
* and "myOp" to "my-op".
*
* @param string $string
*
* @return string
*/
public static function uncamelize($string)
{
assert(!empty($string));
// Transform "myOp" to "MyOp"
$string = ucfirst_codesafe($string);
// Insert hyphens between words and return the string in lowercase
$words = [];
self::regexp_match_all('/[A-Z][a-z0-9]*/', $string, $words);
assert(isset($words[0]) && !empty($words[0]) && strlen(implode('', $words[0])) == strlen($string));
return strtolower_codesafe(implode('-', $words[0]));
}
/**
* Create a new UUID (version 4)
*
* @return string
*/
public static function generateUUID()
{
$charid = strtoupper(md5(uniqid(random_int(0, PHP_INT_MAX), true)));
$hyphen = '-';
$uuid = substr($charid, 0, 8) . $hyphen
. substr($charid, 8, 4) . $hyphen
. '4' . substr($charid, 13, 3) . $hyphen
. strtoupper(dechex(hexdec(ord(substr($charid, 16, 1))) % 4 + 8)) . substr($charid, 17, 3) . $hyphen
. substr($charid, 20, 12);
return $uuid;
}
/**
* Get a mapping from strftime to DateTime::format formatting equivalents.
* Old format: https://www.php.net/manual/en/function.strftime.php
* New format: https://www.php.net/manual/en/datetime.format.php
*
* Introduced in 3.4.0; remove this function (and calls to it) after this is distributed
* in an LTS release.
*/
public static function getStrftimeConversion(): array
{
return [
'%%' => '%', '%h' => 'M', '%d' => 'd', '%a' => 'D',
'%e' => 'j', '%A' => 'l', '%u' => 'N', '%w' => 'w',
'%U' => 'W', '%B' => 'F', '%m' => 'm', '%b' => 'M',
'%Y' => 'Y', '%y' => 'y', '%P' => 'a', '%p' => 'A',
'%l' => 'g', '%k' => 'G', '%I' => 'h', '%H' => 'H',
'%M' => 'i', '%S' => 's', '%Z' => 'T',
];
}
/**
* Convert any strftime-based datetime formatting into DateTime::format equivalent.
* Passes through any strings that are already in the new format without modification.
* Old format: https://www.php.net/manual/en/function.strftime.php
* New format: https://www.php.net/manual/en/datetime.format.php
*
* Introduced in 3.4.0; remove this function (and calls to it) after this is distributed
* in an LTS release.
*/
public static function convertStrftimeFormat(string $format): string
{
// Following the lead of Smarty's date_format modifier, check the
// format string for "%" characters. If found, attempt to convert.
// We don't expect date/time formats to contain other uses of %.
if (strstr($format, '%')) {
if (Config::getVar('debug', 'deprecation_warnings')) {
trigger_error('Deprecated use of strftime-based date format.');
}
$format = strtr($format, self::getStrftimeConversion());
}
return $format;
}
/**
* Matches each symbol of PHP date format string
* to jQuery Datepicker widget date format.
*
* @param string $phpFormat
*
* @return string
*/
public static function dateformatPHP2JQueryDatepicker($phpFormat)
{
return str_replace(
['d', 'j', 'l', 'm', 'n', 'F', 'Y'],
['dd', 'd', 'DD', 'mm', 'm', 'MM', 'yy'],
$phpFormat
);
}
/**
* Get the word count of a string
*/
public static function getWordCount(string $str): int
{
return count(preg_split('/\s+/', trim(str_replace('&nbsp;', ' ', strip_tags($str)))));
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\core\PKPString', '\PKPString');
}
+81
View File
@@ -0,0 +1,81 @@
<?php
/**
* @file classes/core/Registry.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 Registry
*
* @ingroup core
*
* @brief Maintains a static table of keyed references.
* Used for storing/accessing single instance objects and values.
*/
namespace PKP\core;
class Registry
{
/**
* Get a static reference to the registry data structure.
*
*/
public static function &_getRegistry(): array
{
static $registry = [];
return $registry;
}
/**
* Get the value of an item in the registry (optionally setting a default).
*
* @param bool $createIfEmpty Whether or not to create an entry if none exists
* @param mixed $default If $createIfEmpty, this value will be used as a default
*/
public static function &get(string $key, bool $createIfEmpty = false, mixed $default = null): mixed
{
$registry = & self::_getRegistry();
if (isset($registry[$key])) {
return $registry[$key];
}
if ($createIfEmpty) {
self::set($key, $default);
}
return $default;
}
/**
* Set the value of an item in the registry.
*/
public static function set(string $key, mixed &$value): void
{
$registry = & self::_getRegistry();
$registry[$key] = & $value;
}
/**
* Remove an item from the registry.
*/
public static function delete(string $key): void
{
$registry = & self::_getRegistry();
if (isset($registry[$key])) {
unset($registry[$key]);
}
}
/**
* Clear the registry of all contents.
*/
public static function clear(): void
{
$registry = & self::_getRegistry();
foreach (array_keys($registry) as $key) {
unset($registry[$key]);
}
}
}
+132
View File
@@ -0,0 +1,132 @@
<?php
/**
* @file classes/core/RuntimeEnvironment.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 RuntimeEnvironment
*
* @ingroup core
*
* @brief Class that describes a runtime environment.
*/
namespace PKP\core;
use PKP\config\Config;
class RuntimeEnvironment
{
/** @var string */
public $_phpVersionMin;
/** @var string */
public $_phpVersionMax;
/** @var array */
public $_phpExtensions;
/** @var array */
public $_externalPrograms;
public function __construct($phpVersionMin = PKPApplication::PHP_REQUIRED_VERSION, $phpVersionMax = null, $phpExtensions = [], $externalPrograms = [])
{
$this->_phpVersionMin = $phpVersionMin;
$this->_phpVersionMax = $phpVersionMax;
$this->_phpExtensions = $phpExtensions;
$this->_externalPrograms = $externalPrograms;
}
//
// Setters and Getters
//
/**
* Get the min required PHP version
*
* @return string
*/
public function getPhpVersionMin()
{
return $this->_phpVersionMin;
}
/**
* Get the max required PHP version
*
* @return string
*/
public function getPhpVersionMax()
{
return $this->_phpVersionMax;
}
/**
* Get the required PHP extensions
*
* @return array
*/
public function getPhpExtensions()
{
return $this->_phpExtensions;
}
/**
* Get the required external programs
*
* @return array
*/
public function getExternalPrograms()
{
return $this->_externalPrograms;
}
//
// Public methods
//
/**
* Checks whether the current runtime environment is
* compatible with the specified parameters.
*
* @return bool
*/
public function isCompatible()
{
// Check PHP version
if (!is_null($this->_phpVersionMin) && version_compare(PHP_VERSION, $this->_phpVersionMin) < 0) {
return false;
}
if (!is_null($this->_phpVersionMax) && version_compare(PHP_VERSION, $this->_phpVersionMax) > 0) {
return false;
}
// Check PHP extensions
foreach ($this->_phpExtensions as $requiredExtension) {
if (!extension_loaded($requiredExtension)) {
return false;
}
}
// Check external programs
foreach ($this->_externalPrograms as $requiredProgram) {
$externalProgram = Config::getVar('cli', $requiredProgram);
if (!file_exists($externalProgram)) {
return false;
}
if (function_exists('is_executable')) {
if (!is_executable($externalProgram)) {
return false;
}
}
}
// Compatibility check was successful
return true;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\core\RuntimeEnvironment', '\RuntimeEnvironment');
}
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* @file classes/core/SoftDeleteTrait.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class SoftDeleteTrait
*
* @ingroup core
*
* @brief Implements the methods for soft deletion that can be used in entity daos that support it
*/
namespace PKP\core;
use Illuminate\Support\Facades\DB;
trait SoftDeleteTrait
{
/**
* Soft delete an object from the database
*/
protected function _softDelete(DataObject $object): void
{
$this->softDeleteById($object->getId());
}
/**
* Soft delete an object from the database by its id
*/
public function softDeleteById(int $id): void
{
DB::table($this->table)
->where($this->primaryKeyColumn, '=', $id)
->update(['deleted_at' => Core::getCurrentDate()]);
}
}
@@ -0,0 +1,196 @@
<?php
/**
* @file classes/core/VirtualArrayIterator.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 VirtualArrayIterator
*
* @ingroup db
*
* @brief Provides paging and iteration for "virtual" arrays -- arrays for which only
* the current "page" is available, but are much bigger in entirety.
*/
namespace PKP\core;
use PKP\db\DBResultRange;
class VirtualArrayIterator extends ItemIterator
{
/** @var array The array of contents of this iterator. */
public $theArray;
/** @var int Number of items to iterate through on this page */
public $itemsPerPage;
/** @var int The current page. */
public $page;
/** @var int The total number of items. */
public $count;
/** @var bool Whether or not the iterator was empty from the start */
public $wasEmpty;
/**
* Constructor.
*
* @param array $theArray The array of items to iterate through
* @param int $totalItems The total number of items in the virtual "larger" array
* @param int $page the current page number
* @param int $itemsPerPage Number of items to display per page
*/
public function __construct($theArray, $totalItems, $page = -1, $itemsPerPage = -1)
{
parent::__construct();
if ($page >= 1 && $itemsPerPage >= 1) {
$this->page = $page;
} else {
$this->page = 1;
$this->itemsPerPage = max(count($this->theArray), 1);
}
$this->theArray = $theArray;
$this->count = $totalItems;
$this->itemsPerPage = $itemsPerPage;
$this->wasEmpty = count($this->theArray) == 0;
reset($this->theArray);
}
/**
* Factory Method.
* Extracts the appropriate page items from the whole array and
* calls the constructor.
*
* @param array $wholeArray The whole array of items
* @param DBResultRange $rangeInfo The number of items per page
*
* @return object VirtualArrayIterator
*/
public static function factory($wholeArray, $rangeInfo)
{
if ($rangeInfo->isValid()) {
$slicedArray = array_slice($wholeArray, $rangeInfo->getCount() * ($rangeInfo->getPage() - 1), $rangeInfo->getCount(), true);
}
return new VirtualArrayIterator($slicedArray, count($wholeArray), $rangeInfo->getPage(), $rangeInfo->getCount());
}
/**
* Return the next item in the iterator.
*
* @return ?object
*/
public function &next()
{
if (!is_array($this->theArray)) {
$nullVar = null;
return $nullVar;
}
$value = current($this->theArray);
if (next($this->theArray) == null) {
$this->theArray = null;
}
return $value;
}
/**
* Return the next item in the iterator, with key.
*
* @return array (key, value)
*/
public function nextWithKey()
{
$key = key($this->theArray);
$value = $this->next();
return [$key, $value];
}
/**
* Check whether or not this iterator is for the first page of a sequence
*
* @return bool
*/
public function atFirstPage()
{
return $this->page == 1;
}
/**
* Check whether or not this iterator is for the last page of a sequence
*
* @return bool
*/
public function atLastPage()
{
return ($this->page * $this->itemsPerPage) + 1 > $this->count;
}
/**
* Get the page number that this iterator represents
*
* @return int
*/
public function getPage()
{
return $this->page;
}
/**
* Get the total number of items in the virtual array
*
* @return int
*/
public function getCount()
{
return $this->count;
}
/**
* Get the total number of pages in the virtual array
*
* @return int
*/
public function getPageCount()
{
return max(1, ceil($this->count / $this->itemsPerPage));
}
/**
* Return a boolean indicating whether or not we've reached the end of results
* Note: This implementation requires that next() be called before every eof() will
* function properly (except the first call).
*
* @return bool
*/
public function eof()
{
return (($this->theArray == null) || (count($this->theArray) == 0));
}
/**
* Return a boolean indicating whether or not this iterator was empty from the beginning
*
* @return bool
*/
public function wasEmpty()
{
return $this->wasEmpty;
}
/**
* Convert the iterator into an array
*
* @return array
*/
public function &toArray()
{
return $this->theArray;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\core\VirtualArrayIterator', '\VirtualArrayIterator');
}
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/**
* @file classes/core/exceptions/StoreTemporaryFileException.php
*
* Copyright (c) 2014-2023 Simon Fraser University
* Copyright (c) 2000-2023 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class StoreTemporaryFileException
*
* @brief Use this exception when an error is encountered while moving
* a temporary file to a permanent place.
*/
namespace PKP\core\exceptions;
use Exception;
use PKP\core\DataObject;
use PKP\file\TemporaryFile;
use PKP\user\User;
class StoreTemporaryFileException extends Exception
{
public function __construct(public TemporaryFile $temporaryFile, public string $targetPath, public ?User $user, public ?DataObject $dataObject)
{
$message = `Unable to store temporary file {$temporaryFile->getFilePath()} in {$targetPath}.`;
if ($user) {
$message .= ` File was uploaded by {$temporaryFile->getUserId()}. The current user is {$user->getId()}.`;
}
if ($dataObject) {
$class = get_class($dataObject);
$message .= ` Handling {$class} id {$dataObject->getId()}.`;
}
parent::__construct($message);
}
}
@@ -0,0 +1,42 @@
<?php
/**
* @file classes/core/interfaces/CollectorInterface.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 CollectorInterface
*
* @brief An interface describing the methods an Collector class must implement.
*/
namespace PKP\core\interfaces;
use Illuminate\Database\Query\Builder;
interface CollectorInterface
{
/**
* Get the configured query builder
*
* This returns an instance of Laravel's query builder. Use this
* to execute queries on the entity's table that do not already
* have a query method.
*
* The following example shows how to use this method after applying
* query conditions. In this example, the query is used to get
* only the date of the last three announcements:
*
* ```php
* $dates = Repo::announcement()
* ->filterByContextIds([$contextId])
* ->getQueryBuilder()
* ->limit(3)
* ->pluck('date_posted');
* ```
*
* See: https://laravel.com/docs/8.x/queries
*/
public function getQueryBuilder(): Builder;
}
+53
View File
@@ -0,0 +1,53 @@
<?php
/**
* @file classes/core/maps/Base.php
*
* Copyright (c) 2014-2020 Simon Fraser University
* Copyright (c) 2000-2020 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Base
*
* @brief A base class for creating extensible maps of any kind.
*/
namespace PKP\core\maps;
abstract class Base
{
/** @var array Callbacks that should be applied to each object in the map. */
protected $extensions = [];
/**
* Extend the map with a custom callback function
*
* Example:
*
* $map->extend(function($output, $input, $map) {
* $output['example'] = $input->example;
* return $output;
* })
*
*/
public function extend(callable $cb): self
{
$this->extensions[] = $cb;
return $this;
}
/**
* Run extensions applied to this map
*
* Run the callback functions registered with extend.
*
* @param mixed $output The output the object is being mapped to
* @param mixed $input The object that is being mapped from
*/
protected function withExtensions($output, $input)
{
foreach ($this->extensions as $extension) {
$output = call_user_func($extension, $output, $input, $this);
}
return $output;
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
/**
* @file classes/core/maps/Schema.php
*
* Copyright (c) 2014-2020 Simon Fraser University
* Copyright (c) 2000-2020 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class schema
*
* @brief A base class for mapping objects to their schema properties
*/
namespace PKP\core\maps;
use Illuminate\Support\Enumerable;
use PKP\context\Context;
use PKP\core\PKPApplication;
use PKP\core\PKPRequest;
use PKP\services\PKPSchemaService;
abstract class Schema extends Base
{
public PKPRequest $request;
public ?Context $context;
public PKPSchemaService $schemaService;
/** The collection of objects being mapped. Null if only one item is being mapped. */
public Enumerable $collection;
/** The property names for a summary of this entity according to its schema */
public array $summaryProps;
/** The property names of this entity according to its schema */
public array $props;
/** The name of the schema for this entity. One of the \PKP\services\PKPSchemaService::SCHEMA_... constants */
public string $schema;
public function __construct(PKPRequest $request, ?Context $context, PKPSchemaService $schemaService)
{
$this->request = $request;
$this->context = $context;
$this->schemaService = $schemaService;
}
/**
* Get the property names of this entity according to its schema
*/
protected function getProps(): array
{
return $this->props ?? $this->schemaService->getFullProps($this->schema);
}
/**
* Get the property names for a summary of this entity according to its schema
*/
protected function getSummaryProps(): array
{
return $this->summaryProps ?? $this->schemaService->getSummaryProps($this->schema);
}
/**
* Get the URL to an object in the REST API
*/
protected function getApiUrl(string $route, $contextPath = PKPApplication::CONTEXT_ID_ALL): string
{
return $this->request->getDispatcher()->url(
$this->request,
PKPApplication::ROUTE_API,
$contextPath,
$route,
);
}
}
@@ -0,0 +1,71 @@
<?php
/**
* @file classes/core/traits/EntityWithParent.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class EntityWithParent
*
* @ingroup core_traits
*
* @brief A trait for DAO classes that can be used with entities that have a parent entity. For example, a Submission always has a parent Context.
*
*/
namespace PKP\core\traits;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
use PKP\core\DataObject;
/**
* @template T of DataObject
*/
trait EntityWithParent
{
/**
* Get the parent object ID column name
*/
abstract public function getParentColumn(): string;
/**
* @copydoc EntityDAO::fromRow()
*
* @return T
*/
abstract public function fromRow(object $row): DataObject;
/**
* Check if an object exists.
*
* Optionally, pass the ID of a parent entity to check if the object
* exists and is assigned to that parent.
*/
public function exists(int $id, int $parentId = null): bool
{
return DB::table($this->table)
->where($this->primaryKeyColumn, '=', $id)
->when($parentId !== null, fn (Builder $query) => $query->where($this->getParentColumn(), $parentId))
->exists();
}
/**
* Get an object.
*
* Optionally, pass the ID of a parent entity to only get an object
* if it exists and is assigned to that parent.
*
* @return ?T
*/
public function get(int $id, int $parentId = null): ?DataObject
{
$row = DB::table($this->table)
->where($this->primaryKeyColumn, $id)
->when($parentId !== null, fn (Builder $query) => $query->where($this->getParentColumn(), $parentId))
->first();
return $row ? $this->fromRow($row) : null;
}
}