diff --git a/.circleci/config.yml b/.circleci/config.yml index f00d34bd61..99dd1b7271 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -27,18 +27,33 @@ jobs: - setup_remote_docker - run: name: Docker Compose corresponding OS file - command: docker compose -f ~/project/tests/playwright/Docker/docker-compose.yml up -d + command: pushd ~/project/tests/playwright/Docker && docker compose up -d; popd - run: name: Generate Key for XDMoD command: docker exec xdmod openssl genrsa -out /etc/pki/tls/private/localhost.key -rand /proc/cpuinfo:/proc/filesystems:/proc/interrupts:/proc/ioports:/proc/uptime 2048 - run: name: Generate Cert for XDMoD command: docker exec xdmod /usr/bin/openssl req -new -key /etc/pki/tls/private/localhost.key -x509 -sha256 -days 365 -set_serial $RANDOM -extensions v3_req -out /etc/pki/tls/certs/localhost.crt -subj "/C=XX/L=Default City/O=Default Company Ltd" + - run: + name: Update PHP to PHP8.2 + command: | + docker exec xdmod dnf module reset -y php + docker exec xdmod dnf module enable -y php:8.2 + docker exec xdmod dnf install -y php-devel openssl-devel + docker exec xdmod dnf update -y php php-common php-opcache php-cli php-gd php-curl php-pear php-zip php-gmp php-pdo php-xml php-mbstring php-mysqlnd php-pecl-apcu php-pecl-json php-pear + docker exec xdmod pecl uninstall mongodb-1.18.1 + docker exec xdmod pecl install mongodb-1.18.1 + docker exec xdmod pecl install zip + docker exec xdmod dnf remove -y php-devel openssl-devel + docker exec xdmod bash -c ">/var/log/php_errors.log" - run: name: Copy Files for Playwright and XDMoD containers command: | docker cp ~/project xdmod:/root/xdmod - docker cp ~/project playwright:/root/xdmod + docker exec playwright mkdir -p /root/xdmod/tests/ /root/xdmod/tests/artifacts/xdmod/ + docker cp ~/project/tests/playwright playwright:/root/xdmod/tests/ + docker cp ~/project/tests/ci playwright:/root/xdmod/tests/ + docker cp ~/project/tests/artifacts/xdmod/ui playwright:/root/xdmod/tests/artifacts/xdmod/ - run: name: Create test result directories command: | @@ -56,6 +71,13 @@ jobs: - run: name: Install XDMoD Composer Dependencies command: docker exec -w /root/xdmod xdmod composer install + - run: + name: Fixup php.ini for debugging + command: | + docker exec xdmod bash -c "sed -i 's|error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT|error_reporting = E_ALL|g' /etc/php.ini" + docker exec xdmod bash -c "sed -i 's|display_errors = Off|display_errors = On|g' /etc/php.ini" + docker exec xdmod bash -c "sed -i 's|display_startup_errors = Off|display_startup_errors = On|g' /etc/php.ini" + docker exec xdmod bash -c "sed -i 's|;error_log = php_errors.log|error_log = php_errors.log|g' /etc/php.ini" - run: name: Build XDMoD RPM command: docker exec -w /root/xdmod xdmod /root/bin/buildrpm xdmod @@ -78,7 +100,7 @@ jobs: command: docker exec -w /root/xdmod xdmod composer install - run: name: Setup the SimpleSAML server etc. so we can test SSO - command: docker exec xdmod /root/xdmod/tests/ci/samlSetup.sh + command: docker exec xdmod /root/xdmod/tests/ci/samlSetup.sh -t local -h xdmod - run: name: Make sure that the Test Dependencies are installed command: docker exec -w /root/xdmod xdmod composer install --no-progress @@ -196,7 +218,7 @@ jobs: - run: name: Ensure that no PHP command-line errors were generated command: | - docker exec xdmod /bin/bash -c "if [ -s /var/log/php_errors.log ]; then cat /var/log/php_errors.log; false; fi" + docker exec xdmod /bin/bash -c "if [ -s /var/log/php_errors.log ]; then cat /var/log/php_errors.log | grep -v 'Warning'; false; fi" - store_artifacts: path: /tmp/screenshots - store_artifacts: diff --git a/.env b/.env new file mode 100644 index 0000000000..a281273761 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +# There is no need to alter this file except to support custom code development for XDMoD, it is required by +# Symfony but we load the default Symfony required values via DefaultXdmodEnvVarLoader. diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 5a65f30d08..0a6a5a01e4 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -5,6 +5,7 @@ on: pull_request: branches: - main + - symfony_migration env: XDMOD_IS_CORE: 'true' @@ -25,7 +26,7 @@ jobs: - name: Setup php uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.2' extensions: xml tools: composer:v2 diff --git a/bin/console b/bin/console new file mode 100755 index 0000000000..fc5a44d74a --- /dev/null +++ b/bin/console @@ -0,0 +1,39 @@ +#!/usr/bin/env php +warning("Skipping user '${user['username']}' - account already exists"); + $logger->warning("Skipping user '{$user['username']}' - account already exists"); continue; } diff --git a/classes/Authentication/SAML/XDSamlAuthentication.php b/classes/Authentication/SAML/XDSamlAuthentication.php index 17af42656e..be11260769 100644 --- a/classes/Authentication/SAML/XDSamlAuthentication.php +++ b/classes/Authentication/SAML/XDSamlAuthentication.php @@ -6,6 +6,11 @@ use \Exception; use CCR\Log; use Models\Services\Organizations; +use Psr\Log\LoggerInterface; +use SimpleSAML\Auth\Simple; +use SimpleSAML\Auth\Source; +use SimpleSAML\Metadata\MetaDataStorageHandler; +use SimpleSAML\Session; use XDUser; class XDSamlAuthentication @@ -13,7 +18,7 @@ class XDSamlAuthentication /** * The selected auth source * - * @var \SimpleSAML_Auth_Simple + * @var Simple */ protected $_as = null; @@ -65,7 +70,7 @@ public function __construct() ) ); - $this->_sources = \SimpleSAML_Auth_Source::getSources(); + $this->_sources = Source::getSources(); if ($this->isSamlConfigured()) { try { $authSource = \xd_utilities\getConfiguration('authentication', 'source'); @@ -97,7 +102,7 @@ public function isSamlConfigured() */ public function logout(){ if ($this->isSamlConfigured()) { - \SimpleSAML_Session::getSessionFromRequest()->doLogout($this->authSourceName); + Session::getSessionFromRequest()->doLogout($this->authSourceName); } } /** @@ -112,7 +117,7 @@ public function getXdmodAccount() /* * SimpleSAMLphp uses its own session, this sets it back. */ - \SimpleSAML_Session::getSessionFromRequest()->cleanup(); + Session::getSessionFromRequest()->cleanup(); if ($this->_as->isAuthenticated()) { $userName = $samlAttrs['username'][0]; @@ -205,7 +210,7 @@ public function getOrganizationId($samlAttrs, $personId) * * @param string $returnTo the URI to redirect to after auth. * - * @return the login URL or false if no provider is configured + * @return string|bool login URL or false if no provider is configured */ public function getLoginURL($returnTo) { @@ -226,8 +231,8 @@ public function getLoginLink() if (!$this->isSamlConfigured()) { return false; } - $idp = \SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler()->getMetadata( - \SimpleSAML_Auth_Source::getById($this->authSourceName)->getMetadata()->toArray()['idp'], + $idp = MetaDataStorageHandler::getMetadataHandler()->getMetaData( + Source::getById($this->authSourceName)->getMetadata()->toArray()['idp'], 'saml20-idp-remote' ); if (!empty($idp['OrganizationDisplayName'])) { diff --git a/classes/CCR/CCRDBFormatter.php b/classes/CCR/CCRDBFormatter.php index c34ff313d6..c2e3f1ebab 100644 --- a/classes/CCR/CCRDBFormatter.php +++ b/classes/CCR/CCRDBFormatter.php @@ -3,6 +3,7 @@ namespace CCR; use Monolog\Formatter\NormalizerFormatter; +use Monolog\LogRecord; class CCRDBFormatter extends NormalizerFormatter { @@ -12,7 +13,7 @@ class CCRDBFormatter extends NormalizerFormatter * all of the properties from the context. If the message is an empty * string the message property is not added. */ - public function format(array $record) + public function format(LogRecord $record) { $vars = parent::format($record); diff --git a/classes/CCR/CCRDBHandler.php b/classes/CCR/CCRDBHandler.php index b56bbc5adf..a67bf5ff51 100644 --- a/classes/CCR/CCRDBHandler.php +++ b/classes/CCR/CCRDBHandler.php @@ -5,6 +5,8 @@ use CCR\DB\iDatabase; use Exception; use Monolog\Handler\AbstractProcessingHandler; +use Monolog\Level; +use Monolog\LogRecord; /** * This class is meant to provide a means of writing log entries to a database within the Monolog framework. @@ -49,7 +51,7 @@ class CCRDBHandler extends AbstractProcessingHandler */ public function __construct(iDatabase $db = null, $schema = null, $table = null, $level = Log::DEBUG, $bubble = true) { - parent::__construct($level, $bubble); + parent::__construct(Level::fromValue(Log::convertToMonologLevel($level)), $bubble); if (!isset($db)) { $db = DB::factory('logger'); @@ -71,16 +73,16 @@ public function __construct(iDatabase $db = null, $schema = null, $table = null, /** * @see AbstractProcessingHandler::write() */ - protected function write(array $record) + protected function write(LogRecord $record): void { $sql = sprintf("INSERT INTO %s.%s (id, logtime, ident, priority, message) VALUES(:id, NOW(), :ident, :priority, :message)", $this->schema, $this->table); - - $this->db->execute($sql, array( + $params = [ ':id' => $this->getNextId(), ':ident' => $record['channel'], ':priority' => Log::convertToCCRLevel($record['level']), ':message' => $record['formatted'] - )); + ]; + $this->db->execute($sql, $params); } /** diff --git a/classes/CCR/CCRLineFormatter.php b/classes/CCR/CCRLineFormatter.php index 897f7d2941..a795d17fbf 100644 --- a/classes/CCR/CCRLineFormatter.php +++ b/classes/CCR/CCRLineFormatter.php @@ -4,6 +4,7 @@ use Monolog\Formatter\LineFormatter; use Monolog\Formatter\NormalizerFormatter; +use Monolog\LogRecord; use Monolog\Utils; class CCRLineFormatter extends LineFormatter @@ -45,7 +46,7 @@ protected function toJson($data, $ignoreErrors = false): string * string and context object. If either the context is empty or the message * is an empty string they are ommitted. */ - public function format(array $record) + public function format(LogRecord $record): string { $vars = NormalizerFormatter::format($record); @@ -98,6 +99,11 @@ public function format(array $record) // remove leftover %extra.xxx% and %context.xxx% if any if (false !== strpos($output, '%')) { $output = preg_replace('/%(?:extra|context)\..+?%/', '', $output); + if (null === $output) { + $pcreErrorCode = preg_last_error(); + + throw new \RuntimeException('Failed to run preg_replace: ' . $pcreErrorCode . ' / ' . preg_last_error_msg()); + } } return $output; diff --git a/classes/CCR/Log.php b/classes/CCR/Log.php index 8f56dd3c28..bd2d7f8ef6 100644 --- a/classes/CCR/Log.php +++ b/classes/CCR/Log.php @@ -8,7 +8,9 @@ use Monolog\Handler\NativeMailerHandler; use Monolog\Handler\NullHandler; use Monolog\Handler\StreamHandler; +use Monolog\Level; use Psr\Log\LoggerInterface; + use xd_utilities; /** @@ -32,25 +34,25 @@ class Log const DEBUG = 7; private static $logLevels = array( - self::EMERG => \Monolog\Logger::EMERGENCY, - self::ALERT => \Monolog\Logger::ALERT, - self::CRIT => \Monolog\Logger::CRITICAL, - self::ERR => \Monolog\Logger::ERROR, - self::WARNING => \Monolog\Logger::WARNING, - self::NOTICE => \Monolog\Logger::NOTICE, - self::INFO => \Monolog\Logger::INFO, - self::DEBUG => \Monolog\Logger::DEBUG + self::EMERG => \Monolog\Level::Emergency->value, + self::ALERT => \Monolog\Level::Alert->value, + self::CRIT => \Monolog\Level::Critical->value, + self::ERR => \Monolog\Level::Error->value, + self::WARNING => \Monolog\Level::Warning->value, + self::NOTICE => \Monolog\Level::Notice->value, + self::INFO => \Monolog\Level::Info->value, + self::DEBUG => \Monolog\Level::Debug->value ); private static $flippedLogLevels = array( - \Monolog\Logger::EMERGENCY => self::EMERG, - \Monolog\Logger::ALERT => self::ALERT, - \Monolog\Logger::CRITICAL => self::CRIT, - \Monolog\Logger::ERROR => self::ERR, - \Monolog\Logger::WARNING => self::WARNING, - \Monolog\Logger::NOTICE => self::NOTICE, - \Monolog\Logger::INFO => self::INFO, - \Monolog\Logger::DEBUG => self::DEBUG + \Monolog\Level::Emergency->value => self::EMERG, + \Monolog\Level::Alert->value => self::ALERT, + \Monolog\Level::Critical->value => self::CRIT, + \Monolog\Level::Error->value => self::ERR, + \Monolog\Level::Warning->value => self::WARNING, + \Monolog\Level::Notice->value => self::NOTICE, + \Monolog\Level::Info->value => self::INFO, + \Monolog\Level::Debug->value => self::DEBUG ); /** @@ -165,7 +167,7 @@ protected static function getLogger($ident, array $conf) 'mail' ); - $logger = new Logger($ident); + $logger = new \Monolog\Logger($ident); // Short circuit the function if 'null' was asked for since this will be the only handler for the logger. if ($ident === 'null') { @@ -262,7 +264,7 @@ protected static function getDbHandler($ident, array $conf) { $dbLogLevel = $conf['dbLogLevel'] ?? self::getDefaultLogLevel('db'); - $handler = new CCRDBHandler(null, null, null, self::convertToMonologLevel($dbLogLevel)); + $handler = new CCRDBHandler(null, null, null, $dbLogLevel); $handler->setFormatter(new CCRDBFormatter()); return $handler; @@ -341,7 +343,7 @@ public static function convertToCCRLevel($monologLevel) if (array_key_exists($monologLevel, self::$flippedLogLevels)) { return self::$flippedLogLevels[$monologLevel]; } - throw new Exception('Unknown Log Level'); + throw new Exception(sprintf('Unknown Monolog Log Level %s', $monologLevel)); } /** @@ -356,7 +358,7 @@ public static function convertToMonologLevel($ccrLevel) if (array_key_exists($ccrLevel, self::$logLevels)) { return self::$logLevels[$ccrLevel]; } - throw new Exception('Unknown Log Level'); + throw new Exception(sprintf('Unknown CCR Log Level %s', $ccrLevel)); } /** diff --git a/classes/CCR/Logger.php b/classes/CCR/Logger.php deleted file mode 100644 index a936141499..0000000000 --- a/classes/CCR/Logger.php +++ /dev/null @@ -1,93 +0,0 @@ -handlers) { - $this->pushHandler(new StreamHandler('php://stderr', static::DEBUG)); - } - - $levelName = static::getLevelName($level); - - // check if any handler will handle this message so we can return early and save cycles - $handlerKey = null; - reset($this->handlers); - while ($handler = current($this->handlers)) { - if ($handler->isHandling(array('level' => $level))) { - $handlerKey = key($this->handlers); - break; - } - - next($this->handlers); - } - - if (null === $handlerKey) { - return false; - } - - if (!static::$timezone) { - static::$timezone = new \DateTimeZone(date_default_timezone_get() ?: 'UTC'); - } - - // php7.1+ always has microseconds enabled, so we do not need this hack - if ($this->microsecondTimestamps && PHP_VERSION_ID < 70100) { - $ts = \DateTime::createFromFormat('U.u', sprintf('%.6F', microtime(true)), static::$timezone); - } else { - $ts = new \DateTime('now', static::$timezone); - } - $ts->setTimezone(static::$timezone); - - $record = array( - 'message' => (string) $message, - 'context' => $context, - 'level' => $level, - 'level_name' => strtolower($levelName), - 'channel' => $this->name, - 'datetime' => $ts, - 'extra' => array('message' => $message), - ); - - try { - foreach ($this->processors as $processor) { - $record = call_user_func($processor, $record); - } - - while ($handler = current($this->handlers)) { - if (true === $handler->handle($record)) { - break; - } - - next($this->handlers); - } - } catch (Exception $e) { - $this->handleException($e, $record); - } - - return true; - } -} diff --git a/classes/Configuration/Configuration.php b/classes/Configuration/Configuration.php index 9422da03f9..d551ca847c 100644 --- a/classes/Configuration/Configuration.php +++ b/classes/Configuration/Configuration.php @@ -1144,27 +1144,27 @@ protected function deleteSection($name) * ========================================================================================== */ - public function current() + public function current(): mixed { return current($this->sectionData); } - public function key() + public function key(): mixed { return key($this->sectionData); } - public function next() + public function next(): void { - return next($this->sectionData); + next($this->sectionData); } - public function rewind() + public function rewind(): void { - return reset($this->sectionData); + reset($this->sectionData); } - public function valid() + public function valid(): bool { return false !== current($this->sectionData); } diff --git a/classes/DB/FilterListHelper.php b/classes/DB/FilterListHelper.php index a9ca288f78..4a6e46cebb 100644 --- a/classes/DB/FilterListHelper.php +++ b/classes/DB/FilterListHelper.php @@ -65,7 +65,7 @@ public static function getTableName(Query $realmQuery, GroupBy $groupBy1, GroupB $firstId = $groupBy2Id; $secondId = $groupBy1Id; } - $tableName .= "${firstId}___{$secondId}"; + $tableName .= "{$firstId}___{$secondId}"; } return $tableName; diff --git a/classes/DataWarehouse/Access/Usage.php b/classes/DataWarehouse/Access/Usage.php index 1c95a6c317..0d12dbdd8c 100644 --- a/classes/DataWarehouse/Access/Usage.php +++ b/classes/DataWarehouse/Access/Usage.php @@ -115,7 +115,7 @@ private function getSummaryCharts(XDUser $user) { $usageChart = array( 'hc_jsonstore' => array('title' => array('text' => '')), - 'id' => "node=statistic&realm=${usageRealm}&group_by=${usageGroupBy}&statistic=${userStatistic}", + 'id' => "node=statistic&realm={$usageRealm}&group_by={$usageGroupBy}&statistic={$userStatistic}", 'short_title' => $statsClass->getName(), 'random_id' => 'chart_' . mt_rand(), 'subnotes' => $usageSubnotes, @@ -468,7 +468,7 @@ public function getCharts(XDUser $user, $chartsKey = 'data') { $nextFieldNameIndex++; $timeseriesColumn = $timeseriesTemplateColumn; - $timeseriesColumn['header'] = "[${resultRecordDimension}] " . $timeseriesColumn['header']; + $timeseriesColumn['header'] = "[{$resultRecordDimension}] " . $timeseriesColumn['header']; $timeseriesColumn['dataIndex'] = $timeseriesDimensionColumnName; $timeseriesColumns[$resultRecordDimension] = $timeseriesColumn; @@ -616,7 +616,7 @@ public function getCharts(XDUser $user, $chartsKey = 'data') { $usageTitleFontSizeInPixels = 16 + $usageFontSize; $usageTitleStyle = array( 'color' => '#000000', - 'size' => "${usageTitleFontSizeInPixels}", + 'size' => "{$usageTitleFontSizeInPixels}", ); // Get the user's report generator chart pool. @@ -714,8 +714,8 @@ public function getCharts(XDUser $user, $chartsKey = 'data') { // Generate the expected IDs for the chart. $usageMetric = $meRequest['data_series_unencoded'][0]['metric']; - $usageChartId = "node=statistic&realm=${usageRealm}&group_by=${usageGroupBy}&statistic=${usageMetric}"; - $usageChartMenuId = "node=group_by&realm=${usageRealm}&group_by=${usageGroupBy}"; + $usageChartId = "node=statistic&realm={$usageRealm}&group_by={$usageGroupBy}&statistic={$usageMetric}"; + $usageChartMenuId = "node=group_by&realm={$usageRealm}&group_by={$usageGroupBy}"; // Remove extraneous x-axis properties. if ($meRequestIsTimeseries) { @@ -768,7 +768,7 @@ public function getCharts(XDUser $user, $chartsKey = 'data') { $currentCategoryRank = $usageOffset + 1; foreach ($meChartCategories as $meChartCategory) { if (!empty($meChartCategory)) { - $usageChartCategories[] = "${currentCategoryRank}. ${meChartCategory}"; + $usageChartCategories[] = "{$currentCategoryRank}. {$meChartCategory}"; } else { $usageChartCategories[] = ''; @@ -847,7 +847,7 @@ function ($drillTarget) { && $usageGroupBy !== 'none' ) { $rank = $meDataSeries['legendrank'] / 3; - $meDataSeries['name'] = "${rank}. " . $meDataSeries['name']; + $meDataSeries['name'] = "{$rank}. " . $meDataSeries['name']; } } @@ -1166,7 +1166,7 @@ private function convertChartRequest(array $usageRequest, $useGivenFormat) { $unencodedMeRequestParams[$meRequestKey] = $meRequestValue; } foreach ($unencodedMeRequestParams as $meRequestKey => $meRequestValue) { - $meRequest["${meRequestKey}_unencoded"] = $meRequestValue; + $meRequest["{$meRequestKey}_unencoded"] = $meRequestValue; $meRequest[$meRequestKey] = urlencode(json_encode($meRequestValue)); } diff --git a/classes/DataWarehouse/Data/BatchDataset.php b/classes/DataWarehouse/Data/BatchDataset.php index 94bc0df45d..62b6a6ce67 100644 --- a/classes/DataWarehouse/Data/BatchDataset.php +++ b/classes/DataWarehouse/Data/BatchDataset.php @@ -173,7 +173,7 @@ function ($field) { * * @return mixed[] */ - public function current() + public function current(): mixed { return $this->currentRow; } @@ -183,7 +183,7 @@ public function current() * * @return int */ - public function key() + public function key(): mixed { return $this->currentRowIndex; } @@ -193,7 +193,7 @@ public function key() * * Fetches the next row. */ - public function next() + public function next(): void { $this->currentRowIndex++; $this->currentRow = $this->getNextRow(); @@ -204,7 +204,7 @@ public function next() * * Executes the underlying raw query. */ - public function rewind() + public function rewind(): void { $this->originalBufferedQuerySetting = $this->dbh->handle()->getAttribute( PDO::MYSQL_ATTR_USE_BUFFERED_QUERY @@ -225,7 +225,7 @@ public function rewind() * * @return bool */ - public function valid() + public function valid(): bool { return $this->currentRow !== false; } diff --git a/classes/DataWarehouse/Data/TimeseriesDataset.php b/classes/DataWarehouse/Data/TimeseriesDataset.php index b507a0ff73..8b21b9fb35 100644 --- a/classes/DataWarehouse/Data/TimeseriesDataset.php +++ b/classes/DataWarehouse/Data/TimeseriesDataset.php @@ -72,7 +72,7 @@ protected function getSeriesIds($limit, $offset) $seriesIds = array(); while($row = $statement->fetch(\PDO::FETCH_ASSOC, \PDO::FETCH_ORI_NEXT)) { - $seriesIds[] = "${row[$groupIdColumn]}"; + $seriesIds[] = "{$row[$groupIdColumn]}"; } return $seriesIds; diff --git a/classes/DataWarehouse/ExportBuilder.php b/classes/DataWarehouse/ExportBuilder.php index b3b97bc7d3..ff21c45412 100644 --- a/classes/DataWarehouse/ExportBuilder.php +++ b/classes/DataWarehouse/ExportBuilder.php @@ -262,6 +262,31 @@ public static function getFormat( return $format; } + /** + * Validates that the format requested by the user is located in the set of formats that are supported and either + * all formats are allowed ( signified by there being no $allowedFormats ) or the requested format was found in the + * set of allowed formats. If valid the requested format is returned. If no requested format is provided then the + * default value will be returned. + * + * @param string $requestedFormat + * @param string $default + * @param array $allowedFormats + * @return string + */ + public static function validateFormat(string $requestedFormat, string $default = 'jsonstore', array $allowedFormats = []): string + { + if (!isset($requestedFormat)) { + return $default; + } + $requestedFormat = strtolower($requestedFormat); + $formatSupported = isset(self::$supported_formats[$requestedFormat]); + $noFormatSubset = count($allowedFormats) === 0; + $requestedFormatInSubset = in_array($requestedFormat, $allowedFormats); + + + return $formatSupported && ($noFormatSubset || $requestedFormatInSubset) ? $requestedFormat : $default; + } + /** * Export data. * diff --git a/classes/DataWarehouse/Query/TimeAggregationUnit.php b/classes/DataWarehouse/Query/TimeAggregationUnit.php index f373088e2d..c273a7a472 100644 --- a/classes/DataWarehouse/Query/TimeAggregationUnit.php +++ b/classes/DataWarehouse/Query/TimeAggregationUnit.php @@ -219,6 +219,11 @@ public static function getRegsiteredAggregationUnits() */ public static function deriveAggregationUnitName($time_period, $start_date, $end_date, $min_aggregation_unit = null) { + // This has been added because `strtolower` no longer supports null values. + if (empty($time_period)) { + $time_period = 'auto'; + } + $time_period = strtolower($time_period); if ($time_period === 'auto') { @@ -264,6 +269,12 @@ public static function deriveAggregationUnitName($time_period, $start_date, $end */ public static function getMaxUnit($unit_1, $unit_2) { + if (is_null($unit_1)) { + $unit_1 = 'null'; + } + if (is_null($unit_2)) { + $unit_2 = 'null'; + } // Convert input units to the expected unit name format. $unit_1_name = strtolower($unit_1); $unit_2_name = strtolower($unit_2); diff --git a/classes/DataWarehouse/Visualization.php b/classes/DataWarehouse/Visualization.php index 61d511f874..e3cdf5ae2c 100644 --- a/classes/DataWarehouse/Visualization.php +++ b/classes/DataWarehouse/Visualization.php @@ -23,7 +23,7 @@ public static function alterBrightness($color, $steps) return ($a << 24) + ($r << 16) + ($g << 8) + $b; } //http://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/ - public static function getColors($count = NULL, $palleteIndex = 0, $includeWhite = true) + public static function getColors($count = null, $palleteIndex = 0, $includeWhite = true) { $ret = array(); $colors = json_decode(COLORS); @@ -39,7 +39,11 @@ public static function getColors($count = NULL, $palleteIndex = 0, $includeWhite } } $ret_count = count($ret); - srand($count); + if ($count === null) { + srand(); + } else { + srand($count); + } if ($count != NULL && $ret_count < $count) { $value = 15; diff --git a/classes/DataWarehouse/Visualization/AggregateChart.php b/classes/DataWarehouse/Visualization/AggregateChart.php index 15215ea334..5fd9d5f313 100644 --- a/classes/DataWarehouse/Visualization/AggregateChart.php +++ b/classes/DataWarehouse/Visualization/AggregateChart.php @@ -1017,7 +1017,10 @@ public function configure( $labelsAllocated = 0; $pieSum = array_sum($yValues); for ($i = 0; $i < count($xValues); $i++) { - if ($isThumbnail || ($labelsAllocated < $labelLimit && (($yValues[$i] / $pieSum) * 100) >= 2.0)) { + if (is_null($yValues[$i])) { + $yValues[$i] = 0.0; + } + if (!is_null($yValues[$i]) && ($isThumbnail || ($labelsAllocated < $labelLimit && (($yValues[$i] / $pieSum) * 100) >= 2.0))) { $label = $xValues[$i]; // Truncate long data labels to improve visibility. if (mb_strlen($label) >= 60) { diff --git a/classes/DataWarehouse/Visualization/TimeseriesChart.php b/classes/DataWarehouse/Visualization/TimeseriesChart.php index 67a7b982c0..4b79454538 100644 --- a/classes/DataWarehouse/Visualization/TimeseriesChart.php +++ b/classes/DataWarehouse/Visualization/TimeseriesChart.php @@ -508,7 +508,14 @@ public function configure( $xValues[] = $start_ts_array[$i]*1000; $dates[] = $start_ts_array[$i]*1000; $yValues[] = $v; - $text[] = number_format($v, $decimals, '.', ','); + + // This bit has been added due to `number_format` no longer supporting passing nulls. + if (is_null($v)) { + $formatted = null; + } else { + $formatted = number_format($v, $decimals, '.', ','); + } + $text[] = $formatted; $seriesValue = array( 'x' => $start_ts_array[$i]*1000, 'y' => $v, diff --git a/classes/ETL/Aggregator/JobsAggregator.php b/classes/ETL/Aggregator/JobsAggregator.php index c6357173af..88dd6b40c7 100644 --- a/classes/ETL/Aggregator/JobsAggregator.php +++ b/classes/ETL/Aggregator/JobsAggregator.php @@ -252,12 +252,12 @@ protected function getDirtyAggregationPeriods($aggregationUnit) if ( null !== $this->currentStartDate ) { $startDate = $this->sourceHandle->quote($this->currentStartDate); - $ranges[] = "d.${aggregationUnit}_end_ts >= UNIX_TIMESTAMP($startDate)"; + $ranges[] = "d.{$aggregationUnit}_end_ts >= UNIX_TIMESTAMP($startDate)"; } if ( null !== $this->currentEndDate ) { $endDate = $this->sourceHandle->quote($this->currentEndDate); - $ranges[] = "d.${aggregationUnit}_start_ts <= UNIX_TIMESTAMP($endDate)"; + $ranges[] = "d.{$aggregationUnit}_start_ts <= UNIX_TIMESTAMP($endDate)"; } $dateRangeSql = implode(" AND ", $ranges); @@ -306,7 +306,7 @@ protected function getDirtyAggregationPeriods($aggregationUnit) * -------------------------------------------------------------------------------- */ - $whereClauses = array("aggregated_${aggregationUnit} = 0"); + $whereClauses = array("aggregated_{$aggregationUnit} = 0"); if ( null !== $this->resourceIdListString ) { $whereClauses[] = "resource_id IN (" . $this->resourceIdListString . ")"; } @@ -317,8 +317,8 @@ protected function getDirtyAggregationPeriods($aggregationUnit) $minMaxJoin = "(\n $minMaxSql\n) js_limits"; - $dateRangeSql = "d.${aggregationUnit}_end_ts >= js_limits.min_start " . - "AND d.${aggregationUnit}_start_ts <= js_limits.max_end"; + $dateRangeSql = "d.{$aggregationUnit}_end_ts >= js_limits.min_start " . + "AND d.{$aggregationUnit}_start_ts <= js_limits.max_end"; } // else ( $this->getEtlOverseerOptions()->isForce() ) @@ -331,16 +331,16 @@ protected function getDirtyAggregationPeriods($aggregationUnit) "SELECT distinct d.id as period_id, d.`year` as year_value, - d.`${aggregationUnit}` as period_value, - d.${aggregationUnit}_start as period_start, - d.${aggregationUnit}_end as period_end, - d.${aggregationUnit}_start_ts as period_start_ts, - d.${aggregationUnit}_end_ts as period_end_ts, + d.`{$aggregationUnit}` as period_value, + d.{$aggregationUnit}_start as period_start, + d.{$aggregationUnit}_end as period_end, + d.{$aggregationUnit}_start_ts as period_start_ts, + d.{$aggregationUnit}_end_ts as period_end_ts, d.hours as period_hours, d.seconds as period_seconds, 0 as period_start_day_id, 0 as period_end_day_id - FROM {$utilitySchema}.${aggregationUnit}s d" . (null !== $minMaxJoin ? ",\n$minMaxJoin" : "" ) . " + FROM {$utilitySchema}.{$aggregationUnit}s d" . (null !== $minMaxJoin ? ",\n$minMaxJoin" : "" ) . " WHERE $dateRangeSql ORDER BY 2 DESC, 3 DESC"; @@ -391,7 +391,7 @@ protected function checkResourceSpecs() from {$sourceSchema}.jobfact where start_time_ts between unix_timestamp(:startDate) and unix_timestamp(:endDate) - and resource_id not in (select distinct resource_id from ${utilitySchema}.resourcespecs where processors is not null)" . + and resource_id not in (select distinct resource_id from {$utilitySchema}.resourcespecs where processors is not null)" . ( null !== $this->resourceIdListString ? " and resource_id IN (" . $this->resourceIdListString . ")" : ""); $params = array( diff --git a/classes/ETL/Aggregator/pdoAggregator.php b/classes/ETL/Aggregator/pdoAggregator.php index 8d41c8662b..e47afe427a 100644 --- a/classes/ETL/Aggregator/pdoAggregator.php +++ b/classes/ETL/Aggregator/pdoAggregator.php @@ -642,12 +642,12 @@ protected function getDirtyAggregationPeriods($aggregationUnit) if ( null !== $this->currentStartDate ) { $startDate = $this->sourceHandle->quote($this->currentStartDate); - $ranges[] = "$startDate <= d.${aggregationUnit}_end"; + $ranges[] = "$startDate <= d.{$aggregationUnit}_end"; } if ( null !== $this->currentEndDate ) { $endDate = $this->sourceHandle->quote($this->currentEndDate); - $ranges[] = "$endDate >= d.${aggregationUnit}_start"; + $ranges[] = "$endDate >= d.{$aggregationUnit}_start"; } if ( 0 != count($ranges) ) { @@ -706,16 +706,16 @@ protected function getDirtyAggregationPeriods($aggregationUnit) "SELECT distinct d.id as period_id, d.`year` as year_value, - d.`${aggregationUnit}` as period_value, - d.${aggregationUnit}_start as period_start, - d.${aggregationUnit}_end as period_end, - d.${aggregationUnit}_start_ts as period_start_ts, - d.${aggregationUnit}_end_ts as period_end_ts, + d.`{$aggregationUnit}` as period_value, + d.{$aggregationUnit}_start as period_start, + d.{$aggregationUnit}_end as period_end, + d.{$aggregationUnit}_start_ts as period_start_ts, + d.{$aggregationUnit}_end_ts as period_end_ts, d.hours as period_hours, d.seconds as period_seconds, $unitIdToStartDayId as period_start_day_id, $unitIdToEndDayId as period_end_day_id - FROM {$utilitySchema}.${aggregationUnit}s d" + FROM {$utilitySchema}.{$aggregationUnit}s d" . (null !== $minMaxJoin ? ",\n$minMaxJoin" : "" ) . (null !== $dateRangeRestrictionSql ? "\nWHERE $dateRangeRestrictionSql" : "" ) . " ORDER BY 2 DESC, 3 DESC"; @@ -922,7 +922,7 @@ protected function _execute($aggregationUnit) // // NOTE: The ETL date range is supported when querying for dirty aggregation periods - $this->logger->info("Aggregate over $numAggregationPeriods ${aggregationUnit}s"); + $this->logger->info("Aggregate over $numAggregationPeriods {$aggregationUnit}s"); if ( ! $enableBatchAggregation ) { diff --git a/classes/ETL/Configuration/EtlConfiguration.php b/classes/ETL/Configuration/EtlConfiguration.php index b909affb21..c82604e8c3 100644 --- a/classes/ETL/Configuration/EtlConfiguration.php +++ b/classes/ETL/Configuration/EtlConfiguration.php @@ -596,27 +596,27 @@ protected function addBaseDirToPaths() * ========================================================================================== */ - public function current() + public function current(): mixed { return current($this->actionOptions); } // current() - public function key() + public function key(): mixed { return key($this->actionOptions); } // key() - public function next() + public function next(): void { - return next($this->actionOptions); + next($this->actionOptions); } // next() - public function rewind() + public function rewind(): void { - return reset($this->actionOptions); + reset($this->actionOptions); } // rewind() - public function valid() + public function valid(): bool { return false !== current($this->actionOptions); } // valid() diff --git a/classes/ETL/DataEndpoint/DirectoryScanner.php b/classes/ETL/DataEndpoint/DirectoryScanner.php index 779342e0a3..b42f62211f 100644 --- a/classes/ETL/DataEndpoint/DirectoryScanner.php +++ b/classes/ETL/DataEndpoint/DirectoryScanner.php @@ -914,7 +914,7 @@ public function verify($dryrun = false, $leaveConnected = false) * @see current() */ - public function current() + public function current(): mixed { if ( null === $this->currentFileIterator ) { return false; @@ -931,7 +931,7 @@ public function current() * @see key() */ - public function key() + public function key(): mixed { if ( null === $this->currentFileIterator ) { return null; @@ -947,7 +947,7 @@ public function key() * @see Iterator::next() */ - public function next() + public function next(): void { if ( null !== $this->currentFileIterator ) { $this->currentFileIterator->next(); @@ -963,7 +963,7 @@ public function next() * @see Iterator::rewind() */ - public function rewind() + public function rewind(): void { $this->handle->rewind(); $this->numFilesScanned = 0; @@ -1004,7 +1004,7 @@ public function rewind() * @see Iterator::valid() */ - public function valid() + public function valid(): bool { // Ensure the handle is valid since there may be no files matching the specified criteria or // we could be at the end of the file list. @@ -1062,7 +1062,7 @@ public function valid() * @see Countable::count() */ - public function count() + public function count(): int { return $this->numRecordsParsed; } diff --git a/classes/ETL/DataEndpoint/Filter/ExternalProcess.php b/classes/ETL/DataEndpoint/Filter/ExternalProcess.php index c9be2ebc24..fa88fc5e69 100644 --- a/classes/ETL/DataEndpoint/Filter/ExternalProcess.php +++ b/classes/ETL/DataEndpoint/Filter/ExternalProcess.php @@ -36,7 +36,7 @@ class ExternalProcess extends \php_user_filter * @var string The name of the filter, populated by PHP */ - public $filtername = null; + public string $filtername = ''; /** * @var object The parameters passed to this filter by stream_filter_prepend() or @@ -49,7 +49,7 @@ class ExternalProcess extends \php_user_filter * logger: Optional logger for displying error messages */ - public $params = null; + public mixed $params; /** * @var array An array containing file descriptors connected to the application. The following @@ -98,7 +98,7 @@ class ExternalProcess extends \php_user_filter * @return PSFS_ERR_FATAL On error. */ - public function filter($in, $out, &$consumed, $closing) + public function filter($in, $out, &$consumed, $closing): int { $retval = PSFS_FEED_ME; @@ -146,7 +146,7 @@ public function filter($in, $out, &$consumed, $closing) * application and opening read and write pipes to the application. */ - public function onCreate() + public function onCreate(): bool { // Verify parameters @@ -219,7 +219,7 @@ public function onCreate() * Cleanup after the filter is closed. */ - public function onClose() + public function onClose(): void { if ($this->pipes[0]) { fclose($this->pipes[0]); diff --git a/classes/ETL/DataEndpoint/aStructuredFile.php b/classes/ETL/DataEndpoint/aStructuredFile.php index f9a4bb3368..0f84567a56 100644 --- a/classes/ETL/DataEndpoint/aStructuredFile.php +++ b/classes/ETL/DataEndpoint/aStructuredFile.php @@ -490,7 +490,7 @@ public function supportsComplexDataRecords() * @see Iterator::current() */ - public function current() + public function current(): mixed { if ( ! $this->valid() ) { return false; @@ -508,7 +508,7 @@ public function current() * @see Iterator::key() */ - public function key() + public function key(): mixed { return key($this->recordList); } @@ -517,7 +517,7 @@ public function key() * @see Iterator::next() */ - public function next() + public function next(): void { next($this->recordList); } @@ -526,7 +526,7 @@ public function next() * @see Iterator::rewind() */ - public function rewind() + public function rewind(): void { reset($this->recordList); } @@ -535,7 +535,7 @@ public function rewind() * @see Iterator::valid() */ - public function valid() + public function valid(): bool { // return isset($this->recordList[$this->recordListPosition]); // Note that we can't check for values that are FALSE because that is a valid @@ -547,7 +547,7 @@ public function valid() * @see Countable::count() */ - public function count() + public function count(): int { return count($this->recordList); } diff --git a/classes/ETL/DbModel/Column.php b/classes/ETL/DbModel/Column.php index 7d44748bb4..fbb548ec99 100644 --- a/classes/ETL/DbModel/Column.php +++ b/classes/ETL/DbModel/Column.php @@ -201,10 +201,18 @@ public function compare(iEntity $cmp) if ( ( - (null === $srcDefault && null === $srcExtra) - || ('current_timestamp' === strtolower($srcDefault) && 'on update current_timestamp' === strtolower($srcExtra)) + ( + null === $srcDefault && + null === $srcExtra + ) + || + ( + !is_null($srcDefault) && !is_null($srcExtra) && + 'current_timestamp' === strtolower($srcDefault) && + 'on update current_timestamp' === strtolower($srcExtra) + ) ) - && ('current_timestamp' != strtolower($destDefault) || null === $destExtra) + && ((!is_null($destDefault) && 'current_timestamp' != strtolower($destDefault)) || null === $destExtra) ) { $this->logCompareFailure('timestamp', "$srcDefault $srcExtra", "$destDefault $destExtra", $this->name); return -1; diff --git a/classes/ETL/DbModel/Entity.php b/classes/ETL/DbModel/Entity.php index dc2212cd08..a7d57dda48 100644 --- a/classes/ETL/DbModel/Entity.php +++ b/classes/ETL/DbModel/Entity.php @@ -82,9 +82,12 @@ class Entity extends Loggable * ------------------------------------------------------------------------------------------ */ - public function __construct($config, $systemQuoteChar = null, LoggerInterface $logger = null) + public function __construct($config, $systemQuoteChar = '`', LoggerInterface $logger = null) { parent::__construct($logger); + if ($systemQuoteChar === null) { + $systemQuoteChar = ''; + } $this->setSystemQuoteChar($systemQuoteChar); // The configuration can be NULL (nothing is initialized), a string assumed to be diff --git a/classes/ETL/Ingestor/RestIngestor.php b/classes/ETL/Ingestor/RestIngestor.php index 367f2ff9eb..9543d912d9 100644 --- a/classes/ETL/Ingestor/RestIngestor.php +++ b/classes/ETL/Ingestor/RestIngestor.php @@ -342,7 +342,7 @@ function ($value) { while ( false !== ( $retval = curl_exec($this->sourceHandle) ) ) { if ( 0 !== curl_errno($this->sourceHandle) ) { - $this->logger->error("${this} Error during REST call: " . curl_error($this->sourceHandle)); + $this->logger->error("{$this} Error during REST call: " . curl_error($this->sourceHandle)); break; } diff --git a/classes/ETL/aOptions.php b/classes/ETL/aOptions.php index f50543bb7d..4822981b8b 100644 --- a/classes/ETL/aOptions.php +++ b/classes/ETL/aOptions.php @@ -268,7 +268,7 @@ public function __isset($property) * ------------------------------------------------------------------------------------------ */ - public function current() + public function current(): mixed { if ( ! $this->valid() ) { return false; @@ -281,7 +281,7 @@ public function current() * ------------------------------------------------------------------------------------------ */ - public function key() + public function key(): mixed { return key($this->options); } // key() @@ -291,7 +291,7 @@ public function key() * ------------------------------------------------------------------------------------------ */ - public function next() + public function next(): void { next($this->options); } // next() @@ -301,7 +301,7 @@ public function next() * ------------------------------------------------------------------------------------------ */ - public function rewind() + public function rewind(): void { reset($this->options); } // rewind() @@ -311,7 +311,7 @@ public function rewind() * ------------------------------------------------------------------------------------------ */ - public function valid() + public function valid(): bool { // Note that we can't check for values that are FALSE because that is a valid // data value. diff --git a/classes/Models/DBObject.php b/classes/Models/DBObject.php index a515089a7d..65dc4c5cb2 100644 --- a/classes/Models/DBObject.php +++ b/classes/Models/DBObject.php @@ -27,6 +27,7 @@ * * @author Ryan Rathsam */ +#[\AllowDynamicProperties] class DBObject { diff --git a/classes/OpenXdmod/Build/Packager.php b/classes/OpenXdmod/Build/Packager.php index f758e900c4..8f435ecc2f 100644 --- a/classes/OpenXdmod/Build/Packager.php +++ b/classes/OpenXdmod/Build/Packager.php @@ -296,10 +296,28 @@ public function createPackage() $this->copyModuleFiles(); $this->createModuleFile(); $this->createInstallScript(); + $this->addEnvFile(); $this->createTarFile(); $this->cleanUp(); } + /** + * Since we're using Symfony we need a .env file now. This function copies it into place. + * + * @return void + * @throws Exception + */ + private function addEnvFile() + { + $fileName = '.env'; + $srcFile = implode(DIRECTORY_SEPARATOR, array($this->srcDir, $fileName)); + $destFile = implode(DIRECTORY_SEPARATOR, array($this->getPackageDir(), $fileName)); + + $this->logger->info(sprintf('Copying %s to %s', $srcFile, $destFile)); + + $this->copyFile($srcFile, $destFile); + } + /** * Create a clone of the source repository. * diff --git a/classes/OpenXdmod/Migration/AclConfigMigration.php b/classes/OpenXdmod/Migration/AclConfigMigration.php index c266e91870..ffbc2ff106 100644 --- a/classes/OpenXdmod/Migration/AclConfigMigration.php +++ b/classes/OpenXdmod/Migration/AclConfigMigration.php @@ -18,9 +18,10 @@ public function execute() $cmd = BIN_DIR . '/acl-config'; $output = shell_exec($cmd); - $hadError = strpos($output, 'error') !== false; - if ($hadError) { + if ($output === false) { + $this->logger->error("Error executing acl-config"); + } else if ($output !== null) { $this->logger->error($output); } } diff --git a/classes/OpenXdmod/Migration/DotEnvConfigMigration.php b/classes/OpenXdmod/Migration/DotEnvConfigMigration.php new file mode 100644 index 0000000000..99f603066e --- /dev/null +++ b/classes/OpenXdmod/Migration/DotEnvConfigMigration.php @@ -0,0 +1,20 @@ +saveIniConfig($settings, 'portal_settings'); + + file_put_contents(BASE_DIR . '/.env', ''); + SymfonyCommandHelper::dumpDotEnv(); } } diff --git a/classes/Realm/Realm.php b/classes/Realm/Realm.php index 40d9001887..9f08859940 100644 --- a/classes/Realm/Realm.php +++ b/classes/Realm/Realm.php @@ -374,29 +374,30 @@ private static function getSortedObjectList( // use late static binding. For other classes use the class name specified unless the // configuration explicitly provides a class name. - $factoryClassName = ('Realm' == $className ? 'static' : $className); - if ( 'Realm' != $className && isset($configObj->class) ) { - if ( ! class_exists($configObj->class) ) { + $factoryClassName = ('Realm' == $className ? Realm::class : $className); + if ('Realm' != $className && isset($configObj->class)) { + if (!class_exists($configObj->class)) { $msg = sprintf("Attempt to instantiate undefined %s class %s", $className, $configObj->class); - if ( null !== $logger ) { + if (null !== $logger) { $logger->error($msg); } throw new \Exception($msg); } $factoryClassName = $configObj->class; - } elseif ( false === strpos($factoryClassName, '\\') && 'static' != $factoryClassName ) { + } elseif (false === strpos($factoryClassName, '\\') && 'static' != $factoryClassName) { $factoryClassName = sprintf('\\%s\\%s', __NAMESPACE__, $factoryClassName); } - $factory = sprintf('%s::factory', $factoryClassName); - - if ( 'Realm' == $className ) { + // When using the string form of a callable, the use of `static::` is deprecated, hence switching to using the array format for a callable. + $factoryCallable = [$factoryClassName, 'factory']; + if ('Realm' == $className) { // The Realm class already has the configuration and does not need it to be passed // to factory(). - $list[$shortName] = forward_static_call($factory, $shortName, $logger); + $list[$shortName] = forward_static_call($factoryCallable, $shortName, null, null, $logger); } else { + // Entities encapsulated by the realm need their config objects - $list[$shortName] = forward_static_call($factory, $shortName, $config, $realmObj, $logger); + $list[$shortName] = forward_static_call($factoryCallable, $shortName, $config, $realmObj, $logger); } } diff --git a/classes/Rest/Controllers/AdminControllerProvider.php b/classes/Rest/Controllers/AdminControllerProvider.php deleted file mode 100644 index f0f562b0c6..0000000000 --- a/classes/Rest/Controllers/AdminControllerProvider.php +++ /dev/null @@ -1,57 +0,0 @@ - - */ -class AdminControllerProvider extends BaseControllerProvider -{ - public function setupRoutes(Application $app, ControllerCollection $controller) - { - $root = $this->prefix; - $class = get_class($this); - - $controller->post("$root/reset_user_tour_viewed", "$class::resetUserTourViewed"); - } - - /** - * @param Request $request - * @param Application $app - * @return \Symfony\Component\HttpFoundation\JsonResponse - * @throws \Exception - */ - public function resetUserTourViewed(Request $request, Application $app) - { - $this->authorize($request, array('mgr')); - $viewedTour = $this->getIntParam($request, 'viewedTour', true); - $selected_user = XDUser::getUserByID($this->getIntParam($request, 'uid', true)); - - if ($selected_user === null) { - throw new BadRequestHttpException('User not found'); - } - - if (!in_array($viewedTour, [0,1])) { - throw new BadRequestHttpException('Invalid data parameter'); - } - - $storage = new \UserStorage($selected_user, 'viewed_user_tour'); - $storage->upsert(0, ['viewedTour' => $viewedTour]); - - return $app->json( - array( - 'success' => true, - 'total' => 1, - 'message' => 'This user will be now be prompted to view the New User Tour the next time they visit XDMoD' - ) - ); - } -} diff --git a/classes/Rest/Controllers/AuthenticationControllerProvider.php b/classes/Rest/Controllers/AuthenticationControllerProvider.php deleted file mode 100644 index 99db7693dc..0000000000 --- a/classes/Rest/Controllers/AuthenticationControllerProvider.php +++ /dev/null @@ -1,155 +0,0 @@ - - */ -class AuthenticationControllerProvider extends BaseControllerProvider -{ - - /** - * AuthenticationControllerProvider constructor. - * - * @param array $params - * - * @throws \Exception if there is a problem retrieving email addresses from configuration files. - */ - public function __construct(array $params = array()) - { - parent::__construct($params); - } - - - /** - * @see aBaseControllerProvider::setupRoutes - */ - public function setupRoutes(Application $app, \Silex\ControllerCollection $controller) - { - $root = $this->prefix; - $controller->post("$root/login", '\Rest\Controllers\AuthenticationControllerProvider::login'); - $controller->post("$root/logout", '\Rest\Controllers\AuthenticationControllerProvider::logout'); - $controller->get("$root/idpredirect", '\Rest\Controllers\AuthenticationControllerProvider::getIdpRedirect'); - $controller->get("$root/jwt-redirect", '\Rest\Controllers\AuthenticationControllerProvider::redirectWithJwt'); - } - - /** - * Provide the user with an authentication token. - * - * The authentication check has already occurred in middleware when this - * function is called, so it does not perform any authentication work. - * - * @param Request $request that will be used to retrieve the user - * @param Application $app used to facilitate json encoding the response. - * @return \Symfony\Component\HttpFoundation\JsonResponse which contains a - * token and the users full name if the login - * attempt is successful. - * @throws \Exception if the user could not be found or if their account - * is disabled. - */ - public function login(Request $request, Application $app) - { - $user = $this->authorize($request); - - $user->postLogin(); - - return $app->json(array( - 'success' => true, - 'results' => array('token' => $user->getSessionToken(), 'name' => $user->getFormalName()) - )); - } - - /** - * Attempt to log out the user identified by the provided token. - * - * @param Request $request that will be used to retrieve the token. - * @param Application $app that will be used to facilitate the json - * encoding of the response. - * @return \Symfony\Component\HttpFoundation\JsonResponse indicating - * that the user has been successfully logged - * out. - */ - public function logout(Request $request, Application $app) - { - $authInfo = Authentication::getAuthenticationInfo($request); - \XDSessionManager::logoutUser($authInfo['token']); - - return $app->json(array( - 'success' => true, - 'message' => 'User logged out successfully' - )); - } - - /** - * Return an IDP redirect URL for SSO login - */ - public function getIdpRedirect(Request $request, Application $app) - { - $auth = new \Authentication\SAML\XDSamlAuthentication(); - - $redirectUrl = $auth->getLoginURL($this->getStringParam($request, 'returnTo', true)); - - if ($redirectUrl === false ) { - throw new \Exception('SSO not configured.'); - } - - return $app->json($redirectUrl); - } - - /** - * If a JupyterHub is configured, redirect to it with a new JSON Web Token in a cookie. - * - * @param Request $request - * @param Application $app - * @return RedirectResponse to the configured JupyterHub root if the user is - * authenticated, otherwise to the sign-in - * screen. - * @throws HttpException if a JupyterHub is not configured. - */ - public function redirectWithJwt(Request $request, Application $app) - { - try { - $jupyterhub_url = xd_utilities\getConfiguration('jupyterhub', 'url'); - } catch (Exception $e) { - throw new HttpException(501, 'JupyterHub not configured.'); - } - try { - $user = $this->authorize($request); - } catch (UnauthorizedHttpException $e) { - return new RedirectResponse('/#jwt-redirect'); - } - list($jwt, $expiration) = JsonWebToken::encode($user->getUsername()); - $cookie = new Cookie( - 'xdmod_jwt', - $jwt, - $expiration, - '/', // path - null, // domain - true, // secure - true // httpOnly - ); - $response = new RedirectResponse($jupyterhub_url); - $response->headers->setCookie($cookie); - return $response; - } -} diff --git a/classes/Rest/Controllers/BaseControllerProvider.php b/classes/Rest/Controllers/BaseControllerProvider.php deleted file mode 100644 index 338cf5837a..0000000000 --- a/classes/Rest/Controllers/BaseControllerProvider.php +++ /dev/null @@ -1,790 +0,0 @@ - - */ -abstract class BaseControllerProvider implements ControllerProviderInterface -{ - - const _USER = '_request_user'; - const _REQUIREMENTS = 'requirements'; - const _URL_GENERATOR = 'url_generator'; - - const KEY_PREFIX = 'prefix'; - - const EXCEPTION_MESSAGE = 'An error was encountered while attempting to process the requested authorization procedure.'; - - protected $prefix; - - /** - * BaseControllerProvider constructor. - * @param array $params - */ - public function __construct(array $params = array()) - { - if (isset($params[self::KEY_PREFIX])) { - $this->prefix = $params[self::KEY_PREFIX]; - } - } - - - /** - * This function is called when the ControllerProvider is 'mount'ed. - * It is also the main entry point for a ControllerProvider and is - * where the 'setupXXX' functions are called from. All of these methods - * default to a no-op except for 'setupRoutes' which must be implemented - * by all child classes. As this is what is at the heart of a - * ControllerProviders' functionality. - * - * @param Application $app - * @return mixed an instance of the controller collection for this application. - */ - public function connect(Application $app) - { - $controller = $app['controllers_factory']; - - $this->setupDefaultValues($app, $controller); - $this->setupConversions($app, $controller); - $this->setupMiddleware($app, $controller); - $this->setupAssertions($app, $controller); - $this->setupRoutes($app, $controller); - - return $controller; - } // connect - - /** - * This function is responsible for the setting up of any routes that this - * ControllerProvider is going to be managing. It *must* be overridden by - * a child class. - * - * @param Application $app - * @param ControllerCollection $controller - * @return null - */ - abstract public function setupRoutes(Application $app, ControllerCollection $controller); - - /** - * This function is responsible for setting any global default values that this - * ControllerProvider may require or provide. It defaults to a no-op - * function if not overridden by a child class. - * - * @param Application $app - * @param ControllerCollection $controller - * @return null - */ - public function setupDefaultValues(Application $app, ControllerCollection $controller) - { - // NO-OP UNLESS OVERRIDDEN - } // setupDefaultValues - - /** - * This function is responsible for setting up any global conversions that may be - * required by this ControllerProvider to function. A conversion - * takes in a user provided value and emits a value of a different type. - * - * For example: - * $app->get('/users/{id}', function($id) { - * // do something with int $id here.... - * })->convert('id', function($id) { return (int) $id; }); - * - * @param Application $app - * @param ControllerCollection $controller - * @return null - */ - public function setupConversions(Application $app, ControllerCollection $controller) - { - // NO-OP UNLESS OVERRIDDEN - } //setupConversions - - /** - * This function is responsible for setting up any global middleware that is particular - * to this ControllerProvider. Middleware can be thought of as functions that - * execute either before, after, or weighted before or weighted after ( dependant - * on how they are set up ). They can be used to provide such functionality as - * logging, authentication or authorization. Middleware can also "short circuit" the - * normal execution of a route by returning a 'Response' object. In this case, the - * next Middleware will not be run nor will the route callback. - * - * @param Application $app - * @param ControllerCollection $controller - * @return null - */ - public function setupMiddleware(Application $app, ControllerCollection $controller) - { - // NO-OP UNLESS OVERRIDDEN - } // setupMiddleware - - /** - * This function is responsible for setting up any global assertions that - * this ControllerProvider will need during it's lifecycle. An assertion - * allows for the use of regex expressions to restrict the matching of - * specific route parameters. - * - * Example: - * $app->get('/blog/{id}', function ($id) { - * // ... - * })->assert('id', '\d+'); - * - * Here we see that the 'id' route parameter must be one or more digits - * ( 0-9 ). If the route does not conform to this regex then it does not - * match. - * - * @param Application $app - * @param ControllerCollection $controller - * @return null - */ - public function setupAssertions(Application $app, ControllerCollection $controller) - { - // NO-OP UNLESS OVERRIDDEN - } // setupAssertions - - /** - * A simple piece of Middleware that ensures that the user making the current - * request is both authenticated and authorized to do so. - * - * @param Request $request that will be used to identify and authorize - * the current user. - * @param Application $app that will be used to facilitate returning a - * json response if information is found to be - * missing. - * @return \Symfony\Component\HttpFoundation\JsonResponse if and only if - * the user is missing a token or an ip. - * - * @throws Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException - */ - public static function authenticate(Request $request, Application $app) - { - // If the user has already been found, skip this search. - if ($request->attributes->has(BaseControllerProvider::_USER)) { - return; - } - - $user = Authentication::authenticateUser($request); - if ($user === null) { - throw new UnauthorizedHttpException('xdmod', 'You must be logged in to access this endpoint.'); // 401 from framework - } else { - $request->attributes->set(BaseControllerProvider::_USER, $user); - } - } - - /** - * Will attempt to authorize the provided users' roles against the - * provided array of role requirements. - * - * If the user is not authorized, an exception will be thrown. - * Otherwise, the function will simply return the authorized user. - * - * @param Request $request A request containing user information - * that is to be considered for authorization. - * @param array $requirements that a users' roles must satisfy to be - * 'authorized'. If not specified, then only - * whether or not the user is logged in will - * be checked. - * @return \XDUser The user that was checked and is authorized according to - * the given parameters. - * - * @throws Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException - * Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException - */ - public function authorize(Request $request, array $requirements = array()) - { - - $user = $this->getUserFromRequest($request); - - // If role requirements were not given, then the only check to perform - // is that the user is not a public user. - $isPublicUser = $user->isPublicUser(); - if (empty($requirements) && $isPublicUser) { - throw new UnauthorizedHttpException('xdmod', self::EXCEPTION_MESSAGE); - } - - $authorized = $user->hasAcls($requirements); - if (!$authorized && !$isPublicUser) { - throw new AccessDeniedHttpException(self::EXCEPTION_MESSAGE); - } elseif (!$authorized && $isPublicUser) { - throw new UnauthorizedHttpException('xdmod', self::EXCEPTION_MESSAGE); - } - - // Return the successfully-authorized user. - return $user; - } - - /** - * Retrieve the XDMoD user from a request object. - * - * @param Request $request The request to retrieve a user from. - * @return \XDUser The user who made the request. - */ - protected function getUserFromRequest(Request $request) - { - return $request->attributes->get(BaseControllerProvider::_USER); - } - - /** - * Attempt to get a parameter value from a request and filter it. - * - * @param Request $request The request to extract the parameter from. - * @param string $name The name of the parameter. - * @param boolean $mandatory If true, an exception will be thrown if - * the parameter is missing from the request. - * @param mixed $default The value to return if the parameter was not - * specified and the parameter is not mandatory. - * @param int $filterId The ID of the filter to use. See filter_var. - * @param mixed $filterOptions The options to use with the filter. - * The filter should be configured so that - * it returns null if conversion is not - * successful. See filter_var. - * @param string $expectedValueType The expected type for the value. - * This is used purely for errors thrown - * when the parameter value is invalid. - * @return mixed If available and valid, the parameter value. - * Otherwise, if it is missing and not mandatory, - * the given default. - * - * @throws BadRequestHttpException If the parameter was not available - * and the parameter was deemed mandatory, - * or if the parameter value is not valid - * according to the given filter. - */ - private function getParam(Request $request, $name, $mandatory, $default, $filterId, $filterOptions, $expectedValueType) - { - // Attempt to extract the parameter value from the request. - $value = $request->get($name, null); - - // If the parameter was not present, throw an exception if it was - // mandatory and return the default if it was not. - if ($value === null) { - if ($mandatory) { - throw new BadRequestHttpException("$name is a required parameter."); - } else { - return $default; - } - } - - // If the parameter is an array, throw an exception. - $invalidMessage = ( - "Invalid value for $name. Must be a(n) $expectedValueType." - ); - if (is_array($value)) { - throw new BadRequestHttpException($invalidMessage); - } - - // Run the found parameter value through the given filter. - if (array_key_exists('flags', $filterOptions)) { - $filterOptions['flags'] |= FILTER_NULL_ON_FAILURE; - } else { - $filterOptions['flags'] = FILTER_NULL_ON_FAILURE; - } - $value = filter_var($value, $filterId, $filterOptions); - - // If the value is invalid, throw an exception. - if ($value === null) { - throw new BadRequestHttpException($invalidMessage); - } - - // Return the filtered value. - return $value; - } - - /** - * Attempt to get an integer parameter value from a request. - * - * @param Request $request The request to extract the parameter from. - * @param string $name The name of the parameter. - * @param boolean $mandatory (Optional) If true, an exception will be - * thrown if the parameter is missing from the - * request. (Defaults to false.) - * @param mixed $default (Optional) The value to return if the - * parameter was not specified and the parameter - * is not mandatory. (Defaults to null.) - * @return mixed If available and valid, the parameter value - * as an integer. Otherwise, if it is missing - * and not mandatory, the given default. - * - * @throws BadRequestHttpException If the parameter was not available - * and the parameter was deemed mandatory, - * or if the parameter value could not be - * converted to an integer. - */ - protected function getIntParam(Request $request, $name, $mandatory = false, $default = null) - { - return $this->getParam( - $request, - $name, - $mandatory, - $default, - FILTER_VALIDATE_INT, - array( - "options" => array( - "default" => null, - ), - ), - "integer" - ); - } - - /** - * Attempt to get a float parameter value from a request. - * - * @param Request $request The request to extract the parameter from. - * @param string $name The name of the parameter. - * @param boolean $mandatory (Optional) If true, an exception will be - * thrown if the parameter is missing from the - * request. (Defaults to false.) - * @param mixed $default (Optional) The value to return if the - * parameter was not specified and the parameter - * is not mandatory. (Defaults to null.) - * @return mixed If available and valid, the parameter value - * as a float. Otherwise, if it is missing - * and not mandatory, the given default. - * - * @throws BadRequestHttpException If the parameter was not available - * and the parameter was deemed mandatory, - * or if the parameter value could not be - * converted to a float. - */ - protected function getFloatParam(Request $request, $name, $mandatory = false, $default = null) - { - return $this->getParam( - $request, - $name, - $mandatory, - $default, - FILTER_VALIDATE_FLOAT, - array( - "options" => array( - "default" => null, - ), - ), - "float" - ); - } - - /** - * Attempt to get a string parameter value from a request. - * - * @param Request $request The request to extract the parameter from. - * @param string $name The name of the parameter. - * @param boolean $mandatory (Optional) If true, an exception will be - * thrown if the parameter is missing from the - * request. (Defaults to false.) - * @param mixed $default (Optional) The value to return if the - * parameter was not specified and the parameter - * is not mandatory. (Defaults to null.) - * @return mixed If available and valid, the parameter value - * as a string. Otherwise, if it is missing - * and not mandatory, the given default. - * - * @throws BadRequestHttpException If the parameter was not available - * and the parameter was deemed mandatory. - */ - protected function getStringParam(Request $request, $name, $mandatory = false, $default = null) - { - return $this->getParam( - $request, - $name, - $mandatory, - $default, - FILTER_DEFAULT, - array(), - "string" - ); - } - - /** - * Attempt to get a boolean parameter value from a request. - * - * @param Request $request The request to extract the parameter from. - * @param string $name The name of the parameter. - * @param boolean $mandatory (Optional) If true, an exception will be - * thrown if the parameter is missing from the - * request. (Defaults to false.) - * @param mixed $default (Optional) The value to return if the - * parameter was not specified and the parameter - * is not mandatory. (Defaults to null.) - * @return mixed If available and valid, the parameter value - * as a boolean. Otherwise, if it is missing - * and not mandatory, the given default. - * - * @throws BadRequestHttpException If the parameter was not available - * and the parameter was deemed mandatory, - * or if the parameter value could not be - * converted to a boolean. - */ - protected function getBooleanParam(Request $request, $name, $mandatory = false, $default = null) - { - return $this->getParam( - $request, - $name, - $mandatory, - $default, - FILTER_CALLBACK, - array( - "options" => function ($value) { - // Run the found parameter value through a boolean filter. - $filteredValue = filter_var( - $value, - FILTER_VALIDATE_BOOLEAN, - array( - "flags" => FILTER_NULL_ON_FAILURE, - ) - ); - - // If the filter converted the string, return the boolean. - if ($filteredValue !== null) { - return $filteredValue; - } - - // Check the value against 'y' for true and 'n' for false. - $lowercaseValue = strtolower($value); - if ($lowercaseValue === 'y') { - return true; - } - if ($lowercaseValue === 'n') { - return false; - } - - // Return null if all conversion attempts failed. - return null; - }, - ), - "boolean" - ); - } - - /** - * Attempt to get a date parameter value from a request where it is - * submitted as a Unix timestamp. - * - * @param Request $request The request to extract the parameter from. - * @param string $name The name of the parameter. - * @param boolean $mandatory (Optional) If true, an exception will be - * thrown if the parameter is missing from the - * request. (Defaults to false.) - * @param mixed $default (Optional) The value to return if the - * parameter was not specified and the parameter - * is not mandatory. (Defaults to null.) - * @return mixed If available and valid, the parameter value - * as a DateTime. Otherwise, if it is missing - * and not mandatory, the given default. - * - * @throws BadRequestHttpException If the parameter was not available - * and the parameter was deemed mandatory, - * or if the parameter value could not be - * converted to a DateTime. - */ - protected function getDateTimeFromUnixParam(Request $request, $name, $mandatory = false, $default = null) - { - return $this->getParam( - $request, - $name, - $mandatory, - $default, - FILTER_CALLBACK, - array( - "options" => function ($value) { - return self::filterDate($value, 'U'); - }, - ), - "Unix timestamp" - ); - } - - /** - * Attempt to get a date parameter value from a request where it is - * submitted as a ISO 8601 (YYYY-MM-DD) date. - * - * @param Request $request The request to extract the parameter from. - * @param string $name The name of the parameter. - * @param boolean $mandatory (Optional) If true, an exception will be - * thrown if the parameter is missing from the - * request. (Defaults to false.) - * @param mixed $default (Optional) The value to return if the - * parameter was not specified and the parameter - * is not mandatory. (Defaults to null.) - * @return mixed If available and valid, the parameter value - * as a DateTime. Otherwise, if it is missing - * and not mandatory, the given default. - * - * @throws BadRequestHttpException If the parameter was not available - * and the parameter was deemed mandatory, - * or if the parameter value could not be - * converted to a DateTime. - */ - protected function getDateFromISO8601Param( - Request $request, - $name, - $mandatory = false, - $default = null - ) { - return $this->getParam( - $request, - $name, - $mandatory, - $default, - FILTER_CALLBACK, - [ - 'options' => function ($value) { - return self::filterDate($value); - }, - ], - 'ISO 8601 Date' - ); - } - - /** - * Get the best match for the acceptable content type for the request, given a - * list of supported content types. - * - * @param Request $request The request from which to extract the data - * @param array $supportedTypes A list of supported MIME types. - * @param string $paramname (Optional) A parameter that will also be - * checked for the accept type, in addition to the Accept header - * contents. This parameter is checked first. - * @return mixed the best matching entry from the $supportedTypes list or null if no supported types - * were allowable. - */ - protected function getAcceptContentType(Request $request, $supportedTypes, $paramname = null) - { - $acceptTypes = $request->getAcceptableContentTypes(); - - if ($paramname !== null) { - $acceptType = $this->getStringParam($request, $paramname); - if ($acceptType !== null) { - array_unshift($acceptTypes, $acceptType); - } - } - - $selectedType = null; - - foreach ($acceptTypes as $type) { - if (in_array($type, $supportedTypes)) { - $selectedType = $type; - break; - } - } - - return $selectedType; - } - - /** - * Helper function that creates a Response object that will result in - * a file download on the client. - * - * @param $content The content of the file that will be sent - * @param $filename The name of the file to send - * @param $mimetype (Optional) The mimetype to set for the file. If omitted - * then the mime type will be guessed using the finfo() fn. - */ - protected function sendAttachment($content, $filename, $mimetype = null) - { - if ($mimetype === null) { - $finfo = new \finfo(FILEINFO_MIME_TYPE); - $mimetype = $finfo->buffer($content); - } - - $response = new Response( - $content, - Response::HTTP_OK, - array('Content-Type' => $mimetype) - ); - $response->headers->set( - 'Content-Disposition', - $response->headers->makeDisposition( - ResponseHeaderBag::DISPOSITION_ATTACHMENT, - $filename - ) - ); - - return $response; - } - - /** - * Retrieve the 'id' property from the supplied array of values. The 'id' - * property is defined by the provided 'selector'. If the 'id' does not - * exist than a default can be supplied, otherwise null will be returned. - * - * @param array $values - * @param string $selector - * @param null $default - * @return null - */ - protected function getId(array $values, $selector = 'dtype', $default = null) - { - if (!isset($values) || !isset($selector) || !is_string($selector)) { - return null; - } - - $idSelector = isset($values[$selector]) ? $values[$selector] : null; - - return isset($idSelector) && isset($values[$idSelector]) ? $values[$idSelector] : $default; - } - - /** ------------------------------------------------------------------------------------------ - * Format a data structure suitable for logging. The logger will convert an array into a JSON - * blob for storage in the database. - * - * @param string $message A general message - * @param \Symfony\Component\HttpFoundation\Request $request - * @param boolean $includeParams if set to - * TRUE include the GET and POST parameters in the log message. - * - * @return array An associative array containing the message, request path, and a block of - * supplemental data including host, port, method, ip address, get & post parameters, etc. - * - * array('message' => , - * 'path' => - * 'data' => array(...) - * ); - * - * Note: We need to define a standard log message with optional additional information. To - * facilitate parsing/display, I suggest that all log entries have: - * message - human readable message - * internal - optional internal-only message describing the error - * path - the rest path or file/method that the exception was thrown - * data - an associative array of optional data specific to the section - * - * ------------------------------------------------------------------------------------------ - */ - - public function formatLogMesssage($message, Request $request, $includeParams = false) - { - $retval = array('message' => $message); - - $authInfo = Authentication::getAuthenticationInfo($request); - $method = $request->getMethod(); - $host = $request->getHost(); - $port = $request->getPort(); - $retval['path'] = $request->getPathInfo(); - - $retval['data'] = array( - 'host' => $host, - 'port' => $port, - 'method' => $method, - 'username' => $authInfo['username'], - 'ip' => $authInfo['ip'], - 'token' => $authInfo['token'], - 'timestamp' => date("Y-m-d H:i:s", $_SERVER['REQUEST_TIME']) - ); - - if ($includeParams) { - $retval['data']['get'] = $request->query->all(); - $retval['data']['post'] = $request->request->all(); - } - - return $retval; - - } - - /** - * Checks that the `$[start|end]Date` values are valid ( `Y-m-d` ) dates and that `$startDate` - * is before `$endDate`. - * - * @param string $startDate the beginning of the date range. - * @param string $endDate the end of the date range. - * @throws BadRequestHttpException if either start or end dates are not provided in the format - * `Y-m-d`, or if the start date is after the end date. - */ - protected function checkDateRange($startDate, $endDate) - { - $startTimestamp = $this->getTimestamp($startDate, 'start_date'); - $endTimestamp = $this->getTimestamp($endDate, 'end_date'); - - if ($startTimestamp > $endTimestamp) { - throw new BadRequestHttpException('Start Date must not be after End Date'); - } - } - - /** - * Attempt to convert the provided string $date value into an equivalent unix timestamp (int). - * - * @param string $date The value to be converted into a DateTime. - * @param string $paramName 'date', The name of the parameter to be included in the exception - * message if validation fails. - * @param string $format 'Y-m-d', The format that `$date` should be in. - * @return int created from the provided `$date` value. - * @throws BadRequestHttpException if the date is not in the form `Y-m-d`. - */ - protected function getTimestamp($date, $paramName = 'date', $format = 'Y-m-d') - { - $parsed = date_parse_from_format($format, $date); - $date = mktime( - $parsed['hour'], - $parsed['minute'], - $parsed['second'], - $parsed['month'], - $parsed['day'], - $parsed['year'] - ); - - if ($date === false || $parsed['error_count'] > 0) { - throw new BadRequestHttpException("Unable to parse $paramName"); - } - - return $date; - } - - /** - * Attempts to convert the provided $value into an instance of DateTime by using the provided $format. If $value is - * unable to be converted into a valid DateTime or if warnings are generated during the process it will be filtered - * and null returned. - * - * @param string $value the date to be validated against the provided $format. Ex: 2027-08-15 - * @param string $format the format to be used when converting the string $value to an instance of DateTime - * - * @return DateTime|null If the creation of a DateTime was successful without warning then an instance of DateTime - * will be returned, else null; - */ - private static function filterDate(string $value, string $format = 'Y-m-d'): ?DateTime - { - $dateTime = DateTime::createFromFormat($format, $value); - - $lastErrors = DateTime::getLastErrors(); - - /* For PHP versions less than 8.2.0 $lastErrors will always be an array w/ the properties: - * warning_count, warnings, error_count, and errors. For versions >= 8.2.0, it will return false if - * there are no errors else it will return as it did pre-8.2.0. - * - * The below `if` statement takes this into account by ensuring that we specifically check for when - * $value_dt is not false ( i.e. is a DateTime object ) but we do have 1 or more warnings which - * indicates that the value of $value_dt is most likely not what it's expected to be. - * - * Example: parsing the date `2024-01-99` results in a $value_dt of: - * DateTime('2024-04-08') - * and a $lastError of: - * [ - * 'warning_count' => 1, - * 'warnings' => [ - * 10 => 'The parsed date was invalid' - * ], - * 'error_count' => 0, - * 'errors' => [] - * ] - */ - if ($dateTime === false || (is_array($lastErrors) && $lastErrors['warning_count'] > 0)) { - return null; - } - return $dateTime; - } -} diff --git a/classes/Rest/Controllers/LegacyControllerProvider.php b/classes/Rest/Controllers/LegacyControllerProvider.php deleted file mode 100644 index efc53ffeba..0000000000 --- a/classes/Rest/Controllers/LegacyControllerProvider.php +++ /dev/null @@ -1,129 +0,0 @@ - array( - 'route' => '/versions/current', - 'method' => 'GET', - ), - ); - - /** - * Convert a URL arguments string from the old REST stack - * into an associative array. - * - * The arguments string must not be decoded for this to work properly. - * This means the string cannot be passed in from Silex's route helper - * functions, as they will automatically decode the string. - * - * Based on the old REST stack's URL parser. - * - * @param string $urlArgumentsString A string of URL arguments, as defined - * by the old REST stack. - * @return array A mapping of URL argument keys to - * their values. - */ - private function parseUrlArguments($urlArgumentsString) - { - // Replace any blocks of slashes with a single slash. - $urlArgumentsString = preg_replace('/\/{2,}/', '/', $urlArgumentsString); - - // Break up the string by key-value pairs. - $urlArgumentPairs = explode('/', $urlArgumentsString); - - // Create an associative array from the pairs. - $urlArguments = array(); - foreach ($urlArgumentPairs as $urlArgumentPair) { - $urlArgumentPairComponents = explode('=', $urlArgumentPair, 2); - - if (count($urlArgumentPairComponents) < 2) { - continue; - } - - $urlArgumentPairComponents = array_map('urldecode', $urlArgumentPairComponents); - $urlArguments[$urlArgumentPairComponents[0]] = $urlArgumentPairComponents[1]; - } - - // Return the associative array. - return $urlArguments; - } - - /** - * @see BaseControllerProvider::setupRoutes - */ - public function setupRoutes(Application $app, \Silex\ControllerCollection $controller) - { - foreach (self::$legacyRouteMapping as $legacyRoute => $legacyRouteOptions) { - $controller->match($legacyRoute, '\Rest\Controllers\LegacyControllerProvider::redirectLegacyRoute') - ->value('legacyRoute', $legacyRoute) - ->value('options', $legacyRouteOptions); - - $controller->match("$legacyRoute/{urlArguments}", '\Rest\Controllers\LegacyControllerProvider::redirectLegacyRoute') - ->assert('urlArguments', '.*') - ->value('legacyRoute', $legacyRoute) - ->value('options', $legacyRouteOptions); - } - } - - /** - * Internally redirect a legacy route to its current equivalent. - * - * @param Request $request The request used to make this call. - * @param Application $app The router application. - * @param string $legacyRoute The route that invoked this function. - * @param array $options A set of options for redirecting the call. - * @return Response The response from the call this route - * was redirected to. - */ - public function redirectLegacyRoute(Request $request, Application $app, $legacyRoute, $options) - { - // Extract the URL arguments from the URL. - // - // This cannot be passed in from the route definition, - // as Silex will apply a different method of URL decoding than the - // old REST stack did. - list($routeMountPoint, $urlArgumentsAndParamsString) = explode($legacyRoute, $request->getRequestUri(), 2); - list($urlArgumentsString, $urlParamsString) = explode('?', $urlArgumentsAndParamsString, 2); - - $urlArguments = $this->parseUrlArguments($urlArgumentsString); - - // Create a sub-request which points to the new route. - $subrequestParams = new ParameterBag(); - $subrequestParams->add($request->query->all()); - $subrequestParams->add($request->request->all()); - $subrequestParams->add($urlArguments); - - $subrequest = Request::create( - '/' . \xd_utilities\getConfiguration('rest', 'version') . $options['route'], - $options['method'], - $subrequestParams->all(), - $request->cookies->all(), - $request->files->all(), - $request->server->all(), - $request->getContent() - ); - - // Launch the sub-request and return the response. - return $app->handle($subrequest, HttpKernelInterface::SUB_REQUEST, false); - } -} diff --git a/classes/Rest/Controllers/MetricExplorerControllerProvider.php b/classes/Rest/Controllers/MetricExplorerControllerProvider.php deleted file mode 100644 index c1fe0dfcc1..0000000000 --- a/classes/Rest/Controllers/MetricExplorerControllerProvider.php +++ /dev/null @@ -1,405 +0,0 @@ -prefix; - $base = '\Rest\Controllers\MetricExplorerControllerProvider'; - - $idConverter = function ($id) { - return (int)$id; - }; - - // QUERY ROUTES ======================================================== - $controller - ->get("$root/queries", "$base::getQueries"); - - $controller - ->get("$root/queries/{id}", "$base::getQueryById") - ->convert('id', $idConverter); - - $controller - ->post("$root/queries", "$base::createQuery"); - - $controller - ->post("$root/queries/{id}", "$base::updateQueryById") - ->convert('id', $idConverter); - - $controller - ->delete("$root/queries/{id}", "$base::deleteQueryById") - ->convert('id', $idConverter); - // QUERY ROUTES ======================================================== - - } - - /** - * Retrieve all of the queries that the requesting user has currently saved. - * - * @param Request $request - * @param Application $app - * @return JsonResponse - */ - public function getQueries(Request $request, Application $app) - { - $action = 'getQueries'; - $payload = array( - 'success' => false, - 'action' => $action, - ); - $statusCode = 401; - - try { - - $user = $this->authorize($request); - if (isset($user)) { - $queries = new \UserStorage($user, self::_QUERIES_STORE); - $data = $queries->get(); - - foreach ($data as &$query) { - $this->removeRoleFromQuery($user, $query); - $query['name'] = htmlspecialchars($query['name'], ENT_COMPAT, 'UTF-8', false); - } - - $payload['data'] = $data; - $payload['success'] = true; - $statusCode = 200; - } else { - $payload['message'] = self::EXCEPTION_MESSAGE; - } - } catch (BadRequestHttpException $e) { - $payload['message'] = $e->getMessage(); - $statusCode = $e->getStatusCode(); - } catch (HttpException $e) { - $payload['message'] = $e->getMessage(); - $statusCode = $e->getStatusCode(); - } catch (\Exception $e) { - $payload['message'] = $e->getMessage(); - $statusCode = 500; - } - - return $app->json( - $payload, - $statusCode - ); - } - - /** - * Retrieve a query's information by unique id for the requesting user. - * - * @param Request $request - * @param Application $app - * @param $id - * @return JsonResponse - */ - public function getQueryById(Request $request, Application $app, $id) - { - $action = 'getQueryById'; - $payload = array( - 'success' => false, - 'action' => $action, - ); - $statusCode = 401; - - try { - $user = $this->authorize($request); - if (isset($user)) { - $queries = new \UserStorage($user, self::_QUERIES_STORE); - - $query = $queries->getById($id); - - if (isset($query)) { - $payload['data'] = $query; - $payload['data']['name'] = htmlspecialchars($query['name'], ENT_COMPAT, 'UTF-8', false); - $payload['success'] = true; - $statusCode = 200; - } else { - $payload['message'] = 'Unable to find the query identified by the provided id: ' . $id; - $statusCode = 404; - } - } else { - $payload['message'] = self::EXCEPTION_MESSAGE; - } - } catch (BadRequestHttpException $e) { - $payload['message'] = $e->getMessage(); - $statusCode = $e->getStatusCode(); - } catch (HttpException $e) { - $payload['message'] = $e->getMessage(); - $statusCode = $e->getStatusCode(); - } catch (\Exception $e) { - $payload['message'] = $e->getMessage(); - $statusCode = 500; - } - - return $app->json( - $payload, - $statusCode - ); - } - - /** - * Create a new query to be stored in the requesting users User Profile. - * - * @param Request $request - * @param Application $app - * @return JsonResponse - */ - public function createQuery(Request $request, Application $app) - { - $action = 'creatQuery'; - $payload = array( - 'success' => false, - 'action' => $action, - ); - $statusCode = 401; - try { - $user = $this->authorize($request); - if (isset($user)) { - $queries = new \UserStorage($user, self::_QUERIES_STORE); - $data = json_decode( - $this->getStringParam($request, 'data', true), - true - ); - $success = $queries->insert($data) != null; - $payload['success'] = $success; - if ($success) { - $payload['success'] = true; - $payload['data'] = $data; - $statusCode = 200; - } else { - $payload['message'] = 'Error creating chart. User is over the chart limit.'; - $statusCode = 500; - } - } else { - $payload['message'] = self::EXCEPTION_MESSAGE; - } - } catch (BadRequestHttpException $e) { - $payload['message'] = $e->getMessage(); - $statusCode = $e->getStatusCode(); - } catch (HttpException $e) { - $payload['message'] = $e->getMessage(); - $statusCode = $e->getStatusCode(); - } catch (\Exception $e) { - $payload['message'] = $e->getMessage(); - $statusCode = 500; - } - - return $app->json( - $payload, - $statusCode - ); - - } - - /** - * Update the query identified by the provided 'id' parameter with the - * values of the following form params ( if provided ): - * - name - * - config - * - timestamp - * - * @param Request $request - * @param Application $app - * @param $id - * @return JsonResponse - */ - public function updateQueryById(Request $request, Application $app, $id) - { - $action = 'updateQuery'; - $payload = array( - 'success' => false, - 'action' => $action, - 'message' => 'success' - ); - $statusCode = 401; - - try { - $user = $this->authorize($request); - if (isset($user)) { - $queries = new \UserStorage($user, self::_QUERIES_STORE); - - $query = $queries->getById($id); - if (isset($query)) { - - - $data = $this->getStringParam($request, 'data'); - if (isset($data)) { - $jsonData = json_decode($data, true); - $name = isset($jsonData['name']) ? $jsonData['name'] : null; - $config = isset($jsonData['config']) ? $jsonData['config'] : null; - $ts = isset($jsonData['ts']) ? $jsonData['ts'] : microtime(true); - } else { - $name = $this->getStringParam($request, 'name'); - $config = $this->getStringParam($request, 'config'); - $ts = $this->getDateTimeFromUnixParam($request, 'ts'); - } - - if (isset($name)) { - $query['name'] = $name; - } - if (isset($config)) { - $query['config'] = $config; - } - if (isset($ts)) { - $query['ts'] = $ts; - } - - $queries->upsert($id, $query); - - // required for the UI to do it's thing. - $total = count($queries->get()); - - // make sure everything is in place for returning to the - // front end. - $payload['total'] = $total; - $payload['success'] = true; - $statusCode = 200; - } else { - $payload['message'] = 'There was no query found for the given id'; - $statusCode = 404; - } - } else { - $payload['message'] = self::EXCEPTION_MESSAGE; - } - } catch (BadRequestHttpException $e) { - $payload['message'] = $e->getMessage(); - $statusCode = $e->getStatusCode(); - } catch (HttpException $e) { - $payload['message'] = $e->getMessage(); - $statusCode = $e->getStatusCode(); - } catch (\Exception $e) { - $payload['message'] = $e->getMessage(); - $statusCode = 500; - } - - return $app->json( - $payload, - $statusCode - ); - } - - /** - * Delete the query identified by the provided form-param 'id'. - * - * @param Request $request - * @param Application $app - * @param $id of the query to be deleted. - * @return JsonResponse - */ - public function deleteQueryById(Request $request, Application $app, $id) - { - $action = 'deleteQueryById'; - $payload = array( - 'success' => false, - 'action' => $action, - 'message' => 'success' - ); - $statusCode = 401; - - try { - $user = $this->authorize($request); - if (isset($user)) { - $queries = new \UserStorage($user, self::_QUERIES_STORE); - $query = $queries->getById($id); - - - if (isset($query)) { - - $before = count($queries->get()); - $after = $queries->delById($id); - $success = $before > $after; - - // make sure everything is in place for returning to the - // front end. - $payload['success'] = $success; - $payload['message'] = $success ? $payload['message'] : 'There was an error removing the query identified by: ' . $id; - - $statusCode = $success ? 200 : 500; - } else { - $payload['message'] = 'There was no query found for the given id'; - $statusCode = 404; - } - } else { - $payload['message'] = self::EXCEPTION_MESSAGE; - } - } catch (BadRequestHttpException $e) { - $payload['message'] = $e->getMessage(); - $statusCode = $e->getStatusCode(); - } catch (HttpException $e) { - $payload['message'] = $e->getMessage(); - $statusCode = $e->getStatusCode(); - } catch (\Exception $e) { - $payload['message'] = $e->getMessage(); - $statusCode = 500; - } - - return $app->json( - $payload, - $statusCode - ); - } - - private function removeRoleFromQuery(XDUser $user, array &$query) - { - // If the query doesn't have a config, stop. - if (!array_key_exists('config', $query)) { - return; - } - - // If the query config doesn't have an active role, stop. - $queryConfig = json_decode($query['config']); - if (!property_exists($queryConfig, 'active_role')) { - return; - } - - // Remove the active role from the query config. - $activeRoleId = $queryConfig->active_role; - unset($queryConfig->active_role); - - // Check whether or not $activeRoleId is an acl name or acl display value. - // ( Old queries may utilize the `display` property). - $activeRole = Acls::getAclByName($activeRoleId); - if ($activeRole === null) { - $activeRole = Acls::getAclByDisplay($activeRoleId); - if ($activeRole !== null) { - $activeRoleId = $activeRole->getName(); - } - } - // Convert the active role into global filters. - MetricExplorer::convertActiveRoleToGlobalFilters($user, $activeRoleId, $queryConfig->global_filters); - - // Store the updated config in the query. - $query['config'] = json_encode($queryConfig); - } -} diff --git a/classes/Rest/Controllers/PersonControllerProvider.php b/classes/Rest/Controllers/PersonControllerProvider.php deleted file mode 100644 index 6ea4ee3b84..0000000000 --- a/classes/Rest/Controllers/PersonControllerProvider.php +++ /dev/null @@ -1,55 +0,0 @@ - - */ -class PersonControllerProvider extends BaseControllerProvider -{ - public function setupRoutes(Application $app, ControllerCollection $controller) - { - $root = $this->prefix; - $class = get_class($this); - $conversions = '\Rest\Utilities\Conversions'; - - $controller - ->get("$root/{id}/organization", "$class::getOrganizationForPerson") - ->assert('id', '(-)?\d+') - ->convert('id', "$conversions::toInt"); - } - - /** - * @param Request $request - * @param Application $app - * @param $id - * @return \Symfony\Component\HttpFoundation\JsonResponse - * @throws \Exception - */ - public function getOrganizationForPerson(Request $request, Application $app, $id) - { - // Ensure that this route is only authorized for users with the 'mgr' role. - $this->authorize($request, array('mgr')); - - return $app->json( - array( - 'success' => true, - 'results' => array( - 'id' => Organizations::getOrganizationIdForPerson($id) - ) - ) - ); - } -} diff --git a/classes/Rest/RestFacade.php b/classes/Rest/RestFacade.php deleted file mode 100644 index 82a0e90a29..0000000000 --- a/classes/Rest/RestFacade.php +++ /dev/null @@ -1,138 +0,0 @@ -attributes->set(BaseControllerProvider::_USER, $options['user']); - - // Determine the type of request by checking if an existing request - // is accessible. If it is, the type of request to launch is a sub-request. - // Otherwise, a master request needs to be launched. - $request_level = HttpKernelInterface::MASTER_REQUEST; - try { - $existing_request = $app['request']; - $request_level = HttpKernelInterface::SUB_REQUEST; - } catch (\Exception $e) { - - } - - // Launch the request. - $response = $app->handle($request, $request_level, $catch); - - // If the response object was requested, return it. - if ($returnResponse) { - return $response; - } - - // Retrieve the encoded content from the response object. - $encodedContent = $response->getContent(); - - // If decoding was not requested, simply return the encoded contents. - if (!$decodeResponse) { - return $encodedContent; - } - - // Get and return the decoded content of the response. - // Use the encoded content as the return value, if all else fails. - $decodedContent = $encodedContent; - - // If the original content is provided in the response, use that as - // the decoded content to return. Otherwise, attempt to decode the - // response contents. - if (property_exists($response, 'originalContent')) { - $decodedContent = $response->originalContent; - } else { - $contentType = $response->headers->get('Content-Type'); - if ($contentType === 'application/json') { - $decodedContent = json_decode($encodedContent); - } - } - - return $decodedContent; - } -} diff --git a/classes/Rest/Utilities/Authentication.php b/classes/Rest/Utilities/Authentication.php deleted file mode 100644 index 2e3aa15f38..0000000000 --- a/classes/Rest/Utilities/Authentication.php +++ /dev/null @@ -1,275 +0,0 @@ -getAccountStatus() == false) { - throw new HttpException(403, 'This account is disabled.'); - } - } elseif (!isset($authInfo['token']) || \xd_utilities\string_begins_with($authInfo['token'], 'public-')) { - $user = XDUser::getPublicUser(); - } else { - $user = self::resolveUserFromToken( - $authInfo['token'], - $authInfo['ip'] - ); - } - - return $user; - - }//authenticateUser - - /** - * This function will attempt to retrieve the currently logged in users' - * authentication information from the provided Request object. If a - * Request object is not provided than an empty array is returned. - * - * @param Request $request - * @return array of the form array( - * 'username' => , - * 'password' => , - * 'token' => , - * 'ip' => ) - */ - public static function getAuthenticationInfo(Request $request) - { - if (!isset($request)) { - return array(); - } - - try { - $useBasicAuth = \xd_utilities\getConfiguration('rest', 'basic_auth') == 'on'; - } catch (Exception $e) { - $useBasicAuth = false; - } - - if ($useBasicAuth) { - $username = $request->headers->get(Authentication::_DEFAULT_AUTH_USER); - $password = $request->headers->get(Authentication::_DEFAULT_AUTH_PASSWORD); - } - - if (!isset($username)) { - $username = $request->get(Authentication::_DEFAULT_USER); - } - if (!isset($password)) { - $password = $request->get(Authentication::_DEFAULT_PASSWORD); - } - - $token = $request->get(Authentication::_DEFAULT_TOKEN); - if (!isset($token)) { - $token = $request->headers->get(Authentication::_DEFAULT_AUTH_TOKEN); - } - if (!isset($token)) { - $token = $request->cookies->get(Authentication::_DEFAULT_COOKIE_TOKEN); - } - - return array( - 'username' => $username, - 'password' => $password, - 'token' => $token, - 'ip' => $request->getClientIp() - ); - } // _getAuthenticationInfo - - /** - * This function will attempt to retrieve an instance of XDUser for the provided token, and ip_address. - * - * @param $token the session token that will be used to retrieve - * the currently logged in user. - * @param $ip_address the ip_address that is associated with this - * authentication attempt. - * @return XDUser - * @throws Exception - * @throws SessionExpiredException - */ - private static function resolveUserFromToken( - $token, - $ip_address - ) { - \xd_security\start_session(); - - // TODO: A REST API should not depend on the consumer - // sending a session cookie. The below block is for - // handling session expiration in the browser. This - // function and the client code should be refactored - // to not depend on session-related code to detect - // expired REST tokens. - - if (!isset($_SESSION['xdInit'])) { - - // Session died (token no longer valid); - $msg = 'Token invalid or expired. ' - . 'You must authenticate before using this call.'; - throw new \SessionExpiredException($msg); - } - - $session_id = session_id(); - - // Without IP restriction ... relaxed, especially for - // very mobile users (in which network hopping is - // frequent) - - $resolver_query = " - SELECT user_id - FROM SessionManager - WHERE session_token = :session_token - AND session_id = :session_id - AND init_time = :init_time - "; - $resolver_query_params = array( - ':session_token' => $token, - ':session_id' => $session_id, - ':init_time' => $_SESSION['xdInit'], - ); - - $pdo = DB::factory('database'); - - $user_check = $pdo->query( - $resolver_query, - $resolver_query_params - ); - - if (count($user_check) === 1) { - $last_active_time = self::getMicrotime(); - - $last_active_query = " - UPDATE SessionManager - SET last_active = :last_active - WHERE session_token = :session_token - AND session_id = :session_id - AND ip_address = :ip_address - AND init_time = :init_time - "; - $pdo->execute($last_active_query, array( - ':last_active' => $last_active_time, - ':session_token' => $token, - ':session_id' => $session_id, - ':ip_address' => $ip_address, - ':init_time' => $_SESSION['xdInit'], - )); - - $user = XDUser::getUserByID($user_check[0]['user_id']); - - if ($user == null) { - throw new \Exception('Invalid token specified'); - } - - return $user; - } else { - - // An error occurred (session is intact, yet a - // corresponding record pertaining to that session - // does not exist in the DB) - throw new \Exception('Invalid token specified'); - } - } - - /** - * Get the current epoch time in micro seconds. - * - * @return int - */ - private static function getMicrotime() - { - list($usec, $sec) = explode(' ', microtime()); - return $usec + $sec; - } -} diff --git a/classes/Rest/Utilities/Conversions.php b/classes/Rest/Utilities/Conversions.php deleted file mode 100644 index b945478571..0000000000 --- a/classes/Rest/Utilities/Conversions.php +++ /dev/null @@ -1,57 +0,0 @@ - $value) { - $result .= "$key: $value, "; - } - $result .= " )"; - } elseif ($isArray && !$isAssociativeArray) { - $result .= "( "; - $result .= implode(", ", $value); - $result .= " )"; - } else { - $result = strval($value); - } - - return $result; - } - - private static function isAssoc($values) - { - if (!is_array($values)) { - return false; - } - return (bool)count(array_filter(array_keys($values), 'is_string')); - } -} diff --git a/classes/Rest/XdmodApplicationFactory.php b/classes/Rest/XdmodApplicationFactory.php deleted file mode 100644 index 59b43a1386..0000000000 --- a/classes/Rest/XdmodApplicationFactory.php +++ /dev/null @@ -1,266 +0,0 @@ -register(new \Silex\Provider\RoutingServiceProvider()); - - // SET: the regex that will be used to filter the API_SYMBOL in a route. - // in this case we're using it as our base url. - $app['controllers']->assert(self::API_SYMBOL, self::API_REGEX); - - // Set the default value for the REST API version to a string - // representing the latest version. - $app['controllers']->value(self::API_SYMBOL, 'latest'); - - $app['logger.db'] = function () { - return \CCR\Log::factory('rest.logger.db', array( - 'console' => false, - 'file' => false, - 'mail' => false, - 'dbLogLevel' => \CCR\Log::INFO - )); - }; - - $app->before(function (Request $request, Application $app) { - $request->attributes->set('timing.start', microtime(true)); - return $app; - }, Application::EARLY_EVENT); - - // SETUP: a before middleware that detects / starts the query debug mode for a request. - $app->before(function (Request $request, Application $app) { - if ($request->query->getBoolean('debug')) { - PDODB::debugOn(); - } - }); - - // SETUP: the authentication Middleware to be run before the route is. - $app->before("\Rest\Controllers\BaseControllerProvider::authenticate", Application::EARLY_EVENT); - - $app->after(function (Request $request, Response $response, Application $app) { - $logger = $app['logger.db']; - - $retval = array('message' => "Route called"); - - $authInfo = Authentication::getAuthenticationInfo($request); - if (!isset($authInfo['username']) && $request->attributes->has(BaseControllerProvider::_USER)) { - $authInfo['username'] = $request->attributes->get(BaseControllerProvider::_USER)->getUsername(); - } - $method = $request->getMethod(); - $host = $request->getHost(); - $port = $request->getPort(); - - // Extracting any POST variables provided in the Request. - $post = array(); - foreach($request->request->getIterator() as $key => $value) { - if (!in_array($key, self::$loggingBlacklist)) { - $post[$key] = ( - is_string($value) - ? json_decode($value, true) - : null - ); - } - } - - // Calculate the amount of time that has elapsed serving this request. - $start = $request->attributes->get('timing.start'); - $end = microtime(true); - $elapsed = $end - $start; - - $referer = null; - if (isset($_SERVER['HTTP_REFERER'])) { - $referer = $_SERVER['HTTP_REFERER']; - } - - // Begin constructing the value to be logged / "returned". - $retval['path'] = $request->getPathInfo(); - $retval['query'] = $request->getQueryString(); - $retval['referer'] = $referer; - $retval['elapsed'] = $elapsed; - $retval['post'] = $post; - $retval['data'] = array( - 'host' => $host, - 'port' => $port, - 'method' => $method, - 'username' => $authInfo['username'], - 'ip' => $authInfo['ip'], - 'token' => $authInfo['token'], - 'timestamp' => date("Y-m-d H:i:s", $_SERVER['REQUEST_TIME']) - ); - - $logger->info('', $retval); - - }, Application::EARLY_EVENT); - - // SETUP: an after middleware that detects the query debug mode and, if true, retrieves - // and returns the collected sql queries / params. - $app->after(function (Request $request, Response $response, Application $app) { - $origin = $request->headers->get('Origin'); - if ($origin !== null) { - try { - $corsDomains = \xd_utilities\getConfiguration('cors', 'domains'); - if (!empty($corsDomains)){ - $allowedCorsDomains = explode(',', $corsDomains); - if (in_array($origin, $allowedCorsDomains)) { - // If these headers change similar updates will need to be made to the `error` section below - $response->headers->set('Access-Control-Allow-Origin', $origin); - $response->headers->set('Access-Control-Allow-Headers', 'x-requested-with, content-type'); - $response->headers->set('Access-Control-Allow-Credentials', 'true'); - $response->headers->set('Vary', 'Origin'); - } - } - } catch (\Exception $e) { - // this catches if the section or config item does not exist - // in that case we just carry on - } - } - if (PDODB::debugging()) { - $debugInfo = PDODB::debugInfo(); - - $contentType = $response->headers->get('content-type', null); - if ('application/json' === strtolower($contentType)) { - $content = $response->getContent(); - $jsonContent = json_decode($content); - - if (is_array($jsonContent)) { - foreach ($jsonContent as $entry) { - if (is_object($entry)) { - $entry->debug = $debugInfo; - break; - } - } - } elseif (is_object($jsonContent)) { - $jsonContent->debug = $debugInfo; - } - - - $response->setContent(json_encode($jsonContent)); - } - } - }); - - // MOUNT: our Controllers ( note: this calls the BaseControllerProvider::connect method ) - // which calls each of the abstract methods in turn. - $versionedPathMountPoint = "/{" . self::API_SYMBOL . "}"; - $unversionedPathMountPoint = ''; - - // Retrieve the rest end point configuration - $restControllers = XdmodConfiguration::assocArrayFactory('rest.json', CONFIG_DIR); - - foreach ($restControllers as $key => $config) { - if (!array_key_exists('prefix', $config) || !array_key_exists('controller', $config)) { - throw new \Exception("Required REST endpoint information (prefix or controller) missing for $key."); - } - - $prefix = $config['prefix']; - $ControllerClass = $config['controller']; - $controller = new $ControllerClass( - array( - 'prefix' => $prefix - ) - ); - - $app->mount($versionedPathMountPoint, $controller); - $app->mount($unversionedPathMountPoint, $controller); - } - - // SETUP: error handler - $app->error(function (\Exception $e, Request $request, $code) { - if($code == 405 && strtoupper($_SERVER['REQUEST_METHOD']) === 'OPTIONS' && array_key_exists('HTTP_ORIGIN', $_SERVER)){ - try { - $corsDomains = \xd_utilities\getConfiguration('cors', 'domains'); - } catch (\Exception $cors) { - $corsDomains = null; - } - if (!empty($corsDomains)){ - $allowedCorsDomains = explode(',', $corsDomains); - $origin = $_SERVER['HTTP_ORIGIN']; - if (in_array($origin, $allowedCorsDomains)) { - // if these headers change we will need to update the `after` above - return new Response( - '', - 204, /* in `$app->error` this value is ignored use header `X-Status-Code` to force a different status code */ - [ - 'X-Status-Code' => 204, - 'Vary' => 'Origin', - 'Access-Control-Allow-Origin' => $origin, - 'Access-Control-Allow-Headers' => 'x-requested-with, content-type', - 'Access-Control-Allow-Credentials' => 'true' - ] - ); - } - } - } - $exceptionOutput = \handle_uncaught_exception($e); - return new Response( - $exceptionOutput['content'], - $exceptionOutput['httpCode'], - $exceptionOutput['headers'] - ); - }); - - // Set the application instance as the global instance and return it. - self::$instance = $app; - return $app; - } // getInstance() -} diff --git a/classes/UserStorage.php b/classes/UserStorage.php index ae5e2cb0bd..95a4cdd914 100644 --- a/classes/UserStorage.php +++ b/classes/UserStorage.php @@ -79,7 +79,7 @@ public function insert(&$data) private function _getnewid(&$storage) { - $newid = ($storage['maxid'] + 1) % PHP_INT_MAX; + $newid = ((int)($storage['maxid'] + 1)) % PHP_INT_MAX; while(isset($storage['data'][$newid])) { $newid = ($newid + 1) % PHP_INT_MAX; } diff --git a/classes/XDChartPool.php b/classes/XDChartPool.php index e96b3ff9bb..0d5ac8a961 100644 --- a/classes/XDChartPool.php +++ b/classes/XDChartPool.php @@ -9,50 +9,50 @@ * of visiting the portal. * */ - + class XDChartPool { private $_user = null; - + private $_user_id = null; private $_person_id = null; private $_user_full_name = null; private $_user_email = null; private $_user_token = null; - + private $_table_name = 'ChartPool'; - + private $_pdo = null; - + // -------------------------------------------- - + public function __construct($user) { - + $this->_pdo = DB::factory('database'); - + $this->_user = $user; $this->_user_id = $user->getUserID(); $this->_person_id = $user->getPersonID(); $this->_user_full_name = $user->getFormalName(); $this->_user_token = $user->getToken(); - - $this->_user_email = (xd_utilities\getConfiguration('general', 'debug_mode') == 'on') ? - xd_utilities\getConfiguration('general', 'debug_recipient') : - $user->getEmailAddress(); - + + $this->_user_email = (xd_utilities\getConfiguration('general', 'debug_mode') == 'on') ? + xd_utilities\getConfiguration('general', 'debug_recipient') : + $user->getEmailAddress(); + }//__construct // -------------------------------------------- - + public function emptyCache() { - + $this->_pdo->execute( 'UPDATE ChartPool SET image_data=NULL WHERE user_id=:user_id', array( 'user_id' => $this->_user_id ) ); - + }//emptyCache public function addChartToQueue($chartIdentifier, $chartTitle, $chartDrillDetails, $chartDateDesc) { @@ -64,40 +64,40 @@ public function addChartToQueue($chartIdentifier, $chartTitle, $chartDrillDetail if (empty($chartTitle)){ throw new Exception("A chart title must be specified"); } - + // Since we are now letting the user have full control over the titles of charts (courtesy of the Metric Explorer), // we need to make sure the title is escaped properly such that the thumbnails in the Report Generator don't break. - + $chartIdentifier = str_replace("title=".$chartTitle, "title=".urlencode($chartTitle), $chartIdentifier); - + if ($this->chartExistsInQueue($chartIdentifier)){ throw new Exception("chart_exists_in_queue"); } - + $insertQuery = "INSERT INTO {$this->_table_name} (user_id, chart_id, chart_title, chart_drill_details, chart_date_description, type) VALUES " . "(:user_id, :chart_id, :chart_title, :chart_drill_details, :chart_date_description, 'image')"; - + $this->_pdo->execute( - $insertQuery, + $insertQuery, array( 'user_id' => $this->_user_id, 'chart_id' => $chartIdentifier, - 'chart_title'=> $chartTitle, + 'chart_title'=> $chartTitle, 'chart_date_description' => $chartDateDesc, 'chart_drill_details'=> $chartDrillDetails ) ); - + }//addChartToQueue - + // -------------------------------------------- - + public function removeChartFromQueue($chartIdentifier) { - + if (empty($chartIdentifier)){ throw new Exception("A chart identifier must be specified"); } - + if (!$this->chartExistsInQueue($chartIdentifier)){ throw new Exception("chart_does_not_exist_in_queue"); } @@ -105,21 +105,26 @@ public function removeChartFromQueue($chartIdentifier) { $this->_pdo->execute("DELETE FROM {$this->_table_name} WHERE user_id = :user_id AND chart_id = :chart_id", array('user_id' => $this->_user_id, 'chart_id' => $chartIdentifier)); }//removeChartFromQueue - + // -------------------------------------------- - + public function chartExistsInQueue($chartIdentifier, $chartTitle = '') { - + if (empty($chartIdentifier)){ //throw new Exception("A chart identifier must be specified"); } + // This has been added due to urlencode no longer supporting nulls ( PHP 8.2 ) + if (is_null($chartTitle)) { + $chartTitle = ''; + } + $chartIdentifier = str_replace("title=".$chartTitle, "title=".urlencode($chartTitle), $chartIdentifier); - + $results = $this->_pdo->query("SELECT * FROM {$this->_table_name} WHERE user_id = :user_id AND chart_id = :chart_id", array('user_id' => $this->_user_id, 'chart_id' => $chartIdentifier)); - + return (count($results) != 0); - + }//chartExistsInQueue - + }//XDChartPool diff --git a/classes/XDController.php b/classes/XDController.php deleted file mode 100644 index 8edc4f177d..0000000000 --- a/classes/XDController.php +++ /dev/null @@ -1,82 +0,0 @@ -_requirements = $requirements; - $this->_registered_operations = array(); - - $this->_operation_handler_directory = $basePath.'/'.substr(basename($_SERVER["SCRIPT_NAME"]), 0, -4); - - }//construct - - // --------------------------- - - public function registerOperation($operation) { - - $this->_registered_operations[] = $operation; - - }//registerOperation - - // --------------------------- - - public function invoke($method, $session_variable = 'xdUser') { - - - xd_security\enforceUserRequirements($this->_requirements, $session_variable); - - // -------------------- - - $params = array('operation' => RESTRICTION_OPERATION); - - $isValid = xd_security\secureCheck($params, $method); - - if (!$isValid) { - $returnData['status'] = 'operation_not_specified'; - $returnData['success'] = false; - $returnData['totalCount'] = 0; - $returnData['message'] = 'operation_not_specified'; - $returnData['data'] = array(); - xd_controller\returnJSON($returnData); - }; - - // -------------------- - - if(!in_array($_REQUEST['operation'], $this->_registered_operations)){ - $returnData['status'] = 'invalid_operation_specified'; - $returnData['success'] = false; - $returnData['totalCount'] = 0; - $returnData['message'] = 'invalid_operation_specified'; - $returnData['data'] = array(); - xd_controller\returnJSON($returnData); - } - - $operation_handler = $this->_operation_handler_directory.'/'.$_REQUEST['operation'].'.php'; - - if (file_exists($operation_handler)){ - include $operation_handler; - } - else{ - $returnData['status'] = 'operation_not_defined'; - $returnData['success'] = false; - $returnData['totalCount'] = 0; - $returnData['message'] = 'operation_not_defined'; - $returnData['data'] = array(); - xd_controller\returnJSON($returnData); - } - - }//invoke - - }//XDController diff --git a/classes/XDReportManager.php b/classes/XDReportManager.php index aa037fa91c..fbed90641c 100644 --- a/classes/XDReportManager.php +++ b/classes/XDReportManager.php @@ -1153,7 +1153,7 @@ public function fetchChartBlob( ); if (file_exists($temp_file)) { - print file_get_contents($temp_file); + return file_get_contents($temp_file); } else { if ( @@ -1206,10 +1206,8 @@ public function fetchChartBlob( file_put_contents($temp_file, $blob); - print $blob; + return $blob; } - - exit; break; case 'chart_pool': $this->ripTransform($insertion_rank, 'did'); @@ -1234,7 +1232,7 @@ public function fetchChartBlob( $temp_file = $this->generateCachedFilename($insertion_rank); if (file_exists($temp_file)) { - print file_get_contents($temp_file); + return file_get_contents($temp_file); } else { $blob = $this->generateChartBlob( @@ -1244,11 +1242,8 @@ public function fetchChartBlob( $insertion_rank['end_date'] ); file_put_contents($temp_file, $blob); - print $blob; + return $blob; } - - exit; - break; case 'report': $iq = $pdo->query( " @@ -1440,7 +1435,6 @@ public function generateChartBlob( $end_date ) { $pdo = DB::factory('database'); - switch ($type) { case 'volatile': $temp_file = $this->generateCachedFilename( @@ -1451,7 +1445,6 @@ public function generateChartBlob( $temp_file = str_replace('.png', '.xrc', $temp_file); $iq = array(); - if (file_exists($temp_file) == true) { $chart_id_config = file($temp_file); $iq[] = array('chart_id' => $chart_id_config[0]); @@ -1465,7 +1458,6 @@ public function generateChartBlob( ); } break; - case 'chart_pool': $iq = $pdo->query( " @@ -1499,7 +1491,7 @@ public function generateChartBlob( } if (count($iq) == 0) { - throw new \Exception("Unable to target chart entry"); + throw new \Exception("Unable to target chart entry $type {$this->_user_id} $insertion_rank ". (new \Exception())->getTraceAsString()); } $chart_id = $iq[0]['chart_id']; diff --git a/classes/XDSessionManager.php b/classes/XDSessionManager.php index 1f6f537b6d..f0293ee1f5 100644 --- a/classes/XDSessionManager.php +++ b/classes/XDSessionManager.php @@ -6,6 +6,7 @@ */ use CCR\DB; +use xd_security\SessionSingleton; /** * Abstracts access to the following schema: @@ -88,10 +89,10 @@ public static function recordLogin($user) ':last_active' => $init_time, )); - $_SESSION['xdInit'] = $init_time; - $_SESSION['xdUser'] = $user_id; - - $_SESSION['session_token'] = $session_token; + $session = SessionSingleton::getSession(); + $session->set('xdInit', $init_time); + $session->set('xdUser', $user_agent); + $session->set('session_token', $session_token); return $session_token; } @@ -107,12 +108,13 @@ public static function logoutUser($token = "") \xd_security\start_session(); } + $session = SessionSingleton::getSession(); // If a session is still active and a token has been specified, // attempt to record the logout in the SessionManager table // (provided the supplied token is still 'valid' and a // corresponding record in SessionManager can be found) - if (isset($_SESSION['xdInit']) && !empty($token)) { + if ($session->get('xdInit') !== null && !empty($token)) { $session_id = session_id(); $ip_address = $_SERVER['REMOTE_ADDR']; @@ -129,10 +131,11 @@ public static function logoutUser($token = "") ':session_token' => $token, ':session_id' => $session_id, ':ip_address' => $ip_address, - ':init_time' => $_SESSION['xdInit'], + ':init_time' => $session->get('xdInit'), )); } + $session->invalidate(); // Drop the session so that any REST calls requiring // authentication (via tokens) trip the first Exception as the // result of invoking resolveUserFromToken($token) @@ -142,10 +145,10 @@ public static function logoutUser($token = "") $auth = new Authentication\SAML\XDSamlAuthentication(); $auth->logout(); } catch (InvalidArgumentException $ex) { - // This will catch when apache or nginx have been set up - // to to have an alternate saml configuration directory - // that does not exist, so we ignore it as saml isnt set - // up and we dont have to do anything with it + // This will catch when apache or nginx have been set up + // to to have an alternate saml configuration directory + // that does not exist, so we ignore it as saml isnt set + // up and we dont have to do anything with it } } diff --git a/classes/XDUser.php b/classes/XDUser.php index f72135e252..01acf103e6 100644 --- a/classes/XDUser.php +++ b/classes/XDUser.php @@ -7,13 +7,19 @@ use Models\Services\Acls; use Models\Services\Organizations; use DataWarehouse\Query\Exceptions\AccessDeniedException; +use xd_security\SessionSingleton; + +use Symfony\Component\PasswordHasher\PasswordHasherInterface; +use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; +use Symfony\Component\Security\Core\User\UserInterface; /** * XDMoD Portal User * * @Class XDUser */ -class XDUser extends CCR\Loggable implements JsonSerializable +class XDUser extends CCR\Loggable implements JsonSerializable, UserInterface, PasswordAuthenticatedUserInterface, LegacyPasswordAuthenticatedUserInterface { private $_pdo; // PDO Handle (set in __construct) @@ -166,6 +172,11 @@ class XDUser extends CCR\Loggable implements JsonSerializable * @var boolean */ private $sticky; + + /** + * @var PasswordHasherInterface + */ + private $hasher; // --------------------------- /* @@ -194,9 +205,11 @@ function __construct( $organization_id = null, $person_id = null, array $ssoAttrs = array(), - $sticky = false - ) { - + $sticky = false, + $hasher = null + ) + { + $this->hasher = $hasher; $this->_pdo = DB::factory('database'); $userCheck = $this->_pdo->query("SELECT id FROM Users WHERE username=:username", array( @@ -267,7 +280,7 @@ function __construct( 'db' => false, 'mail' => false, 'console' => false, - 'file'=> LOG_DIR . "/" . xd_utilities\getConfiguration('general', 'exceptions_logfile') + 'file' => LOG_DIR . "/" . xd_utilities\getConfiguration('general', 'exceptions_logfile') ) ) ); @@ -622,7 +635,6 @@ public static function getUserByID($uid, &$targetInstance = NULL) // the results will be the same. $user->_roles = $user->getAcls(true); - return $user; }//getUserByID @@ -643,7 +655,7 @@ public function setPassword($raw_password) throw new AccessDeniedException("Permission Denied. Only local accounts may have their passwords modified."); } - return $this->_password = $raw_password; + $this->_password = $this->hash($raw_password); }//setPassword // --------------------------- @@ -866,23 +878,6 @@ public function getInsertQuery($updateToken = false, $includePassword = false) return $result; } - /** - * Accepts an array and outputs a meaningful string representation of said - * array. - * - * @param array $array the array that is to be converted into a string. - * - * @return string representation of the array parameter passed in. - */ - public function arrayToString($array = array()) - { - $result = 'Keys [ '; - $result .= implode(', ', array_keys($array)) . ']'; - $result .= 'Values [ '; - $result .= implode(', ', array_values($array)) . ']'; - return $result; - } - // --------------------------- /** @@ -955,15 +950,18 @@ public function saveUser() } $update_data['username'] = $this->_username; - $includePassword = strlen($this->_password) <= CHARLIM_PASSWORD; + $includePassword = empty($this->_password) || strlen($this->_password) <= CHARLIM_PASSWORD; if ($includePassword) { if ($this->_password == "" || is_null($this->_password)) { $update_data['password'] = NULL; + } else if (!$forUpdate) { + $this->_password = $this->hash($this->_password); + $update_data['password'] = $this->_password; } else { - $this->_password = password_hash($this->_password, PASSWORD_DEFAULT); $update_data['password'] = $this->_password; } } + $update_data['email_address'] = ($this->_email); $update_data['first_name'] = ($this->_firstName); $update_data['middle_name'] = ($this->_middleName); @@ -1002,7 +1000,16 @@ public function saveUser() $this->_id = $new_user_id; } } catch (Exception $e) { - throw new Exception("Exception occured while inserting / updating. UpdateToken: [{$this->_update_token}] Query: [$query] data: [{$this->arrayToString($update_data)}]", null, $e); + $values = array_reduce( + array_values($update_data), + function ($carry, $item) { + $carry[] = var_export($item, true); + return $carry; + } + ); + $formattedOutput = 'Keys [' . implode(', ', array_keys($update_data)) . ']'; + $formattedOutput .= 'Values [' . implode(', ', $values) . ']'; + throw new Exception("Exception occured while inserting / updating. UpdateToken: [{$this->_update_token}] Query: [$query] data: [{$formattedOutput}]", null, $e); } /* END: Execute the query */ @@ -1202,15 +1209,14 @@ public function removeUser() // --------------------------- - /* + /** * * @function getUserType; * * @return int (maps to one of the TYPE_* class constants at the top of this file) * */ - - public function getUserType() + public function getUserType(): int { return $this->_user_type; } @@ -1505,7 +1511,7 @@ public function enumAllAvailableRoles() "A PDOException was thrown in 'XDUser::enumAllAvailableRoles'", array( 'exception' => $e, - 'sql'=> $query + 'sql' => $query ) ); @@ -1780,18 +1786,16 @@ public function getActiveOrganization() * */ - public function getRoles($flag = 'informal') + public function getRoles($flag = 'informal'): array { + $roles = array(); if ($flag == 'informal') { $roles = array_reduce($this->_acls, function ($carry, Acl $item) { $carry[] = $item->getName(); return $carry; - }, array()); - return $roles; - } - - if ($flag == 'formal') { + }, $roles); + } elseif ($flag == 'formal') { $query = << $this->_id, )); - $roles = array(); - foreach ($results as $roleSet) { - $roles[$roleSet['display']] = $roleSet['name']; - } - - return $roles; } + return $roles; }//getRoles // --------------------------- @@ -1895,7 +1894,7 @@ function getAllRoles($includePublicRole = false) public function getUserID() { - return (empty($this->_id)) ? '0' : $this->_id; + return (empty($this->_id)) ? 0 : (int)$this->_id; } /* @@ -1911,21 +1910,20 @@ public function getUserID() public function getPersonID($default = FALSE) { - - // NOTE: RESTful services do not operate on the concept of a session, so we need to check for $_SESSION[..] entities using isset - - if (isset($_SESSION['xdUser']) && ($_SESSION['xdUser'] == $this->_id) && ($default == FALSE)) { + $session = SessionSingleton::getSession(); + $xdUserId = $session->get('xdUser'); + if (isset($xdUserId) && ($xdUserId === $this->_id) && ($default == FALSE)) { // The user object pertains to the user logged in.. - - if (isset($_SESSION['assumed_person_id'])) { - return $_SESSION['assumed_person_id']; + $assumedPersonId = $session->get('assumed_person_id'); + if (isset($assumedPersonId)) { + $personID = $assumedPersonId; } + } else { + $personID = (empty($this->_personID)) ? '0' : $this->_personID; } - - return (empty($this->_personID)) ? '0' : $this->_personID; - + return $personID; }//getPersonID // --------------------------- @@ -1993,7 +1991,7 @@ public function getUpdateTimestamp() * (determines the formal description of a role based on its abbreviation) * * @param string $role_abbrev the role abbreviation to use when looking up the formal name. - * @param bool $pubDisplay Determines whether or not to return the public roles `display` + * @param bool $pubDisplay Determines whether or not to return the public roles `display` * property or it's `name` property. We default to true ( i.e. `display` ) as that is the * behavior that currently exists. * @@ -2127,7 +2125,7 @@ public function setAcls(array $acls) */ public function addAcl(Acl $acl, $overwrite = false) { - if ( ( !array_key_exists($acl->getName(), $this->_acls) && !$overwrite ) || + if ((!array_key_exists($acl->getName(), $this->_acls) && !$overwrite) || $overwrite === true ) { $this->_acls[$acl->getName()] = $acl; @@ -2233,7 +2231,7 @@ public static function getUserByUserName($username) * have the data XDMoD is providing to them filtered by a particular * organization. * - * @param string $aclName the name of the acl that should have a + * @param string $aclName the name of the acl that should have a * relationship created for it with the * provided organization. * @param string $organizationId the name of the organization @@ -2254,7 +2252,7 @@ public function addAclOrganization($aclName, $organizationId) $acl = Acls::getAclByName($aclName); - if ( null == $acl) { + if (null == $acl) { throw new Exception("Unable to retrieve acl for: $aclName"); } @@ -2267,7 +2265,7 @@ public function addAclOrganization($aclName, $organizationId) $this->_pdo->execute($cleanUserAclGroupByParameters, array( ':user_id' => $this->_id, - ':acl_id' => $acl->getAclId() + ':acl_id' => $acl->getAclId() )); $populateUserAclGroupByParameters = <<_pdo->execute($populateUserAclGroupByParameters, array( ':user_id' => $this->_id, - ':acl_id' => $acl->getAclId(), - ':value' => $organizationId + ':acl_id' => $acl->getAclId(), + ':value' => $organizationId )); } // addAclOrganization - /** + /** * Specify data which should be serialized to JSON * @link http://php.net/manual/en/jsonserializable.jsonserialize.php * @return mixed data which can be serialized by json_encode, * which is a value of any type other than a resource. * @since 5.4.0 */ - public function jsonSerialize() + public function jsonSerialize(): mixed { $ignored = array( - '_pdo', '_primary_role', '_publicUser', '_timeCreated','_timeUpdated', + '_pdo', '_primary_role', '_publicUser', '_timeCreated', '_timeUpdated', '_timePasswordUpdated', '_token', 'logger' ); $reflection = new ReflectionClass($this); $results = array(); $properties = $reflection->getProperties(); - foreach($properties as $property) { + foreach ($properties as $property) { $name = $property->getName(); if (!in_array($name, $ignored)) { $property->setAccessible(true); @@ -2353,7 +2351,7 @@ public function setOrganizationID($organizationID) * authenticating / authorizing a password reset. If an $expiration value is provided, that will * be used instead of generating one via the 'email_token_expiration' portal settings value. * - * @param int|null $expiration the date after which this rid is considered invalid. + * @param int|null $expiration the date after which this rid is considered invalid. * @return string in the form "userId|expiration|hash" * @throws Exception If there are any missing configuration properties that this function relies * on. These include: email_token_expiration and application_secret. @@ -2427,7 +2425,7 @@ public static function validateRID($rid) } catch (Exception $e) { // If there was an exception then it was because we couldn't find a user by that username // so log the error and return the default information. - $expirationDate = date('Y-m-d H:i:s', $expiration ); + $expirationDate = date('Y-m-d H:i:s', $expiration); $log->debug("Error occurred while validating RID for User: $userId, Expiration: $expirationDate"); } @@ -2439,7 +2437,8 @@ public static function validateRID($rid) * * @throws Exception if there is a problem executing any of the required post logged in steps. */ - public function postLogin() { + public function postLogin() + { if (!$this->isSticky()) { $this->updatePerson(); $this->synchronizeOrganization(); @@ -2469,12 +2468,12 @@ public function synchronizeOrganization() // If we have ssoAttrs available and this user's person's organization is 'Unknown' ( -1 ). // Then go ahead and lookup the organization value from sso. - if ($expectedOrganization == -1 && isset($this->ssoAttrs['organization']) && count($this->ssoAttrs['organization']) > 0) { - $expectedOrganization = Organizations::getIdByName($this->ssoAttrs['organization'][0]); + if ($expectedOrganization == -1 && count($this->ssoAttrs) > 0) { + $expectedOrganization = Organizations::getIdByName($this->getSSOAttribute('organization')); } // If these don't match then the user's organization has been updated. Steps need to be taken. - if ($actualOrganization !== $expectedOrganization) { + if ($actualOrganization != $expectedOrganization) { $originalAcls = $this->getAcls(true); // if the user is currently assigned an acl that interacts with XDMoD's centers ( i.e. @@ -2493,7 +2492,7 @@ public function synchronizeOrganization() $this->setAcls(array()); // Update the user w/ their new set of acls. - foreach($otherAcls as $aclName) { + foreach ($otherAcls as $aclName) { $acl = Acls::getAclByName($aclName); $this->addAcl($acl); } @@ -2541,7 +2540,6 @@ public function synchronizeOrganization() ) ); } - // Update / save the user with their new organization $this->setOrganizationId($expectedOrganization); $this->saveUser(); @@ -2560,14 +2558,15 @@ public function updatePerson() $hasSSO = count($this->ssoAttrs) > 0; if ($currentPersonId == PERSON_ID_UNASSOCIATED && $hasSSO) { - $username = $this->ssoAttrs['username'][0]; - $systemUserName = isset($this->ssoAttrs['system_username']) ? $this->ssoAttrs['system_username'][0] : $username; + $username = $this->getSSOAttribute('username'); + $systemUserName = $this->getSSOAttribute('system_username', $username); $expectedPersonId = \DataWarehouse::getPersonIdFromPII($systemUserName, null); // As long as the identified person is not Unknown and it is different than our current Person Id // go ahead and update this user with the new person & that person's organization. if ($expectedPersonId != PERSON_ID_UNASSOCIATED && $currentPersonId != $expectedPersonId) { $organizationId = Organizations::getOrganizationIdForPerson($expectedPersonId); + $this->setPersonID($expectedPersonId); $this->setOrganizationID($organizationId); @@ -2668,4 +2667,116 @@ function ($value) use ($handle) { return $db->query($query, $params); } // public function getResources($resourceNames = array()) + + public function getPassword(): ?string + { + return $this->_password; + } + + + + public function getSalt(): ?string + { + return null; + } + + public function eraseCredentials() + { + // This function is required for Symfony's UserInterface but we don't actually support erasing a users credentials. + } + + public function getUserIdentifier(): string + { + return $this->_username; + } + + public function __serialize(): array + { + return [ + $this->_id, + $this->_username, + $this->_password, + $this->_email, + $this->_firstName, + $this->_middleName, + $this->_lastName, + $this->_timeCreated, + $this->_timeUpdated, + $this->_timePasswordUpdated, + $this->_roles, + $this->_field_of_science, + $this->_organizationID, + $this->_personID, + $this->_user_type, + $this->_token + ]; + } + + public function __unserialize(array $data): void + { + [ + $this->_id, + $this->_username, + $this->_password, + $this->_email, + $this->_firstName, + $this->_middleName, + $this->_lastName, + $this->_timeCreated, + $this->_timeUpdated, + $this->_timePasswordUpdated, + $this->_roles, + $this->_field_of_science, + $this->_organizationID, + $this->_personID, + $this->_user_type, + $this->_token + ] = $data; + } + + private function hash($password) + { + if (isset($this->hasher)) { + $hashedPassword = $this->hasher->hash($password); + } else { + // Fall back to MD5 if Symfony hasher is not set + $hashedPassword = password_hash($password, PASSWORD_DEFAULT); + } + return $hashedPassword; + } + + /** + * Get an SSO Attribute for this user. Handles when the sso attributes are in the form: + * ``` + * [ + * "attributeName" => "attributeValue" + * ] + * ``` + * + * and when they're in the form: + * ``` + * [ + * "attributeName" => [ + * "attributeValue" + * ] + * ] + * ``` + * The latter is the original format of SSO attributes, while the former is the current. + * + * @param string $attributeName the name of the SSO attribute to return. + * @return mixed|null null is returned if the $attributeName does not exist within this users sso attributes, else + * the value of the sso attribute identified by $attributeName is returned. + */ + private function getSSOAttribute($attributeName, $default = null) + { + $result = null; + if (isset($this->ssoAttrs[$attributeName])) { + if (!is_array($this->ssoAttrs[$attributeName])) { + $result = $this->ssoAttrs[$attributeName]; + } else { + $result = $this->ssoAttrs[$attributeName][0]; + } + } + return isset($result) ? $result : $default; + } }//XDUser diff --git a/classes/Xdmod/NodeSet.php b/classes/Xdmod/NodeSet.php index 7eb22141f1..dcee6d56fc 100644 --- a/classes/Xdmod/NodeSet.php +++ b/classes/Xdmod/NodeSet.php @@ -97,7 +97,7 @@ function ($v) use ($node) { /** * @see Iterator */ - public function current() + public function current(): mixed { if (!$this->valid()) { throw new OutOfBoundsException(); @@ -109,7 +109,7 @@ public function current() /** * @see Iterator */ - public function key() + public function key(): mixed { return $this->position; } @@ -117,7 +117,7 @@ public function key() /** * @see Iterator */ - public function next() + public function next(): void { ++$this->position; } @@ -125,7 +125,7 @@ public function next() /** * @see Iterator */ - public function rewind() + public function rewind(): void { $this->position = 0; } @@ -133,7 +133,7 @@ public function rewind() /** * @see Iterator */ - public function valid() + public function valid(): bool { return isset($this->nodes[$this->position]); } diff --git a/composer.json b/composer.json index 932d2e8a89..bad5b51fa9 100644 --- a/composer.json +++ b/composer.json @@ -1,43 +1,59 @@ { - "extra": { - "COMMENT": "If kassner/log-parser is updated to version >2.1.1, then the call to web_parser->addPattern in classes/ETL/DataEndpoint/WebServerLogFile.php (added in https://github.com/ubccr/xdmod/pull/1816) can be removed along with this 'extra' section." - }, + "type": "project", + "license": "lgpl", + "minimum-stability": "stable", + "prefer-stable": true, "require": { - "php": "^7.4", - "egulias/email-validator": "^1.2", - "google/recaptcha": "~1.1", - "greenlion/php-sql-parser": "~4.2", - "ircmaxell/password-compat": "~1", - "justinrainbow/json-schema": "~5.2", - "jquery/jquery-min-file":"^3.7.1", + "php": "^8.2", + "cirrusidentity/simplesamlphp-module-authoauth2": "^5.2", + "egulias/email-validator": "^4", + "firebase/php-jwt": "^6.10", + "geoip2/geoip2": "^2.12", + "google/recaptcha": "^1.2", + "greenlion/php-sql-parser": "^4.7", + "ircmaxell/password-compat": "^1.0", + "jquery/jquery-min-file": "^3.7.1", + "justinrainbow/json-schema": "^6.3.1", + "kassner/log-parser": "^2.1", "moment/moment-min-file": "^2.13.0", "moment/moment-timezone-min-file": "^0.5.4", - "paragonie/random_compat": "~2.0", - "phpmailer/phpmailer": "~6.9", + "mongodb/mongodb": "1.18.0", + "monolog/monolog": "^3", + "phpdocumentor/reflection-docblock": "^5.6", + "phpmailer/phpmailer": "^6.9", + "phpoffice/phpword": "^1.3.0", + "phpstan/phpdoc-parser": "^2.1", + "plotly/plotly": "^2.29.1", "robrichards/xmlseclibs": "~3.0", "sencha/extjs-gpl": "3.4.*", - "silex/silex": "v2.3.0", - "simplesamlphp/simplesamlphp": "^1.16", - "symfony/polyfill-php56": "~1.11", - "symfony/process": "~2.0", + "simplesamlphp/simplesamlphp": "*", + "simplesamlphp/simplesamlphp-module-authorize": "^1.7", + "swaggest/json-schema": "^0.12.41", + "symfony/asset": "6.4.*", + "symfony/console": "6.4.*", + "symfony/dotenv": "6.4.*", + "symfony/flex": "^1.17|^2", + "symfony/framework-bundle": "6.4.*", + "symfony/monolog-bundle": "^3.8", + "symfony/property-access": "6.4.*", + "symfony/property-info": "6.4.*", + "symfony/proxy-manager-bridge": "6.4.*", + "symfony/runtime": "6.4.*", + "symfony/security-bundle": "6.4.*", + "symfony/serializer": "6.4.*", + "symfony/twig-bundle": "6.4.*", + "symfony/yaml": "6.4.*", "taq/pdooci": "^1.0", "tildeio/rsvpjs-min-file": "^3.0.18", - "ubccr/simplesamlphp-module-authglobus": "^1.3", - "ubccr/simplesamlphp-module-authoidcoauth2": "^1.1", - "phpoffice/phpword": "^1.2.0", - "monolog/monolog": "^1.25", - "plotly/plotly": "^2.29.1", - "kassner/log-parser": "~1.5", - "geoip2/geoip2": "~2.0", - "ua-parser/uap-php": "^3.9", - "mongodb/mongodb": "^1.14", - "firebase/php-jwt": "^6.10" + "ua-parser/uap-php": "^3.9" }, "require-dev": { - "phpunit/phpunit": "^9.0", "ccampbell/chromephp": "^4.1", - "swaggest/json-schema": "^0.12.41", - "dms/phpunit-arraysubset-asserts": "^0.5.0" + "dms/phpunit-arraysubset-asserts": "^0.4.0", + "phpunit/phpunit": "^9.0", + "symfony/maker-bundle": "^1.43", + "symfony/stopwatch": "6.4.*", + "symfony/web-profiler-bundle": "6.4.*" }, "repositories": [ { @@ -201,6 +217,10 @@ "external_libraries/{$name}": [ "zendframework/zendframework-minimal" ] + }, + "public-dir": "html", + "symfony": { + "docker": false } }, "config": { @@ -209,15 +229,22 @@ "secure-http": false, "allow-plugins": { "composer/installers": true, - "simplesamlphp/composer-module-installer": true - } + "composer/package-versions-deprecated": true, + "simplesamlphp/composer-module-installer": true, + "simplesamlphp/composer-xmlprovider-installer": true, + "symfony/flex": true, + "symfony/runtime": true + }, + "preferred-install": { + "*": "dist" + }, + "sort-packages": true }, "autoload": { "files": [ "configuration/constants.php", "libraries/response.php", "libraries/web_message.php", - "libraries/rest.php", "libraries/versioning.php", "libraries/date.php", "libraries/utilities.php", @@ -232,7 +259,6 @@ "classes/XDChartPool.php", "classes/XDStatistics.php", "classes/SessionExpiredException.php", - "classes/XDController.php", "classes/XDUser.php", "classes/UniqueException.php", "classes/XDError.php", @@ -249,7 +275,7 @@ ], "psr-4": { "Authentication\\": "classes/Authentication/", - "CCR\\": "classes/CCR/", + "CCR\\": ["classes/CCR/", "src/"], "Common\\": "classes/Common/", "Configuration\\": "classes/Configuration/", "DataWarehouse\\": "classes/DataWarehouse/", @@ -260,9 +286,28 @@ "Realm\\": "classes/Realm/", "ReportTemplates\\": "classes/ReportTemplates/", "Reports\\": "classes/Reports/", - "Rest\\": "classes/Rest/", "User\\": "classes/User/", "Xdmod\\": "classes/Xdmod/" } + }, + "replace": { + "symfony/polyfill-ctype": "*", + "symfony/polyfill-iconv": "*", + "symfony/polyfill-php72": "*" + }, + "scripts": { + "auto-scripts": { + "cache:clear": "symfony-cmd", + "assets:install %PUBLIC_DIR%": "symfony-cmd" + }, + "post-install-cmd": [ + "@auto-scripts" + ], + "post-update-cmd": [ + "@auto-scripts" + ] + }, + "conflict": { + "symfony/symfony": "*" } } diff --git a/composer.lock b/composer.lock index b065b79539..8154a9811d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,76 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b1f6a77651cfce3d29c0e5a886429ef7", + "content-hash": "48c3bf3bf15474254556e4e884009ea7", "packages": [ + { + "name": "cirrusidentity/simplesamlphp-module-authoauth2", + "version": "v5.2.0", + "source": { + "type": "git", + "url": "https://github.com/cirrusidentity/simplesamlphp-module-authoauth2.git", + "reference": "64edfd95499a47142c5118ea7b80f5ca77d5350d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cirrusidentity/simplesamlphp-module-authoauth2/zipball/64edfd95499a47142c5118ea7b80f5ca77d5350d", + "reference": "64edfd95499a47142c5118ea7b80f5ca77d5350d", + "shasum": "" + }, + "require": { + "ext-json": "*", + "firebase/php-jwt": "^5.5|^6|^7", + "kevinrob/guzzle-cache-middleware": "^4.1.1", + "league/oauth2-client": "^2.7", + "php": "^8.1", + "psr/cache": "^1.0|^2.0|^3.0", + "simplesamlphp/composer-module-installer": "^1.1", + "simplesamlphp/simplesamlphp": "^v2.3", + "symfony/cache": "^7.0|^6.0|^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10", + "psalm/plugin-phpunit": "^0.19.0", + "simplesamlphp/simplesamlphp-test-framework": "^1.7", + "squizlabs/php_codesniffer": "^3.7" + }, + "suggest": { + "patrickbussmann/oauth2-apple": "Used to provide Apple sign in functionality" + }, + "type": "simplesamlphp-module", + "autoload": { + "psr-4": { + "SimpleSAML\\Module\\authoauth2\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "description": "SSP Module for Oauth2 authentication sources", + "keywords": [ + "oauth2", + "oidc", + "simplesamlphp" + ], + "support": { + "issues": "https://github.com/cirrusidentity/simplesamlphp-module-authoauth2/issues", + "source": "https://github.com/cirrusidentity/simplesamlphp-module-authoauth2/tree/v5.2.0" + }, + "time": "2026-02-21T04:09:35+00:00" + }, { "name": "composer/ca-bundle", - "version": "1.5.0", + "version": "1.5.7", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99" + "reference": "d665d22c417056996c59019579f1967dfe5c1e82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", - "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/d665d22c417056996c59019579f1967dfe5c1e82", + "reference": "d665d22c417056996c59019579f1967dfe5c1e82", "shasum": "" }, "require": { @@ -27,8 +83,8 @@ }, "require-dev": { "phpstan/phpstan": "^1.10", - "psr/log": "^1.0", - "symfony/phpunit-bridge": "^4.2 || ^5", + "phpunit/phpunit": "^8 || ^9", + "psr/log": "^1.0 || ^2.0 || ^3.0", "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "type": "library", @@ -64,7 +120,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.5.0" + "source": "https://github.com/composer/ca-bundle/tree/1.5.7" }, "funding": [ { @@ -80,7 +136,7 @@ "type": "tidelift" } ], - "time": "2024-03-15T14:00:32+00:00" + "time": "2025-05-26T15:08:54+00:00" }, { "name": "composer/installers", @@ -233,33 +289,82 @@ ], "time": "2021-09-13T08:19:44+00:00" }, + { + "name": "doctrine/deprecations", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + }, + "time": "2025-04-07T20:06:18+00:00" + }, { "name": "doctrine/lexer", - "version": "1.2.3", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229" + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229", - "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^9.0", - "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.11" + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer" + "Doctrine\\Common\\Lexer\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -291,7 +396,7 @@ ], "support": { "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/1.2.3" + "source": "https://github.com/doctrine/lexer/tree/3.0.1" }, "funding": [ { @@ -307,34 +412,43 @@ "type": "tidelift" } ], - "time": "2022-02-28T11:07:21+00:00" + "time": "2024-02-05T11:56:58+00:00" }, { "name": "egulias/email-validator", - "version": "1.2.17", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "19674b35a0a3456be1b96e137098d31ed386fb61" + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/19674b35a0a3456be1b96e137098d31ed386fb61", - "reference": "19674b35a0a3456be1b96e137098d31ed386fb61", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", "shasum": "" }, "require": { - "doctrine/lexer": "^1.0.1", - "php": ">=5.3.3" + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" }, "require-dev": { - "phpunit/phpunit": "^4.8.36|^7.5.15", - "satooshi/php-coveralls": "^1.0.1" + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, "autoload": { - "psr-0": { - "Egulias\\": "src/" + "psr-4": { + "Egulias\\EmailValidator\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -346,7 +460,7 @@ "name": "Eduardo Gulias Davis" } ], - "description": "A library for validating emails", + "description": "A library for validating emails against several RFCs", "homepage": "https://github.com/egulias/EmailValidator", "keywords": [ "email", @@ -357,32 +471,38 @@ ], "support": { "issues": "https://github.com/egulias/EmailValidator/issues", - "source": "https://github.com/egulias/EmailValidator/tree/1.2" + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" }, - "time": "2020-04-11T12:59:45+00:00" + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" }, { "name": "firebase/php-jwt", - "version": "v6.10.0", + "version": "v6.11.1", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "a49db6f0a5033aef5143295342f1c95521b075ff" + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/a49db6f0a5033aef5143295342f1c95521b075ff", - "reference": "a49db6f0a5033aef5143295342f1c95521b075ff", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", "shasum": "" }, "require": { - "php": "^7.4||^8.0" + "php": "^8.0" }, "require-dev": { - "guzzlehttp/guzzle": "^6.5||^7.4", + "guzzlehttp/guzzle": "^7.4", "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^9.5", - "psr/cache": "^1.0||^2.0", + "psr/cache": "^2.0||^3.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0" }, @@ -420,9 +540,91 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.10.0" + "source": "https://github.com/firebase/php-jwt/tree/v6.11.1" + }, + "time": "2025-04-09T20:32:01+00:00" + }, + { + "name": "friendsofphp/proxy-manager-lts", + "version": "v1.0.18", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/proxy-manager-lts.git", + "reference": "2c8a6cffc3220e99352ad958fe7cf06bf6f7690f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/proxy-manager-lts/zipball/2c8a6cffc3220e99352ad958fe7cf06bf6f7690f", + "reference": "2c8a6cffc3220e99352ad958fe7cf06bf6f7690f", + "shasum": "" + }, + "require": { + "laminas/laminas-code": "~3.4.1|^4.0", + "php": ">=7.1", + "symfony/filesystem": "^4.4.17|^5.0|^6.0|^7.0" + }, + "conflict": { + "laminas/laminas-stdlib": "<3.2.1", + "zendframework/zend-stdlib": "<3.2.1" + }, + "replace": { + "ocramius/proxy-manager": "^2.1" }, - "time": "2023-12-01T16:26:39+00:00" + "require-dev": { + "ext-phar": "*", + "symfony/phpunit-bridge": "^5.4|^6.0|^7.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/Ocramius/ProxyManager", + "name": "ocramius/proxy-manager" + } + }, + "autoload": { + "psr-4": { + "ProxyManager\\": "src/ProxyManager" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + } + ], + "description": "Adding support for a wider range of PHP versions to ocramius/proxy-manager", + "homepage": "https://github.com/FriendsOfPHP/proxy-manager-lts", + "keywords": [ + "aop", + "lazy loading", + "proxy", + "proxy pattern", + "service proxies" + ], + "support": { + "issues": "https://github.com/FriendsOfPHP/proxy-manager-lts/issues", + "source": "https://github.com/FriendsOfPHP/proxy-manager-lts/tree/v1.0.18" + }, + "funding": [ + { + "url": "https://github.com/Ocramius", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ocramius/proxy-manager", + "type": "tidelift" + } + ], + "time": "2024-03-20T12:50:41+00:00" }, { "name": "geoip2/geoip2", @@ -484,33 +686,28 @@ }, { "name": "gettext/gettext", - "version": "v3.6.1", + "version": "v5.7.3", "source": { "type": "git", "url": "https://github.com/php-gettext/Gettext.git", - "reference": "cd3be64443551e3a693117c4bccbe53e36282456" + "reference": "95820f020e4f2f05e0bbaa5603e4c6ec3edc50f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-gettext/Gettext/zipball/cd3be64443551e3a693117c4bccbe53e36282456", - "reference": "cd3be64443551e3a693117c4bccbe53e36282456", + "url": "https://api.github.com/repos/php-gettext/Gettext/zipball/95820f020e4f2f05e0bbaa5603e4c6ec3edc50f1", + "reference": "95820f020e4f2f05e0bbaa5603e4c6ec3edc50f1", "shasum": "" }, "require": { - "gettext/languages": "2.*", - "php": ">=5.3.0" + "gettext/languages": "^2.3", + "php": "^7.2|^8.0" }, "require-dev": { - "illuminate/view": "*", - "symfony/yaml": "~2", - "twig/extensions": "*", - "twig/twig": "*" - }, - "suggest": { - "illuminate/view": "Is necessary if you want to use the Blade extractor", - "symfony/yaml": "Is necessary if you want to use the Yaml extractor/generator", - "twig/extensions": "Is necessary if you want to use the Twig extractor", - "twig/twig": "Is necessary if you want to use the Twig extractor" + "brick/varexporter": "^0.3.5", + "friendsofphp/php-cs-fixer": "^3.2", + "oscarotero/php-cs-fixer-config": "^2.0", + "phpunit/phpunit": "^8.0|^9.0", + "squizlabs/php_codesniffer": "^3.0" }, "type": "library", "autoload": { @@ -531,7 +728,7 @@ } ], "description": "PHP gettext manager", - "homepage": "https://github.com/oscarotero/Gettext", + "homepage": "https://github.com/php-gettext/Gettext", "keywords": [ "JS", "gettext", @@ -542,23 +739,37 @@ ], "support": { "email": "oom@oscarotero.com", - "issues": "https://github.com/oscarotero/Gettext/issues", - "source": "https://github.com/php-gettext/Gettext/tree/v3.6.1" + "issues": "https://github.com/php-gettext/Gettext/issues", + "source": "https://github.com/php-gettext/Gettext/tree/v5.7.3" }, - "time": "2016-08-01T18:09:57+00:00" + "funding": [ + { + "url": "https://paypal.me/oscarotero", + "type": "custom" + }, + { + "url": "https://github.com/oscarotero", + "type": "github" + }, + { + "url": "https://www.patreon.com/misteroom", + "type": "patreon" + } + ], + "time": "2024-12-01T10:18:08+00:00" }, { "name": "gettext/languages", - "version": "2.10.0", + "version": "2.12.1", "source": { "type": "git", "url": "https://github.com/php-gettext/Languages.git", - "reference": "4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab" + "reference": "0b0b0851c55168e1dfb14305735c64019732b5f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-gettext/Languages/zipball/4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab", - "reference": "4d61d67fe83a2ad85959fe6133d6d9ba7dddd1ab", + "url": "https://api.github.com/repos/php-gettext/Languages/zipball/0b0b0851c55168e1dfb14305735c64019732b5f1", + "reference": "0b0b0851c55168e1dfb14305735c64019732b5f1", "shasum": "" }, "require": { @@ -568,7 +779,8 @@ "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.4" }, "bin": [ - "bin/export-plural-rules" + "bin/export-plural-rules", + "bin/import-cldr-data" ], "type": "library", "autoload": { @@ -607,7 +819,7 @@ ], "support": { "issues": "https://github.com/php-gettext/Languages/issues", - "source": "https://github.com/php-gettext/Languages/tree/2.10.0" + "source": "https://github.com/php-gettext/Languages/tree/2.12.1" }, "funding": [ { @@ -619,34 +831,108 @@ "type": "github" } ], - "time": "2022-10-18T15:00:10+00:00" + "time": "2025-03-19T11:14:02+00:00" + }, + { + "name": "gettext/translator", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/php-gettext/Translator.git", + "reference": "8ae0ac79053bcb732a6c584cd86f7a82ef183161" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-gettext/Translator/zipball/8ae0ac79053bcb732a6c584cd86f7a82ef183161", + "reference": "8ae0ac79053bcb732a6c584cd86f7a82ef183161", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.15", + "gettext/gettext": "^5.0.0", + "oscarotero/php-cs-fixer-config": "^1.0", + "phpunit/phpunit": "^8.0", + "squizlabs/php_codesniffer": "^3.0" + }, + "suggest": { + "gettext/gettext": "Is necessary to load and generate array files used by the translator" + }, + "type": "library", + "autoload": { + "psr-4": { + "Gettext\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oscar Otero", + "email": "oom@oscarotero.com", + "homepage": "http://oscarotero.com", + "role": "Developer" + } + ], + "description": "Gettext translator functions", + "homepage": "https://github.com/php-gettext/Translator", + "keywords": [ + "gettext", + "i18n", + "php", + "translator" + ], + "support": { + "email": "oom@oscarotero.com", + "issues": "https://github.com/php-gettext/Translator/issues", + "source": "https://github.com/php-gettext/Translator/tree/v1.2.1" + }, + "funding": [ + { + "url": "https://paypal.me/oscarotero", + "type": "custom" + }, + { + "url": "https://github.com/oscarotero", + "type": "github" + }, + { + "url": "https://www.patreon.com/misteroom", + "type": "patreon" + } + ], + "time": "2025-01-09T09:20:22+00:00" }, { "name": "google/recaptcha", - "version": "1.2.4", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/google/recaptcha.git", - "reference": "614f25a9038be4f3f2da7cbfd778dc5b357d2419" + "reference": "56522c261d2e8c58ba416c90f81a4cd9f2ed89b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/google/recaptcha/zipball/614f25a9038be4f3f2da7cbfd778dc5b357d2419", - "reference": "614f25a9038be4f3f2da7cbfd778dc5b357d2419", + "url": "https://api.github.com/repos/google/recaptcha/zipball/56522c261d2e8c58ba416c90f81a4cd9f2ed89b9", + "reference": "56522c261d2e8c58ba416c90f81a4cd9f2ed89b9", "shasum": "" }, "require": { - "php": ">=5.5" + "php": ">=8" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^2.2.20|^2.15", - "php-coveralls/php-coveralls": "^2.1", - "phpunit/phpunit": "^4.8.36|^5.7.27|^6.59|^7.5.11" + "friendsofphp/php-cs-fixer": "^3.14", + "php-coveralls/php-coveralls": "^2.5", + "phpunit/phpunit": "^10" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "1.3.x-dev" } }, "autoload": { @@ -671,20 +957,20 @@ "issues": "https://github.com/google/recaptcha/issues", "source": "https://github.com/google/recaptcha" }, - "time": "2020-03-31T17:50:54+00:00" + "time": "2025-06-26T22:21:57+00:00" }, { "name": "greenlion/php-sql-parser", - "version": "v4.6.0", + "version": "v4.7.0", "source": { "type": "git", "url": "https://github.com/greenlion/PHP-SQL-Parser.git", - "reference": "f0e4645eb1612f0a295e3d35bda4c7740ae8c366" + "reference": "0cd49149efc5868db9c32d1a09558ea516892586" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/greenlion/PHP-SQL-Parser/zipball/f0e4645eb1612f0a295e3d35bda4c7740ae8c366", - "reference": "f0e4645eb1612f0a295e3d35bda4c7740ae8c366", + "url": "https://api.github.com/repos/greenlion/PHP-SQL-Parser/zipball/0cd49149efc5868db9c32d1a09558ea516892586", + "reference": "0cd49149efc5868db9c32d1a09558ea516892586", "shasum": "" }, "require": { @@ -693,7 +979,7 @@ "require-dev": { "analog/analog": "^1.0.6", "phpunit/phpunit": "^9.5.13", - "squizlabs/php_codesniffer": "^1.5.1" + "squizlabs/php_codesniffer": "^2.8.1" }, "type": "library", "autoload": { @@ -731,30 +1017,60 @@ "issues": "https://github.com/greenlion/PHP-SQL-Parser/issues", "source": "https://github.com/greenlion/PHP-SQL-Parser" }, - "time": "2023-03-09T20:54:23+00:00" + "time": "2024-12-02T12:14:07+00:00" }, { - "name": "ircmaxell/password-compat", - "version": "v1.0.4", + "name": "guzzlehttp/guzzle", + "version": "7.9.3", "source": { "type": "git", - "url": "https://github.com/ircmaxell/password_compat.git", - "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c" + "url": "https://github.com/guzzle/guzzle.git", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ircmaxell/password_compat/zipball/5c5cde8822a69545767f7c7f3058cb15ff84614c", - "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", "shasum": "" }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, "require-dev": { - "phpunit/phpunit": "4.*" + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" }, "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, "autoload": { "files": [ - "lib/password.php" - ] + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -762,71 +1078,317 @@ ], "authors": [ { - "name": "Anthony Ferrara", - "email": "ircmaxell@php.net", - "homepage": "http://blog.ircmaxell.com" + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" } ], - "description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash", - "homepage": "https://github.com/ircmaxell/password_compat", + "description": "Guzzle is a PHP HTTP client library", "keywords": [ - "hashing", - "password" + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" ], "support": { - "issues": "https://github.com/ircmaxell/password_compat/issues", - "source": "https://github.com/ircmaxell/password_compat/tree/v1.0" + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.3" }, - "time": "2014-11-20T16:49:30+00:00" - }, - { - "name": "jaimeperez/twig-configurable-i18n", - "version": "v1.2", - "source": { - "type": "git", - "url": "https://github.com/jaimeperez/twig-configurable-i18n.git", - "reference": "75d4926fd102c9e62219ad7f94a6136d2f2ccd93" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/jaimeperez/twig-configurable-i18n/zipball/75d4926fd102c9e62219ad7f94a6136d2f2ccd93", - "reference": "75d4926fd102c9e62219ad7f94a6136d2f2ccd93", + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-03-27T13:37:11+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", "shasum": "" }, "require": { - "twig/extensions": "^1.3" + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } }, - "type": "project", "autoload": { "psr-4": { - "JaimePerez\\TwigConfigurableI18n\\": "src/" + "GuzzleHttp\\Promise\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-2.1" + "MIT" ], "authors": [ { - "name": "Jaime Perez", - "email": "jaime.perez@uninett.no" + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" } ], - "description": "This is an extension on top of Twig's i18n extension, allowing you to customize which functions to use for translations.", + "description": "Guzzle promises library", "keywords": [ - "extension", - "gettext", - "i18n", - "internationalization", - "translation", - "twig" + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-03-27T12:30:47+00:00" + }, + { + "name": "ircmaxell/password-compat", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://github.com/ircmaxell/password_compat.git", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ircmaxell/password_compat/zipball/5c5cde8822a69545767f7c7f3058cb15ff84614c", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "4.*" + }, + "type": "library", + "autoload": { + "files": [ + "lib/password.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anthony Ferrara", + "email": "ircmaxell@php.net", + "homepage": "http://blog.ircmaxell.com" + } + ], + "description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash", + "homepage": "https://github.com/ircmaxell/password_compat", + "keywords": [ + "hashing", + "password" ], "support": { - "issues": "https://github.com/jaimeperez/twig-configurable-i18n/issues", - "source": "https://github.com/jaimeperez/twig-configurable-i18n" + "issues": "https://github.com/ircmaxell/password_compat/issues", + "source": "https://github.com/ircmaxell/password_compat/tree/v1.0" }, - "abandoned": "simplesamlphp/twig-configurable-i18n", - "time": "2016-10-03T12:34:15+00:00" + "time": "2014-11-20T16:49:30+00:00" }, { "name": "jquery/jquery-min-file", @@ -850,25 +1412,30 @@ }, { "name": "justinrainbow/json-schema", - "version": "v5.2.13", + "version": "6.4.2", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793" + "reference": "ce1fd2d47799bb60668643bc6220f6278a4c1d02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793", - "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/ce1fd2d47799bb60668643bc6220f6278a4c1d02", + "reference": "ce1fd2d47799bb60668643bc6220f6278a4c1d02", "shasum": "" }, "require": { - "php": ">=5.3.3" + "ext-json": "*", + "marc-mabe/php-enum": "^4.0", + "php": "^7.2 || ^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "friendsofphp/php-cs-fixer": "3.3.0", "json-schema/json-schema-test-suite": "1.2.0", - "phpunit/phpunit": "^4.8.35" + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" }, "bin": [ "bin/validate-json" @@ -876,7 +1443,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0.x-dev" + "dev-master": "6.x-dev" } }, "autoload": { @@ -907,44 +1474,38 @@ } ], "description": "A library to validate a json schema.", - "homepage": "https://github.com/justinrainbow/json-schema", + "homepage": "https://github.com/jsonrainbow/json-schema", "keywords": [ "json", "schema" ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/v5.2.13" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.4.2" }, - "time": "2023-09-26T02:20:38+00:00" + "time": "2025-06-03T18:27:04+00:00" }, { "name": "kassner/log-parser", - "version": "1.5.0", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/kassner/log-parser.git", - "reference": "ea846b7edf24a421c5484902b2501c9c8e065796" + "reference": "6a573bd2985c810e3c459d762cabfad1666c37b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kassner/log-parser/zipball/ea846b7edf24a421c5484902b2501c9c8e065796", - "reference": "ea846b7edf24a421c5484902b2501c9c8e065796", + "url": "https://api.github.com/repos/kassner/log-parser/zipball/6a573bd2985c810e3c459d762cabfad1666c37b4", + "reference": "6a573bd2985c810e3c459d762cabfad1666c37b4", "shasum": "" }, "require": { - "php": ">=5.3.4" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.11", - "phpmd/phpmd": "~2.1", - "phpunit/phpunit": "~4.4", - "sebastian/phpcpd": "~2.0" + "php": ">=7.4.0" }, "type": "library", "autoload": { - "psr-0": { - "Kassner": "src" + "psr-4": { + "Kassner\\LogParser\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -955,7 +1516,7 @@ { "name": "Rafael Kassner", "email": "kassner@gmail.com", - "homepage": "http://www.kassner.com.br/", + "homepage": "https://www.kassner.com.br/", "role": "Developer" } ], @@ -971,156 +1532,440 @@ ], "support": { "issues": "https://github.com/kassner/log-parser/issues", - "source": "https://github.com/kassner/log-parser/tree/master" + "source": "https://github.com/kassner/log-parser/tree/2.2.0" }, - "time": "2019-02-04T07:43:30+00:00" + "time": "2024-08-20T20:01:20+00:00" }, { - "name": "maxmind-db/reader", - "version": "v1.11.1", + "name": "kevinrob/guzzle-cache-middleware", + "version": "v4.1.2", "source": { "type": "git", - "url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git", - "reference": "1e66f73ffcf25e17c7a910a1317e9720a95497c7" + "url": "https://github.com/Kevinrob/guzzle-cache-middleware.git", + "reference": "2546d1035e844da378b03e1fb42d3d1cf53187e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/1e66f73ffcf25e17c7a910a1317e9720a95497c7", - "reference": "1e66f73ffcf25e17c7a910a1317e9720a95497c7", + "url": "https://api.github.com/repos/Kevinrob/guzzle-cache-middleware/zipball/2546d1035e844da378b03e1fb42d3d1cf53187e2", + "reference": "2546d1035e844da378b03e1fb42d3d1cf53187e2", "shasum": "" }, "require": { - "php": ">=7.2" - }, - "conflict": { - "ext-maxminddb": "<1.11.1,>=2.0.0" + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "guzzlehttp/promises": "^1.4 || ^2.0", + "guzzlehttp/psr7": "^1.7.0 || ^2.0.0", + "php": ">=7.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "3.*", - "php-coveralls/php-coveralls": "^2.1", - "phpstan/phpstan": "*", - "phpunit/phpcov": ">=6.0.0", - "phpunit/phpunit": ">=8.0.0,<10.0.0", - "squizlabs/php_codesniffer": "3.*" + "cache/array-adapter": "^0.4 || ^0.5 || ^1.0", + "cache/simple-cache-bridge": "^0.1 || ^1.0", + "doctrine/cache": "^1.10", + "illuminate/cache": "^5.0", + "league/flysystem": "^1.0", + "phpunit/phpunit": "^8.5.15 || ^9.5", + "psr/cache": "^1.0", + "symfony/cache": "^4.4 || ^5.0", + "symfony/phpunit-bridge": "^4.4 || ^5.0" }, "suggest": { - "ext-bcmath": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder", - "ext-gmp": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder", - "ext-maxminddb": "A C-based database decoder that provides significantly faster lookups" + "doctrine/cache": "This library has a lot of ready-to-use cache storage (to be used with Kevinrob\\GuzzleCache\\Storage\\DoctrineCacheStorage). Use only versions >=1.4.0 < 2.0.0", + "guzzlehttp/guzzle": "For using this library. It was created for Guzzle6 (but you can use it with any PSR-7 HTTP client).", + "laravel/framework": "To be used with Kevinrob\\GuzzleCache\\Storage\\LaravelCacheStorage", + "league/flysystem": "To be used with Kevinrob\\GuzzleCache\\Storage\\FlysystemStorage", + "psr/cache": "To be used with Kevinrob\\GuzzleCache\\Storage\\Psr6CacheStorage", + "psr/simple-cache": "To be used with Kevinrob\\GuzzleCache\\Storage\\Psr16CacheStorage" }, "type": "library", "autoload": { "psr-4": { - "MaxMind\\Db\\": "src/MaxMind/Db" + "Kevinrob\\GuzzleCache\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "Apache-2.0" + "MIT" ], "authors": [ { - "name": "Gregory J. Oschwald", - "email": "goschwald@maxmind.com", - "homepage": "https://www.maxmind.com/" + "name": "Kevin Robatel", + "email": "kevinrob2@gmail.com", + "homepage": "https://github.com/Kevinrob" } ], - "description": "MaxMind DB Reader API", - "homepage": "https://github.com/maxmind/MaxMind-DB-Reader-php", + "description": "A HTTP/1.1 Cache for Guzzle 6. It's a simple Middleware to be added in the HandlerStack. (RFC 7234)", + "homepage": "https://github.com/Kevinrob/guzzle-cache-middleware", "keywords": [ - "database", - "geoip", - "geoip2", - "geolocation", - "maxmind" + "Etag", + "Flysystem", + "Guzzle", + "cache", + "cache-control", + "doctrine", + "expiration", + "guzzle6", + "handler", + "http", + "http 1.1", + "middleware", + "performance", + "php", + "promise", + "psr6", + "psr7", + "rfc7234", + "validation" ], "support": { - "issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues", - "source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.11.1" + "issues": "https://github.com/Kevinrob/guzzle-cache-middleware/issues", + "source": "https://github.com/Kevinrob/guzzle-cache-middleware/tree/v4.1.2" }, - "time": "2023-12-02T00:09:23+00:00" + "time": "2023-06-14T11:19:21+00:00" }, { - "name": "maxmind/web-service-common", - "version": "v0.9.0", + "name": "laminas/laminas-code", + "version": "4.16.0", "source": { "type": "git", - "url": "https://github.com/maxmind/web-service-common-php.git", - "reference": "4dc5a3e8df38aea4ca3b1096cee3a038094e9b53" + "url": "https://github.com/laminas/laminas-code.git", + "reference": "1793e78dad4108b594084d05d1fb818b85b110af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maxmind/web-service-common-php/zipball/4dc5a3e8df38aea4ca3b1096cee3a038094e9b53", - "reference": "4dc5a3e8df38aea4ca3b1096cee3a038094e9b53", + "url": "https://api.github.com/repos/laminas/laminas-code/zipball/1793e78dad4108b594084d05d1fb818b85b110af", + "reference": "1793e78dad4108b594084d05d1fb818b85b110af", "shasum": "" }, "require": { - "composer/ca-bundle": "^1.0.3", - "ext-curl": "*", - "ext-json": "*", - "php": ">=7.2" + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "3.*", - "phpstan/phpstan": "*", - "phpunit/phpunit": "^8.0 || ^9.0", - "squizlabs/php_codesniffer": "3.*" + "doctrine/annotations": "^2.0.1", + "ext-phar": "*", + "laminas/laminas-coding-standard": "^3.0.0", + "laminas/laminas-stdlib": "^3.18.0", + "phpunit/phpunit": "^10.5.37", + "psalm/plugin-phpunit": "^0.19.0", + "vimeo/psalm": "^5.15.0" + }, + "suggest": { + "doctrine/annotations": "Doctrine\\Common\\Annotations >=1.0 for annotation features", + "laminas/laminas-stdlib": "Laminas\\Stdlib component" }, "type": "library", "autoload": { "psr-4": { - "MaxMind\\Exception\\": "src/Exception", - "MaxMind\\WebService\\": "src/WebService" + "Laminas\\Code\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "Apache-2.0" + "BSD-3-Clause" ], - "authors": [ - { - "name": "Gregory Oschwald", - "email": "goschwald@maxmind.com" - } + "description": "Extensions to the PHP Reflection API, static code scanning, and code generation", + "homepage": "https://laminas.dev", + "keywords": [ + "code", + "laminas", + "laminasframework" ], - "description": "Internal MaxMind Web Service API", - "homepage": "https://github.com/maxmind/web-service-common-php", "support": { - "issues": "https://github.com/maxmind/web-service-common-php/issues", - "source": "https://github.com/maxmind/web-service-common-php/tree/v0.9.0" + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-code/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-code/issues", + "rss": "https://github.com/laminas/laminas-code/releases.atom", + "source": "https://github.com/laminas/laminas-code" }, - "time": "2022-03-28T17:43:20+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2024-11-20T13:15:13+00:00" }, { - "name": "moment/moment-min-file", - "version": "2.13.0", + "name": "league/oauth2-client", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-client.git", + "reference": "26e8c5da4f3d78cede7021e09b1330a0fc093d5e" + }, "dist": { - "type": "file", - "url": "https://raw.githubusercontent.com/moment/moment/2.13.0/min/moment.min.js", - "shasum": "a8ca7eea2616fa92e2e85ba6291af6ea012fd190" + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/26e8c5da4f3d78cede7021e09b1330a0fc093d5e", + "reference": "26e8c5da4f3d78cede7021e09b1330a0fc093d5e", + "shasum": "" }, "require": { - "composer/installers": "~1.0" + "ext-json": "*", + "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", + "php": "^7.1 || >=8.0.0 <8.6.0" }, - "type": "vanilla-plugin", - "extra": { - "installer-name": "moment" + "require-dev": { + "mockery/mockery": "^1.3.5", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "^3.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "homepage": "https://momentjs.com" - }, - { - "name": "moment/moment-timezone-min-file", - "version": "0.5.4", - "dist": { - "type": "file", - "url": "https://raw.githubusercontent.com/moment/moment-timezone/0.5.4/builds/moment-timezone-with-data.min.js", - "shasum": "39b9fccc20863c23f19524a756d75cfef2ff9cbe" - }, - "require": { - "composer/installers": "~1.0" + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Woody Gilk", + "homepage": "https://github.com/shadowhand", + "role": "Contributor" + } + ], + "description": "OAuth 2.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "identity", + "idp", + "oauth", + "oauth2", + "single sign on" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-client/issues", + "source": "https://github.com/thephpleague/oauth2-client/tree/2.9.0" + }, + "time": "2025-11-25T22:17:17+00:00" + }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.1", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.1" + }, + "time": "2024-11-28T04:54:44+00:00" + }, + { + "name": "maxmind-db/reader", + "version": "v1.12.1", + "source": { + "type": "git", + "url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git", + "reference": "815939e006b7e68062b540ec9e86aaa8be2b6ce4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/815939e006b7e68062b540ec9e86aaa8be2b6ce4", + "reference": "815939e006b7e68062b540ec9e86aaa8be2b6ce4", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "conflict": { + "ext-maxminddb": "<1.11.1 || >=2.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.*", + "phpstan/phpstan": "*", + "phpunit/phpunit": ">=8.0.0,<10.0.0", + "squizlabs/php_codesniffer": "3.*" + }, + "suggest": { + "ext-bcmath": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder", + "ext-gmp": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder", + "ext-maxminddb": "A C-based database decoder that provides significantly faster lookups" + }, + "type": "library", + "autoload": { + "psr-4": { + "MaxMind\\Db\\": "src/MaxMind/Db" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Gregory J. Oschwald", + "email": "goschwald@maxmind.com", + "homepage": "https://www.maxmind.com/" + } + ], + "description": "MaxMind DB Reader API", + "homepage": "https://github.com/maxmind/MaxMind-DB-Reader-php", + "keywords": [ + "database", + "geoip", + "geoip2", + "geolocation", + "maxmind" + ], + "support": { + "issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues", + "source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.12.1" + }, + "time": "2025-05-05T20:56:32+00:00" + }, + { + "name": "maxmind/web-service-common", + "version": "v0.10.0", + "source": { + "type": "git", + "url": "https://github.com/maxmind/web-service-common-php.git", + "reference": "d7c7c42fc31bff26e0ded73a6e187bcfb193f9c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maxmind/web-service-common-php/zipball/d7c7c42fc31bff26e0ded73a6e187bcfb193f9c4", + "reference": "d7c7c42fc31bff26e0ded73a6e187bcfb193f9c4", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.0.3", + "ext-curl": "*", + "ext-json": "*", + "php": ">=8.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.*", + "phpstan/phpstan": "*", + "phpunit/phpunit": "^8.0 || ^9.0", + "squizlabs/php_codesniffer": "3.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "MaxMind\\Exception\\": "src/Exception", + "MaxMind\\WebService\\": "src/WebService" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Gregory Oschwald", + "email": "goschwald@maxmind.com" + } + ], + "description": "Internal MaxMind Web Service API", + "homepage": "https://github.com/maxmind/web-service-common-php", + "support": { + "issues": "https://github.com/maxmind/web-service-common-php/issues", + "source": "https://github.com/maxmind/web-service-common-php/tree/v0.10.0" + }, + "time": "2024-11-14T23:14:52+00:00" + }, + { + "name": "moment/moment-min-file", + "version": "2.13.0", + "dist": { + "type": "file", + "url": "https://raw.githubusercontent.com/moment/moment/2.13.0/min/moment.min.js", + "shasum": "a8ca7eea2616fa92e2e85ba6291af6ea012fd190" + }, + "require": { + "composer/installers": "~1.0" + }, + "type": "vanilla-plugin", + "extra": { + "installer-name": "moment" + }, + "license": [ + "MIT" + ], + "homepage": "https://momentjs.com" + }, + { + "name": "moment/moment-timezone-min-file", + "version": "0.5.4", + "dist": { + "type": "file", + "url": "https://raw.githubusercontent.com/moment/moment-timezone/0.5.4/builds/moment-timezone-with-data.min.js", + "shasum": "39b9fccc20863c23f19524a756d75cfef2ff9cbe" + }, + "require": { + "composer/installers": "~1.0" }, "type": "vanilla-plugin", "extra": { @@ -1133,16 +1978,16 @@ }, { "name": "mongodb/mongodb", - "version": "1.19.0", + "version": "1.18.0", "source": { "type": "git", "url": "https://github.com/mongodb/mongo-php-library.git", - "reference": "cbc8104c0b2c32b7cf572ff759324c872e8dc63a" + "reference": "d421c418ef56a96f3dfa6b2828f936df6848ccf9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mongodb/mongo-php-library/zipball/cbc8104c0b2c32b7cf572ff759324c872e8dc63a", - "reference": "cbc8104c0b2c32b7cf572ff759324c872e8dc63a", + "url": "https://api.github.com/repos/mongodb/mongo-php-library/zipball/d421c418ef56a96f3dfa6b2828f936df6848ccf9", + "reference": "d421c418ef56a96f3dfa6b2828f936df6848ccf9", "shasum": "" }, "require": { @@ -1165,7 +2010,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-master": "1.18.x-dev" } }, "autoload": { @@ -1204,57 +2049,74 @@ ], "support": { "issues": "https://github.com/mongodb/mongo-php-library/issues", - "source": "https://github.com/mongodb/mongo-php-library/tree/1.19.0" + "source": "https://github.com/mongodb/mongo-php-library/tree/1.18.0" }, - "time": "2024-05-10T19:49:08+00:00" + "time": "2024-03-27T17:04:50+00:00" }, { "name": "monolog/monolog", - "version": "1.27.1", + "version": "3.9.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "904713c5929655dc9b97288b69cfeedad610c9a1" + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/904713c5929655dc9b97288b69cfeedad610c9a1", - "reference": "904713c5929655dc9b97288b69cfeedad610c9a1", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", "shasum": "" }, "require": { - "php": ">=5.3.0", - "psr/log": "~1.0" + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" }, "provide": { - "psr/log-implementation": "1.0.0" + "psr/log-implementation": "3.0.0" }, "require-dev": { - "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "aws/aws-sdk-php": "^3.0", "doctrine/couchdb": "~1.0@dev", - "graylog2/gelf-php": "~1.0", - "php-amqplib/php-amqplib": "~2.4", - "php-console/php-console": "^3.1.3", - "phpstan/phpstan": "^0.12.59", - "phpunit/phpunit": "~4.5", - "ruflin/elastica": ">=0.90 <3.0", - "sentry/sentry": "^0.13", - "swiftmailer/swiftmailer": "^5.3|^6.0" + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" }, "suggest": { "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", - "ext-mongo": "Allow sending log messages to a MongoDB server", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", - "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", - "php-console/php-console": "Allow sending log messages to Google Chrome", "rollbar/rollbar": "Allow sending log messages to Rollbar", - "ruflin/elastica": "Allow sending log messages to an Elastic Search server", - "sentry/sentry": "Allow sending log messages to a Sentry server" + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, "autoload": { "psr-4": { "Monolog\\": "src/Monolog" @@ -1268,11 +2130,11 @@ { "name": "Jordi Boggiano", "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" + "homepage": "https://seld.be" } ], "description": "Sends your logs to files, sockets, inboxes, databases and various web services", - "homepage": "http://github.com/Seldaek/monolog", + "homepage": "https://github.com/Seldaek/monolog", "keywords": [ "log", "logging", @@ -1280,7 +2142,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/1.27.1" + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" }, "funding": [ { @@ -1292,36 +2154,49 @@ "type": "tidelift" } ], - "time": "2022-06-09T08:53:42+00:00" + "time": "2025-03-24T10:02:05+00:00" }, { - "name": "paragonie/random_compat", - "version": "v2.0.21", + "name": "nyholm/psr7", + "version": "1.8.2", "source": { "type": "git", - "url": "https://github.com/paragonie/random_compat.git", - "reference": "96c132c7f2f7bc3230723b66e89f8f150b29d5ae" + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/96c132c7f2f7bc3230723b66e89f8f150b29d5ae", - "reference": "96c132c7f2f7bc3230723b66e89f8f150b29d5ae", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", "shasum": "" }, "require": { - "php": ">=5.2.0" + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" }, - "require-dev": { - "phpunit/phpunit": "*" + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" }, - "suggest": { - "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, "autoload": { - "files": [ - "lib/random.php" - ] + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1329,89 +2204,323 @@ ], "authors": [ { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com", - "homepage": "https://paragonie.com" + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" } ], - "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", "keywords": [ - "csprng", - "polyfill", - "pseudorandom", - "random" + "psr-17", + "psr-7" ], "support": { - "email": "info@paragonie.com", - "issues": "https://github.com/paragonie/random_compat/issues", - "source": "https://github.com/paragonie/random_compat" + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" }, - "time": "2022-02-16T17:07:03+00:00" + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" }, { - "name": "phpmailer/phpmailer", - "version": "v6.9.1", + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", "source": { "type": "git", - "url": "https://github.com/PHPMailer/PHPMailer.git", - "reference": "039de174cd9c17a8389754d3b877a2ed22743e18" + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/039de174cd9c17a8389754d3b877a2ed22743e18", - "reference": "039de174cd9c17a8389754d3b877a2ed22743e18", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", "shasum": "" }, "require": { - "ext-ctype": "*", - "ext-filter": "*", - "ext-hash": "*", - "php": ">=5.5.0" - }, - "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "doctrine/annotations": "^1.2.6 || ^1.13.3", - "php-parallel-lint/php-console-highlighter": "^1.0.0", - "php-parallel-lint/php-parallel-lint": "^1.3.2", - "phpcompatibility/php-compatibility": "^9.3.5", - "roave/security-advisories": "dev-latest", - "squizlabs/php_codesniffer": "^3.7.2", - "yoast/phpunit-polyfills": "^1.0.4" - }, - "suggest": { - "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication", - "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", - "ext-openssl": "Needed for secure SMTP sending and DKIM signing", - "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication", - "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", - "league/oauth2-google": "Needed for Google XOAUTH2 authentication", - "psr/log": "For optional PSR-3 debug logging", - "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)", - "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication" + "php": "^7.2 || ^8.0" }, "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, "autoload": { "psr-4": { - "PHPMailer\\PHPMailer\\": "src/" + "phpDocumentor\\Reflection\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-2.1-only" + "MIT" ], "authors": [ { - "name": "Marcus Bointon", - "email": "phpmailer@synchromedia.co.uk" - }, - { - "name": "Jim Jagielski", - "email": "jimjag@gmail.com" - }, - { - "name": "Andy Prevost", - "email": "codeworxtech@users.sourceforge.net" - }, + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.6.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/92dde6a5919e34835c506ac8c523ef095a95ed62", + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.2" + }, + "time": "2025-04-13T19:20:35+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + }, + "time": "2024-11-09T15:12:26+00:00" + }, + { + "name": "phplang/scope-exit", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/phplang/scope-exit.git", + "reference": "239b73abe89f9414aa85a7ca075ec9445629192b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phplang/scope-exit/zipball/239b73abe89f9414aa85a7ca075ec9445629192b", + "reference": "239b73abe89f9414aa85a7ca075ec9445629192b", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpLang\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD" + ], + "authors": [ + { + "name": "Sara Golemon", + "email": "pollita@php.net", + "homepage": "https://twitter.com/SaraMG", + "role": "Developer" + } + ], + "description": "Emulation of SCOPE_EXIT construct from C++", + "homepage": "https://github.com/phplang/scope-exit", + "keywords": [ + "cleanup", + "exit", + "scope" + ], + "support": { + "issues": "https://github.com/phplang/scope-exit/issues", + "source": "https://github.com/phplang/scope-exit/tree/master" + }, + "time": "2016-09-17T00:15:18+00:00" + }, + { + "name": "phpmailer/phpmailer", + "version": "v6.10.0", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144", + "reference": "bf74d75a1fde6beaa34a0ddae2ec5fce0f72a144", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "doctrine/annotations": "^1.2.6 || ^1.13.3", + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcompatibility/php-compatibility": "^9.3.5", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.7.2", + "yoast/phpunit-polyfills": "^1.0.4" + }, + "suggest": { + "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication", + "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", + "ext-openssl": "Needed for secure SMTP sending and DKIM signing", + "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)", + "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "authors": [ + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, { "name": "Brent R. Matzelle" } @@ -1419,7 +2528,7 @@ "description": "PHPMailer is a full-featured email creation and transfer class for PHP", "support": { "issues": "https://github.com/PHPMailer/PHPMailer/issues", - "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.1" + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.10.0" }, "funding": [ { @@ -1427,20 +2536,20 @@ "type": "github" } ], - "time": "2023-11-25T22:23:28+00:00" + "time": "2025-04-24T15:19:31+00:00" }, { "name": "phpoffice/math", - "version": "0.1.0", + "version": "0.3.0", "source": { "type": "git", "url": "https://github.com/PHPOffice/Math.git", - "reference": "f0f8cad98624459c540cdd61d2a174d834471773" + "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/Math/zipball/f0f8cad98624459c540cdd61d2a174d834471773", - "reference": "f0f8cad98624459c540cdd61d2a174d834471773", + "url": "https://api.github.com/repos/PHPOffice/Math/zipball/fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", + "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", "shasum": "" }, "require": { @@ -1455,8 +2564,7 @@ "type": "library", "autoload": { "psr-4": { - "PhpOffice\\Math\\": "src/Math/", - "Tests\\PhpOffice\\Math\\": "tests/Math/" + "PhpOffice\\Math\\": "src/Math/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1478,50 +2586,49 @@ ], "support": { "issues": "https://github.com/PHPOffice/Math/issues", - "source": "https://github.com/PHPOffice/Math/tree/0.1.0" + "source": "https://github.com/PHPOffice/Math/tree/0.3.0" }, - "time": "2023-09-25T12:08:20+00:00" + "time": "2025-05-29T08:31:49+00:00" }, { "name": "phpoffice/phpword", - "version": "1.2.0", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/PHPOffice/PHPWord.git", - "reference": "e76b701ef538cb749641514fcbc31a68078550fa" + "reference": "6d75328229bc93790b37e93741adf70646cea958" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/e76b701ef538cb749641514fcbc31a68078550fa", - "reference": "e76b701ef538cb749641514fcbc31a68078550fa", + "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/6d75328229bc93790b37e93741adf70646cea958", + "reference": "6d75328229bc93790b37e93741adf70646cea958", "shasum": "" }, "require": { "ext-dom": "*", + "ext-gd": "*", "ext-json": "*", "ext-xml": "*", + "ext-zip": "*", "php": "^7.1|^8.0", - "phpoffice/math": "^0.1" + "phpoffice/math": "^0.3" }, "require-dev": { - "dompdf/dompdf": "^2.0", - "ext-gd": "*", + "dompdf/dompdf": "^2.0 || ^3.0", "ext-libxml": "*", - "ext-zip": "*", "friendsofphp/php-cs-fixer": "^3.3", - "mpdf/mpdf": "^8.1", + "mpdf/mpdf": "^7.0 || ^8.0", "phpmd/phpmd": "^2.13", - "phpstan/phpstan-phpunit": "@stable", + "phpstan/phpstan": "^0.12.88 || ^1.0.0", + "phpstan/phpstan-phpunit": "^1.0 || ^2.0", "phpunit/phpunit": ">=7.0", "symfony/process": "^4.4 || ^5.0", "tecnickcom/tcpdf": "^6.5" }, "suggest": { "dompdf/dompdf": "Allows writing PDF", - "ext-gd2": "Allows adding images", "ext-xmlwriter": "Allows writing OOXML and ODF", - "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template", - "ext-zip": "Allows writing OOXML and ODF" + "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template" }, "type": "library", "autoload": { @@ -1531,7 +2638,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0" + "LGPL-3.0-only" ], "authors": [ { @@ -1587,62 +2694,56 @@ ], "support": { "issues": "https://github.com/PHPOffice/PHPWord/issues", - "source": "https://github.com/PHPOffice/PHPWord/tree/1.2.0" + "source": "https://github.com/PHPOffice/PHPWord/tree/1.4.0" }, - "time": "2023-11-30T11:22:23+00:00" + "time": "2025-06-05T10:32:36+00:00" }, { - "name": "pimple/pimple", - "version": "v3.5.0", + "name": "phpstan/phpdoc-parser", + "version": "2.2.0", "source": { "type": "git", - "url": "https://github.com/silexphp/Pimple.git", - "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed" + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/silexphp/Pimple/zipball/a94b3a4db7fb774b3d78dad2315ddc07629e1bed", - "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/b9e61a61e39e02dd90944e9115241c7f7e76bfd8", + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/container": "^1.1 || ^2.0" + "php": "^7.4 || ^8.0" }, "require-dev": { - "symfony/phpunit-bridge": "^5.4@dev" + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4.x-dev" - } - }, "autoload": { - "psr-0": { - "Pimple": "src/" + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - } - ], - "description": "Pimple, a simple Dependency Injection Container", - "homepage": "https://pimple.symfony.com", - "keywords": [ - "container", - "dependency injection" - ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { - "source": "https://github.com/silexphp/Pimple/tree/v3.5.0" + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.2.0" }, - "time": "2021-10-28T11:13:42+00:00" + "time": "2025-07-13T07:04:09+00:00" }, { "name": "plotly/plotly", @@ -1665,31 +2766,31 @@ "homepage": "https://github.com/plotly/plotly.js" }, { - "name": "psr/container", - "version": "2.0.2", + "name": "psr/cache", + "version": "3.0.0", "source": { "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", - "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", "shasum": "" }, "require": { - "php": ">=7.4.0" + "php": ">=8.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Container\\": "src/" + "Psr\\Cache\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1702,47 +2803,38 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", + "description": "Common interface for caching libraries", "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" + "cache", + "psr", + "psr-6" ], "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" + "source": "https://github.com/php-fig/cache/tree/3.0.0" }, - "time": "2021-11-05T16:47:00+00:00" + "time": "2021-02-03T23:26:27+00:00" }, { - "name": "psr/log", - "version": "1.1.4", + "name": "psr/clock", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": "^7.0 || ^8.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, "autoload": { "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "Psr\\Clock\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1755,143 +2847,100 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", "keywords": [ - "log", + "clock", + "now", "psr", - "psr-3" + "psr-20", + "time" ], "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" }, - "time": "2021-05-03T11:20:27+00:00" + "time": "2022-11-25T14:36:26+00:00" }, { - "name": "robrichards/xmlseclibs", - "version": "3.1.1", + "name": "psr/container", + "version": "2.0.2", "source": { "type": "git", - "url": "https://github.com/robrichards/xmlseclibs.git", - "reference": "f8f19e58f26cdb42c54b214ff8a820760292f8df" + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/f8f19e58f26cdb42c54b214ff8a820760292f8df", - "reference": "f8f19e58f26cdb42c54b214ff8a820760292f8df", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { - "ext-openssl": "*", - "php": ">= 5.4" + "php": ">=7.4.0" }, "type": "library", - "autoload": { - "psr-4": { - "RobRichards\\XMLSecLibs\\": "src" - } + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], - "description": "A PHP library for XML Security", - "homepage": "https://github.com/robrichards/xmlseclibs", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", "keywords": [ - "security", - "signature", - "xml", - "xmldsig" + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" ], "support": { - "issues": "https://github.com/robrichards/xmlseclibs/issues", - "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.1" - }, - "time": "2020-09-05T13:00:25+00:00" - }, - { - "name": "sencha/extjs-gpl", - "version": "3.4.1.1", - "dist": { - "type": "zip", - "url": "https://cdn.sencha.com/ext/gpl/ext-3.4.1.1-gpl.zip", - "shasum": "26734b47eae909ff7f8cd7de4cadfb3531bd3cdc" - }, - "require": { - "composer/installers": "~1.0" - }, - "type": "vanilla-plugin", - "extra": { - "installer-name": "extjs" + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" }, - "license": [ - "GPL-3.0" - ], - "homepage": "https://www.sencha.com/products/extjs" + "time": "2021-11-05T16:47:00+00:00" }, { - "name": "silex/silex", - "version": "v2.3.0", + "name": "psr/event-dispatcher", + "version": "1.0.0", "source": { "type": "git", - "url": "https://github.com/silexphp/Silex.git", - "reference": "6bc31c1b8c4ef614a7115320fd2d3b958032f131" + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/silexphp/Silex/zipball/6bc31c1b8c4ef614a7115320fd2d3b958032f131", - "reference": "6bc31c1b8c4ef614a7115320fd2d3b958032f131", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", "shasum": "" }, "require": { - "php": ">=7.1.3", - "pimple/pimple": "^3.0", - "symfony/event-dispatcher": "^4.0", - "symfony/http-foundation": "^4.0", - "symfony/http-kernel": "^4.0", - "symfony/routing": "^4.0" - }, - "replace": { - "silex/api": "self.version", - "silex/providers": "self.version" - }, - "require-dev": { - "doctrine/dbal": "^2.2", - "monolog/monolog": "^1.4.1", - "swiftmailer/swiftmailer": "^5", - "symfony/asset": "^4.0", - "symfony/browser-kit": "^4.0", - "symfony/config": "^4.0", - "symfony/css-selector": "^4.0", - "symfony/debug": "^4.0", - "symfony/doctrine-bridge": "^4.0", - "symfony/dom-crawler": "^4.0", - "symfony/expression-language": "^4.0", - "symfony/finder": "^4.0", - "symfony/form": "^4.0", - "symfony/intl": "^4.0", - "symfony/monolog-bridge": "^4.0", - "symfony/options-resolver": "^4.0", - "symfony/phpunit-bridge": "^3.2", - "symfony/process": "^4.0", - "symfony/security": "^4.0", - "symfony/serializer": "^4.0", - "symfony/translation": "^4.0", - "symfony/twig-bridge": "^4.0", - "symfony/validator": "^4.0", - "symfony/var-dumper": "^4.0", - "symfony/web-link": "^4.0", - "twig/twig": "^2.0" + "php": ">=7.2.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { "psr-4": { - "Silex\\": "src/Silex" + "Psr\\EventDispatcher\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -1900,295 +2949,257 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Igor Wiedler", - "email": "igor@wiedler.ch" + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" } ], - "description": "The PHP micro-framework based on the Symfony Components", - "homepage": "http://silex.sensiolabs.org", + "description": "Standard interfaces for event handling.", "keywords": [ - "microframework" + "events", + "psr", + "psr-14" ], "support": { - "issues": "https://github.com/silexphp/Silex/issues", - "source": "https://github.com/silexphp/Silex/tree/v2.3.0" + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" }, - "abandoned": "symfony/flex", - "time": "2018-04-20T05:17:01+00:00" + "time": "2019-01-08T18:20:26+00:00" }, { - "name": "simplesamlphp/assert", - "version": "v0.8.0", + "name": "psr/http-client", + "version": "1.0.3", "source": { "type": "git", - "url": "https://github.com/simplesamlphp/assert.git", - "reference": "d3b0f38f4ae083822471c15e3c4a0401ddaeac73" + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/assert/zipball/d3b0f38f4ae083822471c15e3c4a0401ddaeac73", - "reference": "d3b0f38f4ae083822471c15e3c4a0401ddaeac73", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", "shasum": "" }, "require": { - "ext-spl": "*", - "php": "^7.4 || ^8.0", - "webmozart/assert": "^1.11" - }, - "require-dev": { - "simplesamlphp/simplesamlphp-test-framework": "^1.2.1" + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "v0.8.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { "psr-4": { - "SimpleSAML\\Assert\\": "src/" + "Psr\\Http\\Client\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-2.1-or-later" + "MIT" ], "authors": [ { - "name": "Tim van Dijen", - "email": "tvdijen@gmail.com" - }, - { - "name": "Jaime Perez Crespo", - "email": "jaimepc@gmail.com" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "A wrapper around webmozart/assert to make it useful beyond checking method arguments", + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], "support": { - "issues": "https://github.com/simplesamlphp/assert/issues", - "source": "https://github.com/simplesamlphp/assert/tree/v0.8.0" + "source": "https://github.com/php-fig/http-client" }, - "time": "2022-09-20T20:18:55+00:00" + "time": "2023-09-23T14:17:50+00:00" }, { - "name": "simplesamlphp/composer-module-installer", - "version": "v1.3.4", + "name": "psr/http-factory", + "version": "1.1.0", "source": { "type": "git", - "url": "https://github.com/simplesamlphp/composer-module-installer.git", - "reference": "36508ed9580a30c4d5ab0bb3c25c00d0b5d42946" + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/composer-module-installer/zipball/36508ed9580a30c4d5ab0bb3c25c00d0b5d42946", - "reference": "36508ed9580a30c4d5ab0bb3c25c00d0b5d42946", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", "shasum": "" }, "require": { - "composer-plugin-api": "^1.1 || ^2.0", - "php": "^7.4 || ^8.0", - "simplesamlphp/assert": "^0.8.0 || ^1.0" - }, - "require-dev": { - "composer/composer": "^2.4", - "simplesamlphp/simplesamlphp-test-framework": "^1.2.1" + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" }, - "type": "composer-plugin", + "type": "library", "extra": { - "class": "SimpleSAML\\Composer\\ModuleInstallerPlugin" + "branch-alias": { + "dev-master": "1.0.x-dev" + } }, "autoload": { "psr-4": { - "SimpleSAML\\Composer\\": "src/" + "Psr\\Http\\Message\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-2.1-only" + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" ], - "description": "A Composer plugin that allows installing SimpleSAMLphp modules through Composer.", "support": { - "issues": "https://github.com/simplesamlphp/composer-module-installer/issues", - "source": "https://github.com/simplesamlphp/composer-module-installer/tree/v1.3.4" + "source": "https://github.com/php-fig/http-factory" }, - "time": "2023-03-08T20:58:22+00:00" + "time": "2024-04-15T12:06:14+00:00" }, { - "name": "simplesamlphp/saml2", - "version": "v3.2.6", + "name": "psr/http-message", + "version": "2.0", "source": { "type": "git", - "url": "https://github.com/simplesamlphp/saml2.git", - "reference": "a56e46ef8e0c5245a4ca7facc3d308b493215751" + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/saml2/zipball/a56e46ef8e0c5245a4ca7facc3d308b493215751", - "reference": "a56e46ef8e0c5245a4ca7facc3d308b493215751", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-openssl": "*", - "ext-zlib": "*", - "php": ">=5.4", - "psr/log": "~1.0", - "robrichards/xmlseclibs": "^3.0" - }, - "require-dev": { - "mockery/mockery": "~0.9", - "phpmd/phpmd": "~1.5", - "phpunit/phpunit": "~4", - "sebastian/phpcpd": "~1.4", - "sensiolabs/security-checker": "~1.1", - "squizlabs/php_codesniffer": "~1.4" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "v3.1.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { - "files": [ - "src/_autoload.php" - ], - "psr-0": { - "SAML2\\": "src/" + "psr-4": { + "Psr\\Http\\Message\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-2.1-or-later" + "MIT" ], "authors": [ { - "name": "Andreas Åkre Solberg", - "email": "andreas.solberg@uninett.no" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "SAML2 PHP library from SimpleSAMLphp", + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], "support": { - "issues": "https://github.com/simplesamlphp/saml2/issues", - "source": "https://github.com/simplesamlphp/saml2/tree/master" + "source": "https://github.com/php-fig/http-message/tree/2.0" }, - "time": "2018-11-20T11:11:28+00:00" + "time": "2023-04-04T09:54:51+00:00" }, { - "name": "simplesamlphp/simplesamlphp", - "version": "1.16.3", + "name": "psr/log", + "version": "3.0.2", "source": { "type": "git", - "url": "https://github.com/simplesamlphp/simplesamlphp.git", - "reference": "abc208dbc9c94eb8bab8266825ca035cc96072ba" + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/simplesamlphp/zipball/abc208dbc9c94eb8bab8266825ca035cc96072ba", - "reference": "abc208dbc9c94eb8bab8266825ca035cc96072ba", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", "shasum": "" }, "require": { - "ext-date": "*", - "ext-dom": "*", - "ext-hash": "*", - "ext-json": "*", - "ext-mbstring": "*", - "ext-openssl": "*", - "ext-pcre": "*", - "ext-spl": "*", - "ext-zlib": "*", - "gettext/gettext": "^3.5", - "jaimeperez/twig-configurable-i18n": "^1.2", - "php": ">=5.4", - "robrichards/xmlseclibs": "^3.0", - "simplesamlphp/saml2": "~3.2.2", - "twig/twig": "~1.0", - "whitehat101/apr1-md5": "~1.0" + "php": ">=8.0.0" }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.2", - "mikey179/vfsstream": "~1.6", - "phpunit/phpunit": "~4.8.35" + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } }, - "type": "project", "autoload": { - "files": [ - "lib/_autoload_modules.php" - ], "psr-4": { - "SimpleSAML\\": "lib/SimpleSAML" + "Psr\\Log\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-2.1-or-later" + "MIT" ], "authors": [ { - "name": "Andreas Åkre Solberg", - "email": "andreas.solberg@uninett.no" - }, - { - "name": "Olav Morken", - "email": "olav.morken@uninett.no" - }, - { - "name": "Jaime Perez", - "email": "jaime.perez@uninett.no" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "A PHP implementation of a SAML 2.0 service provider and identity provider, also compatible with Shibboleth 1.3 and 2.0.", - "homepage": "http://simplesamlphp.org", + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", "keywords": [ - "SAML2", - "idp", - "oauth", - "shibboleth", - "sp", - "ws-federation" + "log", + "psr", + "psr-3" ], "support": { - "issues": "https://github.com/simplesamlphp/simplesamlphp/issues", - "source": "https://github.com/simplesamlphp/simplesamlphp" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2018-12-20T16:49:03+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { - "name": "symfony/debug", - "version": "v4.4.44", + "name": "ralouphie/getallheaders", + "version": "3.0.3", "source": { "type": "git", - "url": "https://github.com/symfony/debug.git", - "reference": "1a692492190773c5310bc7877cb590c04c2f05be" + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/1a692492190773c5310bc7877cb590c04c2f05be", - "reference": "1a692492190773c5310bc7877cb590c04c2f05be", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", "shasum": "" }, "require": { - "php": ">=7.1.3", - "psr/log": "^1|^2|^3" - }, - "conflict": { - "symfony/http-kernel": "<3.4" + "php": ">=5.6" }, "require-dev": { - "symfony/http-kernel": "^3.4|^4.0|^5.0" + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" }, "type": "library", "autoload": { - "psr-4": { - "Symfony\\Component\\Debug\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "files": [ + "src/getallheaders.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2197,86 +3208,3006 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" } ], - "description": "Provides tools to ease debugging PHP code", - "homepage": "https://symfony.com", + "description": "A polyfill for getallheaders.", "support": { - "source": "https://github.com/symfony/debug/tree/v4.4.44" + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "abandoned": "symfony/error-handler", - "time": "2022-07-28T16:29:46+00:00" + "time": "2019-03-08T08:55:37+00:00" }, { - "name": "symfony/deprecation-contracts", - "version": "v2.5.3", + "name": "robrichards/xmlseclibs", + "version": "3.1.3", "source": { "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "80d075412b557d41002320b96a096ca65aa2c98d" + "url": "https://github.com/robrichards/xmlseclibs.git", + "reference": "2bdfd742624d739dfadbd415f00181b4a77aaf07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/80d075412b557d41002320b96a096ca65aa2c98d", - "reference": "80d075412b557d41002320b96a096ca65aa2c98d", + "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/2bdfd742624d739dfadbd415f00181b4a77aaf07", + "reference": "2bdfd742624d739dfadbd415f00181b4a77aaf07", "shasum": "" }, "require": { - "php": ">=7.1" + "ext-openssl": "*", + "php": ">= 5.4" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "2.5-dev" - } - }, "autoload": { - "files": [ - "function.php" - ] + "psr-4": { + "RobRichards\\XMLSecLibs\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } + "BSD-3-Clause" ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.3" + "description": "A PHP library for XML Security", + "homepage": "https://github.com/robrichards/xmlseclibs", + "keywords": [ + "security", + "signature", + "xml", + "xmldsig" + ], + "support": { + "issues": "https://github.com/robrichards/xmlseclibs/issues", + "source": "https://github.com/robrichards/xmlseclibs/tree/3.1.3" + }, + "time": "2024-11-20T21:13:56+00:00" + }, + { + "name": "sencha/extjs-gpl", + "version": "3.4.1.1", + "dist": { + "type": "zip", + "url": "https://cdn.sencha.com/ext/gpl/ext-3.4.1.1-gpl.zip", + "shasum": "26734b47eae909ff7f8cd7de4cadfb3531bd3cdc" + }, + "require": { + "composer/installers": "~1.0" + }, + "type": "vanilla-plugin", + "extra": { + "installer-name": "extjs" + }, + "license": [ + "GPL-3.0" + ], + "homepage": "https://www.sencha.com/products/extjs" + }, + { + "name": "simplesamlphp/assert", + "version": "v1.8.2", + "source": { + "type": "git", + "url": "https://github.com/simplesamlphp/assert.git", + "reference": "b551f50399540172f387d97b2e7246e6c352154d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/simplesamlphp/assert/zipball/b551f50399540172f387d97b2e7246e6c352154d", + "reference": "b551f50399540172f387d97b2e7246e6c352154d", + "shasum": "" + }, + "require": { + "ext-date": "*", + "ext-filter": "*", + "ext-pcre": "*", + "ext-spl": "*", + "guzzlehttp/psr7": "~2.7.1", + "php": "^8.1", + "webmozart/assert": "~1.11.0" + }, + "require-dev": { + "ext-intl": "*", + "simplesamlphp/simplesamlphp-test-framework": "~1.9.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "v1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "SimpleSAML\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Tim van Dijen", + "email": "tvdijen@gmail.com" + }, + { + "name": "Jaime Perez Crespo", + "email": "jaimepc@gmail.com" + } + ], + "description": "A wrapper around webmozart/assert to make it useful beyond checking method arguments", + "support": { + "issues": "https://github.com/simplesamlphp/assert/issues", + "source": "https://github.com/simplesamlphp/assert/tree/v1.8.2" + }, + "time": "2025-06-28T12:57:30+00:00" + }, + { + "name": "simplesamlphp/composer-module-installer", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/simplesamlphp/composer-module-installer.git", + "reference": "edb2155d200e2a208816d06f42cfa78bfd9e7cf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/simplesamlphp/composer-module-installer/zipball/edb2155d200e2a208816d06f42cfa78bfd9e7cf4", + "reference": "edb2155d200e2a208816d06f42cfa78bfd9e7cf4", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.6", + "php": "^8.1", + "simplesamlphp/assert": "^1.6" + }, + "require-dev": { + "composer/composer": "^2.8.3", + "simplesamlphp/simplesamlphp-test-framework": "^1.8.0" + }, + "type": "composer-plugin", + "extra": { + "class": "SimpleSAML\\Composer\\ModuleInstallerPlugin" + }, + "autoload": { + "psr-4": { + "SimpleSAML\\Composer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "description": "A Composer plugin that allows installing SimpleSAMLphp modules through Composer.", + "support": { + "issues": "https://github.com/simplesamlphp/composer-module-installer/issues", + "source": "https://github.com/simplesamlphp/composer-module-installer/tree/v1.4.0" + }, + "time": "2024-12-08T16:57:03+00:00" + }, + { + "name": "simplesamlphp/composer-xmlprovider-installer", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/simplesamlphp/composer-xmlprovider-installer.git", + "reference": "3d882187b5b0b404c381a2e4d17498ca4b2785b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/simplesamlphp/composer-xmlprovider-installer/zipball/3d882187b5b0b404c381a2e4d17498ca4b2785b3", + "reference": "3d882187b5b0b404c381a2e4d17498ca4b2785b3", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "php": "^8.1" + }, + "require-dev": { + "composer/composer": "^2.4", + "simplesamlphp/simplesamlphp-test-framework": "^1.5.4" + }, + "type": "composer-plugin", + "extra": { + "class": "SimpleSAML\\Composer\\XMLProvider\\XMLProviderInstallerPlugin" + }, + "autoload": { + "psr-4": { + "SimpleSAML\\Composer\\XMLProvider\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "description": "A composer plugin that will auto-generate a classmap with all classes that implement SerializableElementInterface.", + "support": { + "issues": "https://github.com/simplesamlphp/composer-xmlprovider-installer/issues", + "source": "https://github.com/simplesamlphp/composer-xmlprovider-installer/tree/v1.0.2" + }, + "time": "2025-06-28T18:54:25+00:00" + }, + { + "name": "simplesamlphp/saml2", + "version": "v5.0.2", + "source": { + "type": "git", + "url": "https://github.com/simplesamlphp/saml2.git", + "reference": "d23dce11ac5a9b84a37a283ea7fbb0d780771e6c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/simplesamlphp/saml2/zipball/d23dce11ac5a9b84a37a283ea7fbb0d780771e6c", + "reference": "d23dce11ac5a9b84a37a283ea7fbb0d780771e6c", + "shasum": "" + }, + "require": { + "ext-date": "*", + "ext-dom": "*", + "ext-filter": "*", + "ext-libxml": "*", + "ext-openssl": "*", + "ext-pcre": "*", + "ext-zlib": "*", + "nyholm/psr7": "~1.8.2", + "php": "^8.1", + "psr/clock": "~1.0.0", + "psr/http-message": "~2.0", + "psr/log": "~2.3.1 || ~3.0.0", + "simplesamlphp/assert": "~1.8.1", + "simplesamlphp/xml-common": "~1.25.0", + "simplesamlphp/xml-security": "~1.13.4", + "simplesamlphp/xml-soap": "~1.7.0" + }, + "require-dev": { + "beste/clock": "~3.0.0", + "ext-intl": "*", + "mockery/mockery": "~1.6.12", + "simplesamlphp/composer-xmlprovider-installer": "~1.0.2", + "simplesamlphp/simplesamlphp-test-framework": "~1.9.2" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "v5.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "SimpleSAML\\SAML2\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Andreas Åkre Solberg", + "email": "andreas.solberg@uninett.no" + } + ], + "description": "SAML2 PHP library from SimpleSAMLphp", + "support": { + "issues": "https://github.com/simplesamlphp/saml2/issues", + "source": "https://github.com/simplesamlphp/saml2/tree/v5.0.2" + }, + "time": "2025-07-01T19:07:40+00:00" + }, + { + "name": "simplesamlphp/saml2-legacy", + "version": "v4.18.1", + "source": { + "type": "git", + "url": "https://github.com/simplesamlphp/saml2-legacy.git", + "reference": "9bbf43a5ace9c8e5107dad3a613b014b456ecd56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/simplesamlphp/saml2-legacy/zipball/9bbf43a5ace9c8e5107dad3a613b014b456ecd56", + "reference": "9bbf43a5ace9c8e5107dad3a613b014b456ecd56", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-openssl": "*", + "ext-zlib": "*", + "php": ">=7.1 || ^8.0", + "psr/log": "~1.1 || ^2.0 || ^3.0", + "robrichards/xmlseclibs": "^3.1.1", + "webmozart/assert": "^1.9" + }, + "conflict": { + "robrichards/xmlseclibs": "3.1.2" + }, + "require-dev": { + "mockery/mockery": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "sebastian/phpcpd": "~4.1 || ^5.0 || ^6.0", + "simplesamlphp/simplesamlphp-test-framework": "~0.1.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "v4.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "SAML2\\": "src/SAML2" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Andreas Åkre Solberg", + "email": "andreas.solberg@uninett.no" + } + ], + "description": "SAML2 PHP library from SimpleSAMLphp", + "support": { + "source": "https://github.com/simplesamlphp/saml2-legacy/tree/v4.18.1" + }, + "time": "2025-03-16T11:50:02+00:00" + }, + { + "name": "simplesamlphp/simplesamlphp", + "version": "v2.4.2", + "source": { + "type": "git", + "url": "https://github.com/simplesamlphp/simplesamlphp.git", + "reference": "d791ed73656102f4d553f7e0335cc6a528b1c2dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/simplesamlphp/simplesamlphp/zipball/d791ed73656102f4d553f7e0335cc6a528b1c2dd", + "reference": "d791ed73656102f4d553f7e0335cc6a528b1c2dd", + "shasum": "" + }, + "require": { + "ext-date": "*", + "ext-dom": "*", + "ext-hash": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-pcre": "*", + "ext-session": "*", + "ext-simplexml": "*", + "ext-spl": "*", + "ext-zlib": "*", + "gettext/gettext": "^5.7", + "gettext/translator": "^1.1", + "php": "^8.1", + "phpmailer/phpmailer": "^6.8", + "psr/log": "^3.0", + "simplesamlphp/assert": "^1.1", + "simplesamlphp/composer-module-installer": "^1.3", + "simplesamlphp/saml2": "^5.0.0", + "simplesamlphp/saml2-legacy": "^4.18.1", + "simplesamlphp/simplesamlphp-assets-base": "~2.3.0", + "simplesamlphp/xml-security": "^1.7", + "symfony/cache": "^6.4", + "symfony/config": "^6.4", + "symfony/console": "^6.4", + "symfony/dependency-injection": "^6.4", + "symfony/filesystem": "^6.4", + "symfony/finder": "^6.4", + "symfony/framework-bundle": "^6.4", + "symfony/http-foundation": "^6.4", + "symfony/http-kernel": "^6.4", + "symfony/intl": "^6.4", + "symfony/password-hasher": "^6.4", + "symfony/polyfill-intl-icu": "^1.28", + "symfony/routing": "^6.4", + "symfony/translation-contracts": "^3.0", + "symfony/twig-bridge": "^6.4", + "symfony/var-exporter": "^6.4", + "symfony/yaml": "^6.4", + "twig/intl-extra": "^3.7", + "twig/twig": "^3.14.0" + }, + "require-dev": { + "ext-curl": "*", + "ext-pdo_sqlite": "*", + "gettext/php-scanner": "1.3.1", + "mikey179/vfsstream": "~1.6", + "predis/predis": "^2.2", + "simplesamlphp/simplesamlphp-test-framework": "^1.9.2", + "symfony/translation": "^6.4" + }, + "suggest": { + "ext-curl": "Needed in order to check for updates automatically", + "ext-intl": "Needed if translations for non-English languages are required.", + "ext-ldap": "Needed if an LDAP backend is used", + "ext-memcache": "Needed if a Memcache server is used to store session information", + "ext-mysql": "Needed if a MySQL backend is used, either for authentication or to store session information", + "ext-pdo": "Needed if a database backend is used, either for authentication or to store session information", + "ext-pgsql": "Needed if a PostgreSQL backend is used, either for authentication or to store session information", + "predis/predis": "Needed if a Redis server is used to store session information" + }, + "type": "project", + "extra": { + "branch-alias": { + "dev-master": "2.5.0.x-dev" + } + }, + "autoload": { + "files": [ + "src/_autoload_modules.php" + ], + "psr-4": { + "SimpleSAML\\": "src/SimpleSAML", + "SimpleSAML\\Module\\core\\": "modules/core/src", + "SimpleSAML\\Module\\cron\\": "modules/cron/src", + "SimpleSAML\\Module\\saml\\": "modules/saml/src", + "SimpleSAML\\Module\\admin\\": "modules/admin/src", + "SimpleSAML\\Module\\multiauth\\": "modules/multiauth/src", + "SimpleSAML\\Module\\exampleauth\\": "modules/exampleauth/src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Andreas Åkre Solberg", + "email": "andreas.solberg@uninett.no" + }, + { + "name": "Olav Morken", + "email": "olav.morken@uninett.no" + }, + { + "name": "Jaime Perez", + "email": "jaime.perez@uninett.no" + } + ], + "description": "A PHP implementation of a SAML 2.0 service provider and identity provider.", + "homepage": "https://simplesamlphp.org", + "keywords": [ + "SAML2", + "idp", + "oauth", + "shibboleth", + "sp", + "ws-federation" + ], + "support": { + "issues": "https://github.com/simplesamlphp/simplesamlphp/issues", + "source": "https://github.com/simplesamlphp/simplesamlphp" + }, + "time": "2025-06-04T13:10:38+00:00" + }, + { + "name": "simplesamlphp/simplesamlphp-assets-base", + "version": "v2.3.10", + "source": { + "type": "git", + "url": "https://github.com/simplesamlphp/simplesamlphp-assets-base.git", + "reference": "39ac268fb1c49333a188df6094b69e28e35150f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/simplesamlphp/simplesamlphp-assets-base/zipball/39ac268fb1c49333a188df6094b69e28e35150f6", + "reference": "39ac268fb1c49333a188df6094b69e28e35150f6", + "shasum": "" + }, + "require": { + "php": "^8.1", + "simplesamlphp/composer-module-installer": "^1.3.4" + }, + "type": "simplesamlphp-module", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Tim van Dijen", + "email": "tvdijen@gmail.com" + } + ], + "description": "Assets for the SimpleSAMLphp main repository", + "support": { + "issues": "https://github.com/simplesamlphp/simplesamlphp-assets-base/issues", + "source": "https://github.com/simplesamlphp/simplesamlphp-assets-base/tree/v2.3.10" + }, + "time": "2025-07-20T01:44:13+00:00" + }, + { + "name": "simplesamlphp/simplesamlphp-module-authorize", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/simplesamlphp/simplesamlphp-module-authorize.git", + "reference": "f436ecc5616de82748f30f7518afcc3da0294798" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/simplesamlphp/simplesamlphp-module-authorize/zipball/f436ecc5616de82748f30f7518afcc3da0294798", + "reference": "f436ecc5616de82748f30f7518afcc3da0294798", + "shasum": "" + }, + "require": { + "php": "^8.1", + "simplesamlphp/saml2": "~5.0.2", + "simplesamlphp/simplesamlphp": "^2.4", + "symfony/http-foundation": "^6.4" + }, + "require-dev": { + "simplesamlphp/simplesamlphp-test-framework": "^1.9.3" + }, + "type": "simplesamlphp-module", + "autoload": { + "psr-4": { + "SimpleSAML\\Module\\authorize\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Ernesto Revilla", + "email": "erny@yaco.es" + } + ], + "description": "This module provides a user authorization filter based on attribute matching", + "keywords": [ + "authorize", + "simplesamlphp" + ], + "support": { + "issues": "https://github.com/simplesamlphp/simplesamlphp-module-authorize/issues", + "source": "https://github.com/simplesamlphp/simplesamlphp-module-authorize" + }, + "time": "2025-08-26T08:27:30+00:00" + }, + { + "name": "simplesamlphp/xml-common", + "version": "v1.25.1", + "source": { + "type": "git", + "url": "https://github.com/simplesamlphp/xml-common.git", + "reference": "999603aa521d91e17b562bb0b498513af80eb190" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/simplesamlphp/xml-common/zipball/999603aa521d91e17b562bb0b498513af80eb190", + "reference": "999603aa521d91e17b562bb0b498513af80eb190", + "shasum": "" + }, + "require": { + "ext-date": "*", + "ext-dom": "*", + "ext-filter": "*", + "ext-libxml": "*", + "ext-pcre": "*", + "ext-spl": "*", + "php": "^8.1", + "simplesamlphp/assert": "~1.8.1", + "simplesamlphp/composer-xmlprovider-installer": "~1.0.2", + "symfony/finder": "~6.4.0" + }, + "require-dev": { + "simplesamlphp/simplesamlphp-test-framework": "~1.9.2" + }, + "type": "simplesamlphp-xmlprovider", + "autoload": { + "psr-4": { + "SimpleSAML\\XML\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Jaime Perez", + "email": "jaime.perez@uninett.no" + }, + { + "name": "Tim van Dijen", + "email": "tvdijen@gmail.com" + } + ], + "description": "A library with classes and utilities for handling XML structures.", + "homepage": "http://simplesamlphp.org", + "keywords": [ + "saml", + "xml" + ], + "support": { + "issues": "https://github.com/simplesamlphp/xml-common/issues", + "source": "https://github.com/simplesamlphp/xml-common" + }, + "time": "2025-06-29T13:05:44+00:00" + }, + { + "name": "simplesamlphp/xml-security", + "version": "v1.13.7", + "source": { + "type": "git", + "url": "https://github.com/simplesamlphp/xml-security.git", + "reference": "f6f32a3c2c6b398408d5bccc9d59445edc1cb67d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/simplesamlphp/xml-security/zipball/f6f32a3c2c6b398408d5bccc9d59445edc1cb67d", + "reference": "f6f32a3c2c6b398408d5bccc9d59445edc1cb67d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-pcre": "*", + "ext-spl": "*", + "php": "^8.1", + "simplesamlphp/assert": "~1.8.1", + "simplesamlphp/xml-common": "~1.25.0" + }, + "require-dev": { + "simplesamlphp/simplesamlphp-test-framework": "~1.9.2" + }, + "type": "simplesamlphp-xmlprovider", + "autoload": { + "psr-4": { + "SimpleSAML\\XMLSecurity\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Jaime Perez Crespo", + "email": "jaime.perez@uninett.no", + "role": "Maintainer" + }, + { + "name": "Tim van Dijen", + "email": "tvdijen@gmail.com", + "role": "Maintainer" + } + ], + "description": "SimpleSAMLphp library for XML Security", + "homepage": "https://github.com/simplesamlphp/xml-security", + "keywords": [ + "security", + "signature", + "xml", + "xmldsig" + ], + "support": { + "issues": "https://github.com/simplesamlphp/xml-security/issues", + "source": "https://github.com/simplesamlphp/xml-security/tree/v1.13.7" + }, + "time": "2025-06-29T13:07:27+00:00" + }, + { + "name": "simplesamlphp/xml-soap", + "version": "v1.7.1", + "source": { + "type": "git", + "url": "https://github.com/simplesamlphp/xml-soap.git", + "reference": "ca1ee4ea29c62fa66fc30d040b4013b4543f4f76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/simplesamlphp/xml-soap/zipball/ca1ee4ea29c62fa66fc30d040b4013b4543f4f76", + "reference": "ca1ee4ea29c62fa66fc30d040b4013b4543f4f76", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "php": "^8.1", + "simplesamlphp/assert": "~1.8.1", + "simplesamlphp/xml-common": "~1.25.0" + }, + "require-dev": { + "simplesamlphp/simplesamlphp-test-framework": "~1.9.2" + }, + "type": "simplesamlphp-xmlprovider", + "extra": { + "branch-alias": { + "dev-master": "v2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "SimpleSAML\\SOAP\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Tim van Dijen", + "email": "tvdijen@gmail.com" + } + ], + "description": "SimpleSAMLphp library for XML SOAP", + "support": { + "issues": "https://github.com/simplesamlphp/xml-soap/issues", + "source": "https://github.com/simplesamlphp/xml-soap/tree/v1.7.1" + }, + "time": "2025-06-03T21:07:04+00:00" + }, + { + "name": "swaggest/json-diff", + "version": "v3.12.1", + "source": { + "type": "git", + "url": "https://github.com/swaggest/json-diff.git", + "reference": "7ebc4eab95bcc73916433964c266588d09b35052" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swaggest/json-diff/zipball/7ebc4eab95bcc73916433964c266588d09b35052", + "reference": "7ebc4eab95bcc73916433964c266588d09b35052", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.1" + }, + "require-dev": { + "phperf/phpunit": "4.8.37" + }, + "type": "library", + "autoload": { + "psr-4": { + "Swaggest\\JsonDiff\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Viacheslav Poturaev", + "email": "vearutop@gmail.com" + } + ], + "description": "JSON diff/rearrange/patch/pointer library for PHP", + "support": { + "issues": "https://github.com/swaggest/json-diff/issues", + "source": "https://github.com/swaggest/json-diff/tree/v3.12.1" + }, + "time": "2025-03-10T08:22:10+00:00" + }, + { + "name": "swaggest/json-schema", + "version": "v0.12.43", + "source": { + "type": "git", + "url": "https://github.com/swaggest/php-json-schema.git", + "reference": "1f3a77a382c5d273a0f1fe34be3b8af4060a88cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/swaggest/php-json-schema/zipball/1f3a77a382c5d273a0f1fe34be3b8af4060a88cd", + "reference": "1f3a77a382c5d273a0f1fe34be3b8af4060a88cd", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.1", + "phplang/scope-exit": "^1.0", + "swaggest/json-diff": "^3.8.2", + "symfony/polyfill-mbstring": "^1.19" + }, + "require-dev": { + "phperf/phpunit": "4.8.37" + }, + "suggest": { + "ext-mbstring": "For better performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "Swaggest\\JsonSchema\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Viacheslav Poturaev", + "email": "vearutop@gmail.com" + } + ], + "description": "High definition PHP structures with JSON-schema based validation", + "support": { + "email": "vearutop@gmail.com", + "issues": "https://github.com/swaggest/php-json-schema/issues", + "source": "https://github.com/swaggest/php-json-schema/tree/v0.12.43" + }, + "time": "2024-12-22T21:18:27+00:00" + }, + { + "name": "symfony/asset", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/asset.git", + "reference": "cfee7c0d64be113383db74a2fdd65d426b7f3aab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/asset/zipball/cfee7c0d64be113383db74a2fdd65d426b7f3aab", + "reference": "cfee7c0d64be113383db74a2fdd65d426b7f3aab", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "symfony/http-foundation": "<5.4" + }, + "require-dev": { + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Asset\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/asset/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:14:14+00:00" + }, + { + "name": "symfony/cache", + "version": "v6.4.28", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache.git", + "reference": "31628f36fc97c5714d181b3a8d29efb85c6a7677" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache/zipball/31628f36fc97c5714d181b3a8d29efb85c6a7677", + "reference": "31628f36fc97c5714d181b3a8d29efb85c6a7677", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^2.0|^3.0", + "psr/log": "^1.1|^2|^3", + "symfony/cache-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3", + "symfony/var-exporter": "^6.3.6|^7.0" + }, + "conflict": { + "doctrine/dbal": "<2.13.1", + "symfony/dependency-injection": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/var-dumper": "<5.4" + }, + "provide": { + "psr/cache-implementation": "2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0", + "symfony/cache-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Cache\\": "" + }, + "classmap": [ + "Traits/ValueWrapper.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", + "homepage": "https://symfony.com", + "keywords": [ + "caching", + "psr6" + ], + "support": { + "source": "https://github.com/symfony/cache/tree/v6.4.28" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-10-30T08:37:02+00:00" + }, + { + "name": "symfony/cache-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/cache-contracts.git", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/cache-contracts/zipball/5d68a57d66910405e5c0b63d6f0af941e66fc868", + "reference": "5d68a57d66910405e5c0b63d6f0af941e66fc868", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/cache": "^3.0" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Cache\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to caching", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/cache-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-13T15:25:07+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/config", + "version": "v6.4.28", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "15947c18ef3ddb0b2f4ec936b9e90e2520979f62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/15947c18ef3ddb0b2f4ec936b9e90e2520979f62", + "reference": "15947c18ef3ddb0b2f4ec936b9e90e2520979f62", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<5.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v6.4.28" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-01T19:52:02+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "59266a5bf6a596e3e0844fd95e6ad7ea3c1d3350" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/59266a5bf6a596e3e0844fd95e6ad7ea3c1d3350", + "reference": "59266a5bf6a596e3e0844fd95e6ad7ea3c1d3350", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-30T10:38:54+00:00" + }, + { + "name": "symfony/dependency-injection", + "version": "v6.4.26", + "source": { + "type": "git", + "url": "https://github.com/symfony/dependency-injection.git", + "reference": "5f311eaf0b321f8ec640f6bae12da43a14026898" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/5f311eaf0b321f8ec640f6bae12da43a14026898", + "reference": "5f311eaf0b321f8ec640f6bae12da43a14026898", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/service-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4.20|^7.2.5" + }, + "conflict": { + "ext-psr": "<1.1|>=2", + "symfony/config": "<6.1", + "symfony/finder": "<5.4", + "symfony/proxy-manager-bridge": "<6.3", + "symfony/yaml": "<5.4" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "symfony/service-implementation": "1.1|2.0|3.0" + }, + "require-dev": { + "symfony/config": "^6.1|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DependencyInjection\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows you to standardize and centralize the way objects are constructed in your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.26" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T09:57:09+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/dotenv", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/dotenv.git", + "reference": "234b6c602f12b00693f4b0d1054386fb30dfc8ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dotenv/zipball/234b6c602f12b00693f4b0d1054386fb30dfc8ff", + "reference": "234b6c602f12b00693f4b0d1054386fb30dfc8ff", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "symfony/console": "<5.4", + "symfony/process": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Dotenv\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Registers environment variables from a .env file", + "homepage": "https://symfony.com", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "source": "https://github.com/symfony/dotenv/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:14:14+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "0b31a944fcd8759ae294da4d2808cbc53aebd0c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/0b31a944fcd8759ae294da4d2808cbc53aebd0c3", + "reference": "0b31a944fcd8759ae294da4d2808cbc53aebd0c3", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^6.4|^7.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-07T08:17:57+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "497f73ac996a598c92409b44ac43b6690c4f666d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/497f73ac996a598c92409b44ac43b6690c4f666d", + "reference": "497f73ac996a598c92409b44ac43b6690c4f666d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-22T09:11:45+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", + "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^5.4|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:14:14+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "73089124388c8510efb8d2d1689285d285937b08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/73089124388c8510efb8d2d1689285d285937b08", + "reference": "73089124388c8510efb8d2d1689285d285937b08", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T12:02:45+00:00" + }, + { + "name": "symfony/flex", + "version": "v2.8.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/flex.git", + "reference": "423c36e369361003dc31ef11c5f15fb589e52c01" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/flex/zipball/423c36e369361003dc31ef11c5f15fb589e52c01", + "reference": "423c36e369361003dc31ef11c5f15fb589e52c01", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.1", + "php": ">=8.0" + }, + "conflict": { + "composer/semver": "<1.7.2" + }, + "require-dev": { + "composer/composer": "^2.1", + "symfony/dotenv": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/phpunit-bridge": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Symfony\\Flex\\Flex" + }, + "autoload": { + "psr-4": { + "Symfony\\Flex\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien.potencier@gmail.com" + } + ], + "description": "Composer plugin for Symfony", + "support": { + "issues": "https://github.com/symfony/flex/issues", + "source": "https://github.com/symfony/flex/tree/v2.8.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-05T07:45:19+00:00" + }, + { + "name": "symfony/framework-bundle", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/framework-bundle.git", + "reference": "869b94902dd38f2f33718908f2b5d4868e3b9241" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/869b94902dd38f2f33718908f2b5d4868e3b9241", + "reference": "869b94902dd38f2f33718908f2b5d4868e3b9241", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.1", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/config": "^6.1|^7.0", + "symfony/dependency-injection": "^6.4.12|^7.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.1|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4", + "symfony/polyfill-mbstring": "~1.0", + "symfony/routing": "^6.4|^7.0" + }, + "conflict": { + "doctrine/annotations": "<1.13.1", + "doctrine/persistence": "<1.3", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/asset": "<5.4", + "symfony/asset-mapper": "<6.4", + "symfony/clock": "<6.3", + "symfony/console": "<5.4|>=7.0", + "symfony/dom-crawler": "<6.4", + "symfony/dotenv": "<5.4", + "symfony/form": "<5.4", + "symfony/http-client": "<6.3", + "symfony/lock": "<5.4", + "symfony/mailer": "<5.4", + "symfony/messenger": "<6.3", + "symfony/mime": "<6.4", + "symfony/property-access": "<5.4", + "symfony/property-info": "<5.4", + "symfony/runtime": "<5.4.45|>=6.0,<6.4.13|>=7.0,<7.1.6", + "symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4", + "symfony/security-core": "<5.4", + "symfony/security-csrf": "<5.4", + "symfony/serializer": "<6.4", + "symfony/stopwatch": "<5.4", + "symfony/translation": "<6.4", + "symfony/twig-bridge": "<5.4", + "symfony/twig-bundle": "<5.4", + "symfony/validator": "<6.4", + "symfony/web-profiler-bundle": "<6.4", + "symfony/workflow": "<6.4" + }, + "require-dev": { + "doctrine/annotations": "^1.13.1|^2", + "doctrine/persistence": "^1.3|^2|^3", + "dragonmantank/cron-expression": "^3.1", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "seld/jsonlint": "^1.10", + "symfony/asset": "^5.4|^6.0|^7.0", + "symfony/asset-mapper": "^6.4|^7.0", + "symfony/browser-kit": "^5.4|^6.0|^7.0", + "symfony/clock": "^6.2|^7.0", + "symfony/console": "^5.4.9|^6.0.9|^7.0", + "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/dotenv": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/form": "^5.4|^6.0|^7.0", + "symfony/html-sanitizer": "^6.1|^7.0", + "symfony/http-client": "^6.3|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/mailer": "^5.4|^6.0|^7.0", + "symfony/messenger": "^6.3|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/notifier": "^5.4|^6.0|^7.0", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0", + "symfony/scheduler": "^6.4.4|^7.0.4", + "symfony/security-bundle": "^5.4|^6.0|^7.0", + "symfony/semaphore": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/string": "^5.4|^6.0|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/twig-bundle": "^5.4|^6.0|^7.0", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/web-link": "^5.4|^6.0|^7.0", + "symfony/workflow": "^6.4|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0", + "twig/twig": "^2.10|^3.0.4" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\FrameworkBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/framework-bundle/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-30T07:06:12+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "0341e41d8d8830c31a1dff5cbc5bdb3ec872a073" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/0341e41d8d8830c31a1dff5cbc5bdb3ec872a073", + "reference": "0341e41d8d8830c31a1dff5cbc5bdb3ec872a073", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:14:14+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "b81dcdbe34b8e8f7b3fc7b2a47fa065d5bf30726" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b81dcdbe34b8e8f7b3fc7b2a47fa065d5bf30726", + "reference": "b81dcdbe34b8e8f7b3fc7b2a47fa065d5bf30726", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<5.4", + "symfony/cache": "<5.4", + "symfony/config": "<6.1", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<5.4", + "symfony/form": "<5.4", + "symfony/http-client": "<5.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<5.4", + "symfony/messenger": "<5.4", + "symfony/translation": "<5.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<5.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.3", + "twig/twig": "<2.13" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^5.4|^6.0|^7.0", + "symfony/clock": "^6.2|^7.0", + "symfony/config": "^6.1|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dom-crawler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4.5|^6.0.5|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.4|^7.0.4", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4|^6.0|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.4|^7.0", + "symfony/var-exporter": "^6.2|^7.0", + "twig/twig": "^2.13|^3.0.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-31T09:23:30+00:00" + }, + { + "name": "symfony/intl", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/intl.git", + "reference": "c0938cd29804e65308051a42d1387f0dd57e1eaf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/intl/zipball/c0938cd29804e65308051a42d1387f0dd57e1eaf", + "reference": "c0938cd29804e65308051a42d1387f0dd57e1eaf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Intl\\": "" + }, + "exclude-from-classmap": [ + "/Tests/", + "/Resources/data/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Eriksen Costa", + "email": "eriksen.costa@infranology.com.br" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides access to the localization data of the ICU library", + "homepage": "https://symfony.com", + "keywords": [ + "i18n", + "icu", + "internationalization", + "intl", + "l10n", + "localization" + ], + "support": { + "source": "https://github.com/symfony/intl/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:14:14+00:00" + }, + { + "name": "symfony/monolog-bridge", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bridge.git", + "reference": "b0ff45e8d9289062a963deaf8b55e92488322e3f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/b0ff45e8d9289062a963deaf8b55e92488322e3f", + "reference": "b0ff45e8d9289062a963deaf8b55e92488322e3f", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.25.1|^2|^3", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/console": "<5.4", + "symfony/http-foundation": "<5.4", + "symfony/security-core": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/mailer": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/security-core": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Monolog\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Monolog with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/monolog-bridge/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:14:14+00:00" + }, + { + "name": "symfony/monolog-bundle", + "version": "v3.10.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bundle.git", + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.25.1 || ^2.0 || ^3.0", + "php": ">=7.2.5", + "symfony/config": "^5.4 || ^6.0 || ^7.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", + "symfony/monolog-bridge": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/phpunit-bridge": "^6.3 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MonologBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony MonologBundle", + "homepage": "https://symfony.com", + "keywords": [ + "log", + "logging" + ], + "support": { + "issues": "https://github.com/symfony/monolog-bundle/issues", + "source": "https://github.com/symfony/monolog-bundle/tree/v3.10.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-06T17:08:13+00:00" + }, + { + "name": "symfony/password-hasher", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/password-hasher.git", + "reference": "dcab5ac87450aaed26483ba49c2ce86808da7557" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/dcab5ac87450aaed26483ba49c2ce86808da7557", + "reference": "dcab5ac87450aaed26483ba49c2ce86808da7557", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "symfony/security-core": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/security-core": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PasswordHasher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Robin Chalas", + "email": "robin.chalas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides password hashing utilities", + "homepage": "https://symfony.com", + "keywords": [ + "hashing", + "password" + ], + "support": { + "source": "https://github.com/symfony/password-hasher/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-10T08:14:14+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-icu", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-icu.git", + "reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c", + "reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance and support of other locales than \"en\"" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Icu\\": "" + }, + "classmap": [ + "Resources/stubs" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's ICU-related data and classes", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "icu", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-20T22:24:30+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, "funding": [ { @@ -2292,39 +6223,126 @@ "type": "tidelift" } ], - "time": "2023-01-24T14:02:46+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/error-handler", - "version": "v4.4.44", + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", "source": { "type": "git", - "url": "https://github.com/symfony/error-handler.git", - "reference": "be731658121ef2d8be88f3a1ec938148a9237291" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/be731658121ef2d8be88f3a1ec938148a9237291", - "reference": "be731658121ef2d8be88f3a1ec938148a9237291", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { - "php": ">=7.1.3", - "psr/log": "^1|^2|^3", - "symfony/debug": "^4.4.5", - "symfony/var-dumper": "^4.4|^5.0" + "ext-iconv": "*", + "php": ">=7.2" }, - "require-dev": { - "symfony/http-kernel": "^4.4|^5.0", - "symfony/serializer": "^4.4|^5.0" + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Component\\ErrorHandler\\": "" + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, - "exclude-from-classmap": [ - "/Tests/" + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2333,18 +6351,188 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides tools to manage errors and ease debugging PHP code", + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], "support": { - "source": "https://github.com/symfony/error-handler/tree/v4.4.44" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" }, "funding": [ { @@ -2360,52 +6548,34 @@ "type": "tidelift" } ], - "time": "2022-07-28T16:29:46+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/event-dispatcher", - "version": "v4.4.44", + "name": "symfony/property-access", + "version": "v6.4.24", "source": { "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "1e866e9e5c1b22168e0ce5f0b467f19bba61266a" + "url": "https://github.com/symfony/property-access.git", + "reference": "a33acdae7c76f837c1db5465cc3445adf3ace94a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/1e866e9e5c1b22168e0ce5f0b467f19bba61266a", - "reference": "1e866e9e5c1b22168e0ce5f0b467f19bba61266a", + "url": "https://api.github.com/repos/symfony/property-access/zipball/a33acdae7c76f837c1db5465cc3445adf3ace94a", + "reference": "a33acdae7c76f837c1db5465cc3445adf3ace94a", "shasum": "" }, "require": { - "php": ">=7.1.3", - "symfony/event-dispatcher-contracts": "^1.1", - "symfony/polyfill-php80": "^1.16" - }, - "conflict": { - "symfony/dependency-injection": "<3.4" - }, - "provide": { - "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "1.1" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/property-info": "^5.4|^6.0|^7.0" }, "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^3.4|^4.0|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/error-handler": "~3.4|~4.4", - "symfony/expression-language": "^3.4|^4.0|^5.0", - "symfony/http-foundation": "^3.4|^4.0|^5.0", - "symfony/service-contracts": "^1.1|^2", - "symfony/stopwatch": "^3.4|^4.0|^5.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" + "symfony/cache": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" + "Symfony\\Component\\PropertyAccess\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2425,10 +6595,21 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "description": "Provides functions to read and write from/to an object or array using a simple string notation", "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.44" + "source": "https://github.com/symfony/property-access/tree/v6.4.24" }, "funding": [ { @@ -2439,48 +6620,59 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2022-07-20T09:59:04+00:00" + "time": "2025-07-15T12:03:16+00:00" }, { - "name": "symfony/event-dispatcher-contracts", - "version": "v1.10.0", + "name": "symfony/property-info", + "version": "v6.4.24", "source": { "type": "git", - "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "761c8b8387cfe5f8026594a75fdf0a4e83ba6974" + "url": "https://github.com/symfony/property-info.git", + "reference": "1056ae3621eeddd78d7c5ec074f1c1784324eec6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/761c8b8387cfe5f8026594a75fdf0a4e83ba6974", - "reference": "761c8b8387cfe5f8026594a75fdf0a4e83ba6974", + "url": "https://api.github.com/repos/symfony/property-info/zipball/1056ae3621eeddd78d7c5ec074f1c1784324eec6", + "reference": "1056ae3621eeddd78d7c5ec074f1c1784324eec6", "shasum": "" }, "require": { - "php": ">=7.1.3" + "php": ">=8.1", + "symfony/string": "^5.4|^6.0|^7.0" }, - "suggest": { - "psr/event-dispatcher": "", - "symfony/event-dispatcher-implementation": "" + "conflict": { + "doctrine/annotations": "<1.12", + "phpdocumentor/reflection-docblock": "<5.2", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/cache": "<5.4", + "symfony/dependency-injection": "<5.4|>=6.0,<6.4", + "symfony/serializer": "<5.4" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "1.1-dev" - } + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "phpdocumentor/reflection-docblock": "^5.2", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/serializer": "^5.4|^6.4|^7.0" }, + "type": "library", "autoload": { "psr-4": { - "Symfony\\Contracts\\EventDispatcher\\": "" - } + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2488,26 +6680,26 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to dispatching event", + "description": "Extracts information about PHP class' properties using metadata of popular sources", "homepage": "https://symfony.com", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v1.10.0" + "source": "https://github.com/symfony/property-info/tree/v6.4.24" }, "funding": [ { @@ -2518,47 +6710,48 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2022-07-20T09:59:04+00:00" + "time": "2025-07-14T16:38:25+00:00" }, { - "name": "symfony/http-client-contracts", - "version": "v2.5.3", + "name": "symfony/proxy-manager-bridge", + "version": "v6.4.24", "source": { "type": "git", - "url": "https://github.com/symfony/http-client-contracts.git", - "reference": "e5cc97c2b4a4db0ba26bebc154f1426e3fd1d2f1" + "url": "https://github.com/symfony/proxy-manager-bridge.git", + "reference": "2a14a1539f2854a8adb73319abf8923b1d7a6589" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/e5cc97c2b4a4db0ba26bebc154f1426e3fd1d2f1", - "reference": "e5cc97c2b4a4db0ba26bebc154f1426e3fd1d2f1", + "url": "https://api.github.com/repos/symfony/proxy-manager-bridge/zipball/2a14a1539f2854a8adb73319abf8923b1d7a6589", + "reference": "2a14a1539f2854a8adb73319abf8923b1d7a6589", "shasum": "" }, "require": { - "php": ">=7.2.5" - }, - "suggest": { - "symfony/http-client-implementation": "" + "friendsofphp/proxy-manager-lts": "^1.0.2", + "php": ">=8.1", + "symfony/dependency-injection": "^6.3|^7.0", + "symfony/deprecation-contracts": "^2.5|^3" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "2.5-dev" - } + "require-dev": { + "symfony/config": "^6.1|^7.0" }, + "type": "symfony-bridge", "autoload": { "psr-4": { - "Symfony\\Contracts\\HttpClient\\": "" - } + "Symfony\\Bridge\\ProxyManager\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2566,26 +6759,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to HTTP clients", + "description": "Provides integration for ProxyManager with various Symfony components", "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], "support": { - "source": "https://github.com/symfony/http-client-contracts/tree/v2.5.3" + "source": "https://github.com/symfony/proxy-manager-bridge/tree/v6.4.24" }, "funding": [ { @@ -2596,41 +6781,54 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-03-26T19:42:53+00:00" + "time": "2025-07-14T16:38:25+00:00" }, { - "name": "symfony/http-foundation", - "version": "v4.4.49", + "name": "symfony/routing", + "version": "v6.4.24", "source": { "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "191413c7b832c015bb38eae963f2e57498c3c173" + "url": "https://github.com/symfony/routing.git", + "reference": "e4f94e625c8e6f910aa004a0042f7b2d398278f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/191413c7b832c015bb38eae963f2e57498c3c173", - "reference": "191413c7b832c015bb38eae963f2e57498c3c173", + "url": "https://api.github.com/repos/symfony/routing/zipball/e4f94e625c8e6f910aa004a0042f7b2d398278f5", + "reference": "e4f94e625c8e6f910aa004a0042f7b2d398278f5", "shasum": "" }, "require": { - "php": ">=7.1.3", - "symfony/mime": "^4.3|^5.0", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "symfony/config": "<6.2", + "symfony/dependency-injection": "<5.4", + "symfony/yaml": "<5.4" }, "require-dev": { - "predis/predis": "~1.0", - "symfony/expression-language": "^3.4|^4.0|^5.0" + "doctrine/annotations": "^1.12|^2", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.2|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\HttpFoundation\\": "" + "Symfony\\Component\\Routing\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2650,10 +6848,16 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Defines an object-oriented layer for the HTTP specification", + "description": "Maps an HTTP request to a set of configuration variables", "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], "support": { - "source": "https://github.com/symfony/http-foundation/tree/v4.4.49" + "source": "https://github.com/symfony/routing/tree/v6.4.24" }, "funding": [ { @@ -2664,77 +6868,53 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2022-11-04T16:17:57+00:00" + "time": "2025-07-15T08:46:37+00:00" }, { - "name": "symfony/http-kernel", - "version": "v4.4.51", + "name": "symfony/runtime", + "version": "v6.4.24", "source": { "type": "git", - "url": "https://github.com/symfony/http-kernel.git", - "reference": "ad8ab192cb619ff7285c95d28c69b36d718416c7" + "url": "https://github.com/symfony/runtime.git", + "reference": "c1cc6721646f546627236c57f835272806087337" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/ad8ab192cb619ff7285c95d28c69b36d718416c7", - "reference": "ad8ab192cb619ff7285c95d28c69b36d718416c7", + "url": "https://api.github.com/repos/symfony/runtime/zipball/c1cc6721646f546627236c57f835272806087337", + "reference": "c1cc6721646f546627236c57f835272806087337", "shasum": "" }, "require": { - "php": ">=7.1.3", - "psr/log": "^1|^2", - "symfony/error-handler": "^4.4", - "symfony/event-dispatcher": "^4.4", - "symfony/http-client-contracts": "^1.1|^2", - "symfony/http-foundation": "^4.4.30|^5.3.7", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.16" + "composer-plugin-api": "^1.0|^2.0", + "php": ">=8.1" }, "conflict": { - "symfony/browser-kit": "<4.3", - "symfony/config": "<3.4", - "symfony/console": ">=5", - "symfony/dependency-injection": "<4.3", - "symfony/translation": "<4.2", - "twig/twig": "<1.43|<2.13,>=2" - }, - "provide": { - "psr/log-implementation": "1.0|2.0" + "symfony/dotenv": "<5.4" }, "require-dev": { - "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^4.3|^5.0", - "symfony/config": "^3.4|^4.0|^5.0", - "symfony/console": "^3.4|^4.0", - "symfony/css-selector": "^3.4|^4.0|^5.0", - "symfony/dependency-injection": "^4.3|^5.0", - "symfony/dom-crawler": "^3.4|^4.0|^5.0", - "symfony/expression-language": "^3.4|^4.0|^5.0", - "symfony/finder": "^3.4|^4.0|^5.0", - "symfony/process": "^3.4|^4.0|^5.0", - "symfony/routing": "^3.4|^4.0|^5.0", - "symfony/stopwatch": "^3.4|^4.0|^5.0", - "symfony/templating": "^3.4|^4.0|^5.0", - "symfony/translation": "^4.2|^5.0", - "symfony/translation-contracts": "^1.1|^2", - "twig/twig": "^1.43|^2.13|^3.0.4" + "composer/composer": "^1.0.2|^2.0", + "symfony/console": "^5.4.9|^6.0.9|^7.0", + "symfony/dotenv": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0" }, - "suggest": { - "symfony/browser-kit": "", - "symfony/config": "", - "symfony/console": "", - "symfony/dependency-injection": "" + "type": "composer-plugin", + "extra": { + "class": "Symfony\\Component\\Runtime\\Internal\\ComposerPlugin" }, - "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\HttpKernel\\": "" + "Symfony\\Component\\Runtime\\": "", + "Symfony\\Runtime\\Symfony\\Component\\": "Internal/" }, "exclude-from-classmap": [ "/Tests/" @@ -2746,18 +6926,21 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides a structured process for converting a Request into a Response", + "description": "Enables decoupling PHP applications from global state", "homepage": "https://symfony.com", + "keywords": [ + "runtime" + ], "support": { - "source": "https://github.com/symfony/http-kernel/tree/v4.4.51" + "source": "https://github.com/symfony/runtime/tree/v6.4.24" }, "funding": [ { @@ -2768,54 +6951,89 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2023-11-10T13:31:29+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { - "name": "symfony/mime", - "version": "v5.4.41", + "name": "symfony/security-bundle", + "version": "v6.4.24", "source": { "type": "git", - "url": "https://github.com/symfony/mime.git", - "reference": "c71c7a1aeed60b22d05e738197e31daf2120bd42" + "url": "https://github.com/symfony/security-bundle.git", + "reference": "3b1b64ab12e74d76fedddd1df1fa68bd014d3efb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/c71c7a1aeed60b22d05e738197e31daf2120bd42", - "reference": "c71c7a1aeed60b22d05e738197e31daf2120bd42", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/3b1b64ab12e74d76fedddd1df1fa68bd014d3efb", + "reference": "3b1b64ab12e74d76fedddd1df1fa68bd014d3efb", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-intl-idn": "^1.10", - "symfony/polyfill-mbstring": "^1.0", - "symfony/polyfill-php80": "^1.16" + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.1", + "symfony/clock": "^6.3|^7.0", + "symfony/config": "^6.1|^7.0", + "symfony/dependency-injection": "^6.4.11|^7.1.4", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.2|^7.0", + "symfony/http-kernel": "^6.2", + "symfony/password-hasher": "^5.4|^6.0|^7.0", + "symfony/security-core": "^6.2|^7.0", + "symfony/security-csrf": "^5.4|^6.0|^7.0", + "symfony/security-http": "^6.3.6|^7.0", + "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "egulias/email-validator": "~3.0.0", - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", - "symfony/mailer": "<4.4", - "symfony/serializer": "<5.4.35|>=6,<6.3.12|>=6.4,<6.4.3" + "symfony/browser-kit": "<5.4", + "symfony/console": "<5.4", + "symfony/framework-bundle": "<6.4", + "symfony/http-client": "<5.4", + "symfony/ldap": "<5.4", + "symfony/serializer": "<6.4", + "symfony/twig-bundle": "<5.4", + "symfony/validator": "<6.4" }, "require-dev": { - "egulias/email-validator": "^2.1.10|^3.1|^4", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/process": "^5.4|^6.4", - "symfony/property-access": "^4.4|^5.1|^6.0", - "symfony/property-info": "^4.4|^5.1|^6.0", - "symfony/serializer": "^5.4.35|~6.3.12|^6.4.3" - }, - "type": "library", + "symfony/asset": "^5.4|^6.0|^7.0", + "symfony/browser-kit": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/dom-crawler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/form": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/ldap": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/translation": "^5.4|^6.0|^7.0", + "symfony/twig-bridge": "^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^5.4|^6.0|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0", + "twig/twig": "^2.13|^3.0.4", + "web-token/jwt-checker": "^3.1", + "web-token/jwt-signature-algorithm-ecdsa": "^3.1", + "web-token/jwt-signature-algorithm-eddsa": "^3.1", + "web-token/jwt-signature-algorithm-hmac": "^3.1", + "web-token/jwt-signature-algorithm-none": "^3.1", + "web-token/jwt-signature-algorithm-rsa": "^3.1" + }, + "type": "symfony-bundle", "autoload": { "psr-4": { - "Symfony\\Component\\Mime\\": "" + "Symfony\\Bundle\\SecurityBundle\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2835,14 +7053,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Allows manipulating MIME messages", + "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", "homepage": "https://symfony.com", - "keywords": [ - "mime", - "mime-type" - ], "support": { - "source": "https://github.com/symfony/mime/tree/v5.4.41" + "source": "https://github.com/symfony/security-bundle/tree/v6.4.24" }, "funding": [ { @@ -2853,50 +7067,67 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-06-28T09:36:24+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { - "name": "symfony/polyfill-ctype", - "version": "v1.30.0", + "name": "symfony/security-core", + "version": "v6.4.24", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "0424dff1c58f028c451efff2045f5d92410bd540" + "url": "https://github.com/symfony/security-core.git", + "reference": "8ff659ffd3b823f0b3969b6c7a602b80b6ec2e53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", - "reference": "0424dff1c58f028c451efff2045f5d92410bd540", + "url": "https://api.github.com/repos/symfony/security-core/zipball/8ff659ffd3b823f0b3969b6c7a602b80b6ec2e53", + "reference": "8ff659ffd3b823f0b3969b6c7a602b80b6ec2e53", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher-contracts": "^2.5|^3", + "symfony/password-hasher": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3" }, - "provide": { - "ext-ctype": "*" + "conflict": { + "symfony/event-dispatcher": "<5.4", + "symfony/http-foundation": "<5.4", + "symfony/ldap": "<5.4", + "symfony/security-guard": "<5.4", + "symfony/translation": "<5.4.35|>=6.0,<6.3.12|>=6.4,<6.4.3|>=7.0,<7.0.3", + "symfony/validator": "<5.4" }, - "suggest": { - "ext-ctype": "For best performance" + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "psr/container": "^1.1|^2.0", + "psr/log": "^1|^2|^3", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/ldap": "^5.4|^6.0|^7.0", + "symfony/string": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4.35|~6.3.12|^6.4.3|^7.0.3", + "symfony/validator": "^6.4|^7.0" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } + "Symfony\\Component\\Security\\Core\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2904,24 +7135,18 @@ ], "authors": [ { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for ctype functions", + "description": "Symfony Security Component - Core Library", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" + "source": "https://github.com/symfony/security-core/tree/v6.4.24" }, "funding": [ { @@ -2932,49 +7157,51 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { - "name": "symfony/polyfill-intl-idn", - "version": "v1.30.0", + "name": "symfony/security-csrf", + "version": "v7.3.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c" + "url": "https://github.com/symfony/security-csrf.git", + "reference": "2b4b0c46c901729e4e90719eacd980381f53e0a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a6e83bdeb3c84391d1dfe16f42e40727ce524a5c", - "reference": "a6e83bdeb3c84391d1dfe16f42e40727ce524a5c", + "url": "https://api.github.com/repos/symfony/security-csrf/zipball/2b4b0c46c901729e4e90719eacd980381f53e0a3", + "reference": "2b4b0c46c901729e4e90719eacd980381f53e0a3", "shasum": "" }, "require": { - "php": ">=7.1", - "symfony/polyfill-intl-normalizer": "^1.10", - "symfony/polyfill-php72": "^1.10" + "php": ">=8.2", + "symfony/security-core": "^6.4|^7.0" }, - "suggest": { - "ext-intl": "For best performance" + "conflict": { + "symfony/http-foundation": "<6.4" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0" }, + "type": "library", "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Intl\\Idn\\": "" - } + "Symfony\\Component\\Security\\Csrf\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2982,30 +7209,18 @@ ], "authors": [ { - "name": "Laurent Bassin", - "email": "laurent@bassin.info" - }, - { - "name": "Trevor Rowbotham", - "email": "trevor.rowbotham@pm.me" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "description": "Symfony Security Component - CSRF Library", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "idn", - "intl", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.30.0" + "source": "https://github.com/symfony/security-csrf/tree/v7.3.0" }, "funding": [ { @@ -3021,44 +7236,59 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2025-01-02T18:42:10+00:00" }, { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.30.0", + "name": "symfony/security-http", + "version": "v6.4.24", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" + "url": "https://github.com/symfony/security-http.git", + "reference": "bd6ce061b70071afea0a4805903b6ed3f6f64e07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", - "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "url": "https://api.github.com/repos/symfony/security-http/zipball/bd6ce061b70071afea0a4805903b6ed3f6f64e07", + "reference": "bd6ce061b70071afea0a4805903b6ed3f6f64e07", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-foundation": "^6.2|^7.0", + "symfony/http-kernel": "^6.3|^7.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3" }, - "suggest": { - "ext-intl": "For best performance" + "conflict": { + "symfony/clock": "<6.3", + "symfony/event-dispatcher": "<5.4.9|>=6,<6.0.9", + "symfony/http-client-contracts": "<3.0", + "symfony/security-bundle": "<5.4", + "symfony/security-csrf": "<5.4" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/clock": "^6.3|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^3.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/security-csrf": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4|^6.0|^7.0", + "web-token/jwt-checker": "^3.1", + "web-token/jwt-signature-algorithm-ecdsa": "^3.1" }, + "type": "library", "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + "Symfony\\Component\\Security\\Http\\": "" }, - "classmap": [ - "Resources/stubs" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -3067,26 +7297,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } ], + "description": "Symfony Security Component - HTTP Integration", + "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" + "source": "https://github.com/symfony/security-http/tree/v6.4.24" }, "funding": [ { @@ -3097,50 +7319,79 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.30.0", + "name": "symfony/serializer", + "version": "v6.4.24", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" + "url": "https://github.com/symfony/serializer.git", + "reference": "c01c719c8a837173dc100f2bd141a6271ea68a1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", - "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "url": "https://api.github.com/repos/symfony/serializer/zipball/c01c719c8a837173dc100f2bd141a6271ea68a1d", + "reference": "c01c719c8a837173dc100f2bd141a6271ea68a1d", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8" }, - "provide": { - "ext-mbstring": "*" + "conflict": { + "doctrine/annotations": "<1.12", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/dependency-injection": "<5.4", + "symfony/property-access": "<5.4", + "symfony/property-info": "<5.4.24|>=6,<6.2.11", + "symfony/uid": "<5.4", + "symfony/validator": "<6.4", + "symfony/yaml": "<5.4" }, - "suggest": { - "ext-mbstring": "For best performance" + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", + "seld/jsonlint": "^1.10", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/filesystem": "^5.4|^6.0|^7.0", + "symfony/form": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4.26|^6.3|^7.0", + "symfony/property-info": "^5.4.24|^6.2.11|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } + "Symfony\\Component\\Serializer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3148,25 +7399,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for the Mbstring extension", + "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" + "source": "https://github.com/symfony/serializer/tree/v6.4.24" }, "funding": [ { @@ -3177,40 +7421,57 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-06-19T12:30:46+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { - "name": "symfony/polyfill-php56", - "version": "v1.20.0", + "name": "symfony/service-contracts", + "version": "v3.6.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php56.git", - "reference": "54b8cd7e6c1643d78d011f3be89f3ef1f9f4c675" + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/54b8cd7e6c1643d78d011f3be89f3ef1f9f4c675", - "reference": "54b8cd7e6c1643d78d011f3be89f3ef1f9f4c675", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" }, - "type": "metapackage", + "type": "library", "extra": { "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "1.20-dev" + "dev-main": "3.6-dev" } }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" @@ -3225,16 +7486,18 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 5.6+ features to lower PHP versions", + "description": "Generic abstractions related to writing services", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" ], "support": { - "source": "https://github.com/symfony/polyfill-php56/tree/v1.20.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -3245,44 +7508,60 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2020-10-23T14:02:19+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { - "name": "symfony/polyfill-php72", - "version": "v1.30.0", + "name": "symfony/string", + "version": "v7.3.2", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "10112722600777e02d2745716b70c5db4ca70442" + "url": "https://github.com/symfony/string.git", + "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/10112722600777e02d2745716b70c5db4ca70442", - "reference": "10112722600777e02d2745716b70c5db4ca70442", + "url": "https://api.github.com/repos/symfony/string/zipball/42f505aff654e62ac7ac2ce21033818297ca89ca", + "reference": "42f505aff654e62ac7ac2ce21033818297ca89ca", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } + "conflict": { + "symfony/translation-contracts": "<2.5" }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", "autoload": { "files": [ - "bootstrap.php" + "Resources/functions.php" ], "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - } + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3298,16 +7577,18 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" ], "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.30.0" + "source": "https://github.com/symfony/string/tree/v7.3.2" }, "funding": [ { @@ -3318,46 +7599,50 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-06-19T12:30:46+00:00" + "time": "2025-07-10T08:47:49+00:00" }, { - "name": "symfony/polyfill-php73", - "version": "v1.30.0", + "name": "symfony/translation-contracts", + "version": "v3.6.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1" + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/ec444d3f3f6505bb28d11afa41e75faadebc10a1", - "reference": "ec444d3f3f6505bb28d11afa41e75faadebc10a1", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1" }, "type": "library", "extra": { "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" } }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" + "Symfony\\Contracts\\Translation\\": "" }, - "classmap": [ - "Resources/stubs" + "exclude-from-classmap": [ + "/Test/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -3374,16 +7659,18 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "description": "Generic abstractions related to translation", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.30.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" }, "funding": [ { @@ -3399,41 +7686,80 @@ "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2024-09-27T08:32:26+00:00" }, { - "name": "symfony/polyfill-php80", - "version": "v1.30.0", + "name": "symfony/twig-bridge", + "version": "v6.4.24", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" + "url": "https://github.com/symfony/twig-bridge.git", + "reference": "af9ef04e348f93410c83d04d2806103689a3d924" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", - "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", + "url": "https://api.github.com/repos/symfony/twig-bridge/zipball/af9ef04e348f93410c83d04d2806103689a3d924", + "reference": "af9ef04e348f93410c83d04d2806103689a3d924", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/translation-contracts": "^2.5|^3", + "twig/twig": "^2.13|^3.0.4" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } + "conflict": { + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/console": "<5.4", + "symfony/form": "<6.3", + "symfony/http-foundation": "<5.4", + "symfony/http-kernel": "<6.4", + "symfony/mime": "<6.2", + "symfony/serializer": "<6.4", + "symfony/translation": "<5.4", + "symfony/workflow": "<5.4" }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/asset": "^5.4|^6.0|^7.0", + "symfony/asset-mapper": "^6.3|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/form": "^6.4.20|^7.2.5", + "symfony/html-sanitizer": "^6.1|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/mime": "^6.2|^7.0", + "symfony/polyfill-intl-icu": "~1.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/security-acl": "^2.8|^3.0", + "symfony/security-core": "^5.4|^6.0|^7.0", + "symfony/security-csrf": "^5.4|^6.0|^7.0", + "symfony/security-http": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/translation": "^6.1|^7.0", + "symfony/web-link": "^5.4|^6.0|^7.0", + "symfony/workflow": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0", + "twig/cssinliner-extra": "^2.12|^3", + "twig/inky-extra": "^2.12|^3", + "twig/markdown-extra": "^2.12|^3" + }, + "type": "symfony-bridge", "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" + "Symfony\\Bridge\\Twig\\": "" }, - "classmap": [ - "Resources/stubs" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -3442,28 +7768,18 @@ ], "authors": [ { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "description": "Provides integration for Twig with various Symfony components", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" + "source": "https://github.com/symfony/twig-bridge/tree/v6.4.24" }, "funding": [ { @@ -3474,46 +7790,64 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-05-31T15:07:36+00:00" + "time": "2025-07-26T12:47:35+00:00" }, { - "name": "symfony/polyfill-php81", - "version": "v1.29.0", + "name": "symfony/twig-bundle", + "version": "v6.4.24", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d" + "url": "https://github.com/symfony/twig-bundle.git", + "reference": "3b48b6e8225495c6d2438828982b4d219ca565ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/c565ad1e63f30e7477fc40738343c62b40bc672d", - "reference": "c565ad1e63f30e7477fc40738343c62b40bc672d", + "url": "https://api.github.com/repos/symfony/twig-bundle/zipball/3b48b6e8225495c6d2438828982b4d219ca565ba", + "reference": "3b48b6e8225495c6d2438828982b4d219ca565ba", "shasum": "" }, "require": { - "php": ">=7.1" + "composer-runtime-api": ">=2.1", + "php": ">=8.1", + "symfony/config": "^6.1|^7.0", + "symfony/dependency-injection": "^6.1|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^6.2", + "symfony/twig-bridge": "^6.4", + "twig/twig": "^2.13|^3.0.4" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } + "conflict": { + "symfony/framework-bundle": "<5.4", + "symfony/translation": "<5.4" }, + "require-dev": { + "symfony/asset": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/form": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^5.4|^6.0|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4|^6.0|^7.0", + "symfony/web-link": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "symfony-bundle", "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Php81\\": "" + "Symfony\\Bundle\\TwigBundle\\": "" }, - "classmap": [ - "Resources/stubs" + "exclude-from-classmap": [ + "/Tests/" ] }, "notification-url": "https://packagist.org/downloads/", @@ -3522,24 +7856,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "description": "Provides a tight integration of Twig into the Symfony full-stack framework", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.29.0" + "source": "https://github.com/symfony/twig-bundle/tree/v6.4.24" }, "funding": [ { @@ -3550,108 +7878,131 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { - "name": "symfony/process", - "version": "v2.8.52", + "name": "symfony/var-dumper", + "version": "v7.3.2", "source": { "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8" + "url": "https://github.com/symfony/var-dumper.git", + "reference": "53205bea27450dc5c65377518b3275e126d45e75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/c3591a09c78639822b0b290d44edb69bf9f05dc8", - "reference": "c3591a09c78639822b0b290d44edb69bf9f05dc8", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/53205bea27450dc5c65377518b3275e126d45e75", + "reference": "53205bea27450dc5c65377518b3275e126d45e75", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.8-dev" - } + "conflict": { + "symfony/console": "<6.4" }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", "autoload": { + "files": [ + "Resources/functions/dump.php" + ], "psr-4": { - "Symfony\\Component\\Process\\": "" + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "url": "https://github.com/nicolas-grekas", + "type": "github" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "description": "Symfony Process Component", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v2.8.50" - }, - "time": "2018-11-11T11:18:13+00:00" + "time": "2025-07-29T20:02:46+00:00" }, { - "name": "symfony/routing", - "version": "v4.4.44", + "name": "symfony/var-exporter", + "version": "v6.4.26", "source": { "type": "git", - "url": "https://github.com/symfony/routing.git", - "reference": "f7751fd8b60a07f3f349947a309b5bdfce22d6ae" + "url": "https://github.com/symfony/var-exporter.git", + "reference": "466fcac5fa2e871f83d31173f80e9c2684743bfc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/f7751fd8b60a07f3f349947a309b5bdfce22d6ae", - "reference": "f7751fd8b60a07f3f349947a309b5bdfce22d6ae", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/466fcac5fa2e871f83d31173f80e9c2684743bfc", + "reference": "466fcac5fa2e871f83d31173f80e9c2684743bfc", "shasum": "" }, "require": { - "php": ">=7.1.3", - "symfony/polyfill-php80": "^1.16" - }, - "conflict": { - "symfony/config": "<4.2", - "symfony/dependency-injection": "<3.4", - "symfony/yaml": "<3.4" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { - "doctrine/annotations": "^1.10.4", - "psr/log": "^1|^2|^3", - "symfony/config": "^4.2|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/expression-language": "^3.4|^4.0|^5.0", - "symfony/http-foundation": "^3.4|^4.0|^5.0", - "symfony/yaml": "^3.4|^4.0|^5.0" - }, - "suggest": { - "doctrine/annotations": "For using the annotation loader", - "symfony/config": "For using the all-in-one router or any loader", - "symfony/expression-language": "For using expression matching", - "symfony/http-foundation": "For using a Symfony Request object", - "symfony/yaml": "For using the YAML loader" + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { "psr-4": { - "Symfony\\Component\\Routing\\": "" + "Symfony\\Component\\VarExporter\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -3663,24 +8014,28 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Maps an HTTP request to a set of configuration variables", + "description": "Allows exporting any serializable PHP data structure to plain PHP code", "homepage": "https://symfony.com", "keywords": [ - "router", - "routing", - "uri", - "url" + "clone", + "construct", + "export", + "hydrate", + "instantiate", + "lazy-loading", + "proxy", + "serialize" ], "support": { - "source": "https://github.com/symfony/routing/tree/v4.4.44" + "source": "https://github.com/symfony/var-exporter/tree/v6.4.26" }, "funding": [ { @@ -3691,58 +8046,49 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2022-07-20T09:59:04+00:00" + "time": "2025-09-11T09:57:09+00:00" }, { - "name": "symfony/var-dumper", - "version": "v5.4.42", + "name": "symfony/yaml", + "version": "v6.4.24", "source": { "type": "git", - "url": "https://github.com/symfony/var-dumper.git", - "reference": "0c17c56d8ea052fc33942251c75d0e28936e043d" + "url": "https://github.com/symfony/yaml.git", + "reference": "742a8efc94027624b36b10ba58e23d402f961f51" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0c17c56d8ea052fc33942251c75d0e28936e043d", - "reference": "0c17c56d8ea052fc33942251c75d0e28936e043d", + "url": "https://api.github.com/repos/symfony/yaml/zipball/742a8efc94027624b36b10ba58e23d402f961f51", + "reference": "742a8efc94027624b36b10ba58e23d402f961f51", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "^1.16" + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/console": "<4.4" + "symfony/console": "<5.4" }, "require-dev": { - "ext-iconv": "*", - "symfony/console": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^4.4|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/uid": "^5.1|^6.0", - "twig/twig": "^2.13|^3.0.4" - }, - "suggest": { - "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", - "ext-intl": "To show region name in time zone dump", - "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" + "symfony/console": "^5.4|^6.0|^7.0" }, "bin": [ - "Resources/bin/var-dump-server" + "Resources/bin/yaml-lint" ], "type": "library", "autoload": { - "files": [ - "Resources/functions/dump.php" - ], "psr-4": { - "Symfony\\Component\\VarDumper\\": "" + "Symfony\\Component\\Yaml\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -3754,22 +8100,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", - "keywords": [ - "debug", - "dump" - ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v5.4.42" + "source": "https://github.com/symfony/yaml/tree/v6.4.24" }, "funding": [ { @@ -3780,12 +8122,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-07-26T12:23:09+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { "name": "taq/pdooci", @@ -3857,42 +8203,35 @@ "homepage": "https://github.com/tildeio/rsvp.js" }, { - "name": "twig/extensions", - "version": "v1.5.4", + "name": "twig/intl-extra", + "version": "v3.21.0", "source": { "type": "git", - "url": "https://github.com/twigphp/Twig-extensions.git", - "reference": "57873c8b0c1be51caa47df2cdb824490beb16202" + "url": "https://github.com/twigphp/intl-extra.git", + "reference": "05bc5d46b9df9e62399eae53e7c0b0633298b146" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig-extensions/zipball/57873c8b0c1be51caa47df2cdb824490beb16202", - "reference": "57873c8b0c1be51caa47df2cdb824490beb16202", + "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/05bc5d46b9df9e62399eae53e7c0b0633298b146", + "reference": "05bc5d46b9df9e62399eae53e7c0b0633298b146", "shasum": "" }, "require": { - "twig/twig": "^1.27|^2.0" + "php": ">=8.1.0", + "symfony/intl": "^5.4|^6.4|^7.0", + "twig/twig": "^3.13|^4.0" }, "require-dev": { - "symfony/phpunit-bridge": "^3.4", - "symfony/translation": "^2.7|^3.4" - }, - "suggest": { - "symfony/translation": "Allow the time_diff output to be translated" + "symfony/phpunit-bridge": "^6.4|^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.5-dev" - } - }, "autoload": { - "psr-0": { - "Twig_Extensions_": "lib/" - }, "psr-4": { - "Twig\\Extensions\\": "src/" - } + "Twig\\Extra\\Intl\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3901,53 +8240,65 @@ "authors": [ { "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" } ], - "description": "Common additional features for Twig that do not directly belong in core", + "description": "A Twig extension for Intl", + "homepage": "https://twig.symfony.com", "keywords": [ - "i18n", - "text" + "intl", + "twig" ], "support": { - "issues": "https://github.com/twigphp/Twig-extensions/issues", - "source": "https://github.com/twigphp/Twig-extensions/tree/master" + "source": "https://github.com/twigphp/intl-extra/tree/v3.21.0" }, - "abandoned": true, - "time": "2018-12-05T18:34:18+00:00" + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2025-01-31T20:45:36+00:00" }, { "name": "twig/twig", - "version": "v1.44.7", + "version": "v3.21.1", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "0887422319889e442458e48e2f3d9add1a172ad5" + "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/0887422319889e442458e48e2f3d9add1a172ad5", - "reference": "0887422319889e442458e48e2f3d9add1a172ad5", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/285123877d4dd97dd7c11842ac5fb7e86e60d81d", + "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-ctype": "^1.8" + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" }, "require-dev": { - "psr/container": "^1.0", - "symfony/phpunit-bridge": "^4.4.9|^5.0.9" + "phpstan/phpstan": "^2.0", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.44-dev" - } - }, "autoload": { - "psr-0": { - "Twig_": "lib/" - }, + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], "psr-4": { "Twig\\": "src/" } @@ -3980,7 +8331,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v1.44.7" + "source": "https://github.com/twigphp/Twig/tree/v3.21.1" }, "funding": [ { @@ -3992,20 +8343,20 @@ "type": "tidelift" } ], - "time": "2022-09-28T08:38:36+00:00" + "time": "2025-05-03T07:21:55+00:00" }, { "name": "ua-parser/uap-php", - "version": "v3.9.14", + "version": "v3.10.0", "source": { "type": "git", "url": "https://github.com/ua-parser/uap-php.git", - "reference": "b796c5ea5df588e65aeb4e2c6cce3811dec4fed6" + "reference": "f44bdd1b38198801cf60b0681d2d842980e47af5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ua-parser/uap-php/zipball/b796c5ea5df588e65aeb4e2c6cce3811dec4fed6", - "reference": "b796c5ea5df588e65aeb4e2c6cce3811dec4fed6", + "url": "https://api.github.com/repos/ua-parser/uap-php/zipball/f44bdd1b38198801cf60b0681d2d842980e47af5", + "reference": "f44bdd1b38198801cf60b0681d2d842980e47af5", "shasum": "" }, "require": { @@ -4053,99 +8404,9 @@ "description": "A multi-language port of Browserscope's user agent parser.", "support": { "issues": "https://github.com/ua-parser/uap-php/issues", - "source": "https://github.com/ua-parser/uap-php/tree/v3.9.14" - }, - "time": "2020-10-02T23:36:20+00:00" - }, - { - "name": "ubccr/simplesamlphp-module-authglobus", - "version": "1.3.0", - "source": { - "type": "git", - "url": "https://github.com/ubccr/simplesamlphp-module-authglobus.git", - "reference": "d81f53960bdfdb015de267d804863e85d2efb5f6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ubccr/simplesamlphp-module-authglobus/zipball/d81f53960bdfdb015de267d804863e85d2efb5f6", - "reference": "d81f53960bdfdb015de267d804863e85d2efb5f6", - "shasum": "" - }, - "require": { - "simplesamlphp/composer-module-installer": "~1.0" - }, - "require-dev": { - "simplesamlphp/simplesamlphp": "^1.14", - "squizlabs/php_codesniffer": "2.8.0" - }, - "type": "simplesamlphp-module", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-3.0" - ], - "authors": [ - { - "name": "Rudra Chakraborty", - "email": "rudracha@buffalo.edu", - "role": "Scientific Programmer, University at Buffalo" - } - ], - "description": "Globus Auth module for SimpleSAMLphp.", - "support": { - "issues": "https://github.com/ubccr/simplesamlphp-module-authglobus/issues", - "source": "https://github.com/ubccr/simplesamlphp-module-authglobus/tree/master" - }, - "time": "2018-09-10T15:22:34+00:00" - }, - { - "name": "ubccr/simplesamlphp-module-authoidcoauth2", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/ubccr/simplesamlphp-module-authoidcoauth2.git", - "reference": "bad54f7b08bbadfee2444c8a469289a8f0ca51ad" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ubccr/simplesamlphp-module-authoidcoauth2/zipball/bad54f7b08bbadfee2444c8a469289a8f0ca51ad", - "reference": "bad54f7b08bbadfee2444c8a469289a8f0ca51ad", - "shasum": "" - }, - "require": { - "simplesamlphp/composer-module-installer": "~1.0" - }, - "require-dev": { - "simplesamlphp/simplesamlphp": "^1.14", - "squizlabs/php_codesniffer": "2.8.0" - }, - "type": "simplesamlphp-module", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-3.0" - ], - "authors": [ - { - "name": "Open XDMoD", - "email": "ccr-xdmod-help@buffalo.edu", - "role": "Open XDMoD Project Team, University at Buffalo" - }, - { - "name": "Ben Plessinger", - "email": "bpless@buffalo.edu", - "role": "Senior Scientific Programmer, University at Buffalo" - }, - { - "name": "Ryan Rathsam", - "email": "ryanrath@buffalo.edu", - "role": "Scientific Programmer, University at Buffalo" - } - ], - "description": "Oauth2 / OIDC auth module for SimpleSAMLphp.", - "support": { - "issues": "https://github.com/ubccr/simplesamlphp-module-authoidcoauth2/issues", - "source": "https://github.com/ubccr/simplesamlphp-module-authoidcoauth2/tree/v1.1.0" + "source": "https://github.com/ua-parser/uap-php/tree/v3.10.0" }, - "time": "2020-09-11T18:18:04+00:00" + "time": "2025-07-17T15:43:24+00:00" }, { "name": "webmozart/assert", @@ -4204,54 +8465,6 @@ "source": "https://github.com/webmozarts/assert/tree/1.11.0" }, "time": "2022-06-03T18:03:27+00:00" - }, - { - "name": "whitehat101/apr1-md5", - "version": "v1.0.0", - "source": { - "type": "git", - "url": "https://github.com/whitehat101/apr1-md5.git", - "reference": "8b261c9fc0481b4e9fa9d01c6ca70867b5d5e819" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/whitehat101/apr1-md5/zipball/8b261c9fc0481b4e9fa9d01c6ca70867b5d5e819", - "reference": "8b261c9fc0481b4e9fa9d01c6ca70867b5d5e819", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "4.0.*" - }, - "type": "library", - "autoload": { - "psr-4": { - "WhiteHat101\\Crypt\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jeremy Ebler", - "email": "jebler@gmail.com" - } - ], - "description": "Apache's APR1-MD5 algorithm in pure PHP", - "homepage": "https://github.com/whitehat101/apr1-md5", - "keywords": [ - "MD5", - "apr1" - ], - "support": { - "issues": "https://github.com/whitehat101/apr1-md5/issues", - "source": "https://github.com/whitehat101/apr1-md5/tree/master" - }, - "time": "2015-02-11T11:06:42+00:00" } ], "packages-dev": [ @@ -4304,24 +8517,25 @@ }, { "name": "dms/phpunit-arraysubset-asserts", - "version": "v0.5.0", + "version": "v0.4.0", "source": { "type": "git", "url": "https://github.com/rdohms/phpunit-arraysubset-asserts.git", - "reference": "aa6b9e858414e91cca361cac3b2035ee57d212e0" + "reference": "428293c2a00eceefbad71a2dbdfb913febb35de2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rdohms/phpunit-arraysubset-asserts/zipball/aa6b9e858414e91cca361cac3b2035ee57d212e0", - "reference": "aa6b9e858414e91cca361cac3b2035ee57d212e0", + "url": "https://api.github.com/repos/rdohms/phpunit-arraysubset-asserts/zipball/428293c2a00eceefbad71a2dbdfb913febb35de2", + "reference": "428293c2a00eceefbad71a2dbdfb913febb35de2", "shasum": "" }, "require": { "php": "^5.4 || ^7.0 || ^8.0", - "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0" + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0" }, "require-dev": { - "dms/coding-standard": "^9" + "dms/coding-standard": "^9", + "squizlabs/php_codesniffer": "^3.4" }, "type": "library", "autoload": { @@ -4335,43 +8549,134 @@ ], "authors": [ { - "name": "Rafael Dohms", - "email": "rdohms@gmail.com" + "name": "Rafael Dohms", + "email": "rdohms@gmail.com" + } + ], + "description": "This package provides ArraySubset and related asserts once deprecated in PHPUnit 8", + "support": { + "issues": "https://github.com/rdohms/phpunit-arraysubset-asserts/issues", + "source": "https://github.com/rdohms/phpunit-arraysubset-asserts/tree/v0.4.0" + }, + "time": "2022-02-13T15:00:28+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.0.10", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^11.0", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", + "vimeo/psalm": "^4.25 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.0.10" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" } ], - "description": "This package provides ArraySubset and related asserts once deprecated in PHPUnit 8", - "support": { - "issues": "https://github.com/rdohms/phpunit-arraysubset-asserts/issues", - "source": "https://github.com/rdohms/phpunit-arraysubset-asserts/tree/v0.5.0" - }, - "time": "2023-06-02T17:33:53+00:00" + "time": "2024-02-18T20:23:39+00:00" }, { "name": "doctrine/instantiator", - "version": "1.5.0", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^9 || ^11", + "doctrine/coding-standard": "^11", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^0.16 || ^1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.30 || ^5.4" + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" }, "type": "library", "autoload": { @@ -4398,7 +8703,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" }, "funding": [ { @@ -4414,20 +8719,20 @@ "type": "tidelift" } ], - "time": "2022-12-30T00:15:36+00:00" + "time": "2022-12-30T00:23:10+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.13.3", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", "shasum": "" }, "require": { @@ -4435,11 +8740,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -4465,7 +8771,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3" }, "funding": [ { @@ -4473,20 +8779,20 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2025-07-05T12:25:42+00:00" }, { "name": "nikic/php-parser", - "version": "v5.0.2", + "version": "v5.6.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13" + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/139676794dc1e9231bf7bcd123cfc0c99182cb13", - "reference": "139676794dc1e9231bf7bcd123cfc0c99182cb13", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/221b0d0fdf1369c71047ad1d18bb5880017bbc56", + "reference": "221b0d0fdf1369c71047ad1d18bb5880017bbc56", "shasum": "" }, "require": { @@ -4497,7 +8803,7 @@ }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" @@ -4529,9 +8835,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.0" }, - "time": "2024-03-05T20:51:40+00:00" + "time": "2025-07-27T20:03:57+00:00" }, { "name": "phar-io/manifest", @@ -4651,85 +8957,37 @@ }, "time": "2022-02-21T01:04:05+00:00" }, - { - "name": "phplang/scope-exit", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/phplang/scope-exit.git", - "reference": "239b73abe89f9414aa85a7ca075ec9445629192b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phplang/scope-exit/zipball/239b73abe89f9414aa85a7ca075ec9445629192b", - "reference": "239b73abe89f9414aa85a7ca075ec9445629192b", - "shasum": "" - }, - "require-dev": { - "phpunit/phpunit": "*" - }, - "type": "library", - "autoload": { - "psr-4": { - "PhpLang\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD" - ], - "authors": [ - { - "name": "Sara Golemon", - "email": "pollita@php.net", - "homepage": "https://twitter.com/SaraMG", - "role": "Developer" - } - ], - "description": "Emulation of SCOPE_EXIT construct from C++", - "homepage": "https://github.com/phplang/scope-exit", - "keywords": [ - "cleanup", - "exit", - "scope" - ], - "support": { - "issues": "https://github.com/phplang/scope-exit/issues", - "source": "https://github.com/phplang/scope-exit/tree/master" - }, - "time": "2016-09-17T00:15:18+00:00" - }, { "name": "phpunit/php-code-coverage", - "version": "9.2.31", + "version": "9.2.32", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", - "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", + "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.18 || ^5.0", + "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "phpunit/php-file-iterator": "^3.0.6", + "phpunit/php-text-template": "^2.0.4", + "sebastian/code-unit-reverse-lookup": "^2.0.3", + "sebastian/complexity": "^2.0.3", + "sebastian/environment": "^5.1.5", + "sebastian/lines-of-code": "^1.0.4", + "sebastian/version": "^3.0.2", + "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^9.6" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -4738,7 +8996,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -4767,7 +9025,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32" }, "funding": [ { @@ -4775,7 +9033,7 @@ "type": "github" } ], - "time": "2024-03-02T06:37:42+00:00" + "time": "2024-08-22T04:23:01+00:00" }, { "name": "phpunit/php-file-iterator", @@ -5020,45 +9278,45 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.19", + "version": "9.6.23", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8" + "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8", - "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", + "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1 || ^2", + "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", + "myclabs/deep-copy": "^1.13.1", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.28", - "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-code-coverage": "^9.2.32", + "phpunit/php-file-iterator": "^3.0.6", "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", + "phpunit/php-text-template": "^2.0.4", + "phpunit/php-timer": "^5.0.3", + "sebastian/cli-parser": "^1.0.2", + "sebastian/code-unit": "^1.0.8", "sebastian/comparator": "^4.0.8", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.5", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.2", + "sebastian/diff": "^4.0.6", + "sebastian/environment": "^5.1.5", + "sebastian/exporter": "^4.0.6", + "sebastian/global-state": "^5.0.7", + "sebastian/object-enumerator": "^4.0.4", + "sebastian/resource-operations": "^3.0.4", + "sebastian/type": "^3.2.1", "sebastian/version": "^3.0.2" }, "suggest": { @@ -5103,7 +9361,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.23" }, "funding": [ { @@ -5114,12 +9372,20 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2024-04-05T04:35:58+00:00" + "time": "2025-05-02T06:40:34+00:00" }, { "name": "sebastian/cli-parser", @@ -6085,29 +10351,57 @@ "time": "2020-09-28T06:39:44+00:00" }, { - "name": "swaggest/json-diff", - "version": "v3.10.5", + "name": "symfony/maker-bundle", + "version": "v1.64.0", "source": { "type": "git", - "url": "https://github.com/swaggest/json-diff.git", - "reference": "17bfc66b330f46e12a7e574133497a290cd79ba5" + "url": "https://github.com/symfony/maker-bundle.git", + "reference": "c86da84640b0586e92aee2b276ee3638ef2f425a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/swaggest/json-diff/zipball/17bfc66b330f46e12a7e574133497a290cd79ba5", - "reference": "17bfc66b330f46e12a7e574133497a290cd79ba5", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/c86da84640b0586e92aee2b276ee3638ef2f425a", + "reference": "c86da84640b0586e92aee2b276ee3638ef2f425a", "shasum": "" }, "require": { - "ext-json": "*" + "doctrine/inflector": "^2.0", + "nikic/php-parser": "^5.0", + "php": ">=8.1", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.2|^3", + "symfony/filesystem": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0" + }, + "conflict": { + "doctrine/doctrine-bundle": "<2.10", + "doctrine/orm": "<2.15" }, "require-dev": { - "phperf/phpunit": "4.8.37" + "composer/semver": "^3.0", + "doctrine/doctrine-bundle": "^2.5.0", + "doctrine/orm": "^2.15|^3", + "symfony/http-client": "^6.4|^7.0", + "symfony/phpunit-bridge": "^6.4.1|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/security-http": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0", + "twig/twig": "^3.0|^4.x-dev" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } }, - "type": "library", "autoload": { "psr-4": { - "Swaggest\\JsonDiff\\": "src/" + "Symfony\\Bundle\\MakerBundle\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -6116,49 +10410,213 @@ ], "authors": [ { - "name": "Viacheslav Poturaev", - "email": "vearutop@gmail.com" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "JSON diff/rearrange/patch/pointer library for PHP", + "description": "Symfony Maker helps you create empty commands, controllers, form classes, tests and more so you can forget about writing boilerplate code.", + "homepage": "https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html", + "keywords": [ + "code generator", + "dev", + "generator", + "scaffold", + "scaffolding" + ], "support": { - "issues": "https://github.com/swaggest/json-diff/issues", - "source": "https://github.com/swaggest/json-diff/tree/v3.10.5" + "issues": "https://github.com/symfony/maker-bundle/issues", + "source": "https://github.com/symfony/maker-bundle/tree/v1.64.0" }, - "time": "2023-11-17T11:12:46+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:08+00:00" }, { - "name": "swaggest/json-schema", - "version": "v0.12.42", + "name": "symfony/process", + "version": "v7.4.0", "source": { "type": "git", - "url": "https://github.com/swaggest/php-json-schema.git", - "reference": "d23adb53808b8e2da36f75bc0188546e4cbe3b45" + "url": "https://github.com/symfony/process.git", + "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/swaggest/php-json-schema/zipball/d23adb53808b8e2da36f75bc0188546e4cbe3b45", - "reference": "d23adb53808b8e2da36f75bc0188546e4cbe3b45", + "url": "https://api.github.com/repos/symfony/process/zipball/7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", + "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", "shasum": "" }, "require": { - "ext-json": "*", - "php": ">=5.4", - "phplang/scope-exit": "^1.0", - "swaggest/json-diff": "^3.8.2", - "symfony/polyfill-mbstring": "^1.19" + "php": ">=8.2" }, - "require-dev": { - "phperf/phpunit": "4.8.37" + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, - "suggest": { - "ext-mbstring": "For better performance" + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-10-16T11:21:06+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "b67e94e06a05d9572c2fa354483b3e13e3cb1898" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/b67e94e06a05d9572c2fa354483b3e13e3cb1898", + "reference": "b67e94e06a05d9572c2fa354483b3e13e3cb1898", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/service-contracts": "^2.5|^3" }, "type": "library", "autoload": { "psr-4": { - "Swaggest\\JsonSchema\\": "src/" + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } + ], + "time": "2025-07-10T08:14:14+00:00" + }, + { + "name": "symfony/web-profiler-bundle", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/web-profiler-bundle.git", + "reference": "ae16f886ab3e3ed0a8db07d2a7c4d9d60b1eafcd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/ae16f886ab3e3ed0a8db07d2a7c4d9d60b1eafcd", + "reference": "ae16f886ab3e3ed0a8db07d2a7c4d9d60b1eafcd", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/twig-bundle": "^5.4|^6.0", + "twig/twig": "^2.13|^3.0.4" + }, + "conflict": { + "symfony/form": "<5.4", + "symfony/mailer": "<5.4", + "symfony/messenger": "<5.4", + "symfony/twig-bundle": ">=7.0" + }, + "require-dev": { + "symfony/browser-kit": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\WebProfilerBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -6166,17 +10624,41 @@ ], "authors": [ { - "name": "Viacheslav Poturaev", - "email": "vearutop@gmail.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "High definition PHP structures with JSON-schema based validation", + "description": "Provides a development tool that gives detailed information about the execution of any request", + "homepage": "https://symfony.com", + "keywords": [ + "dev" + ], "support": { - "email": "vearutop@gmail.com", - "issues": "https://github.com/swaggest/php-json-schema/issues", - "source": "https://github.com/swaggest/php-json-schema/tree/v0.12.42" + "source": "https://github.com/symfony/web-profiler-bundle/tree/v6.4.24" }, - "time": "2023-09-12T14:43:42+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-20T15:15:57+00:00" }, { "name": "theseer/tokenizer", @@ -6232,11 +10714,11 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": {}, - "prefer-stable": false, + "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^7.4" + "php": "^8.2" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/config/bundles.php b/config/bundles.php new file mode 100644 index 0000000000..c6e7d818ab --- /dev/null +++ b/config/bundles.php @@ -0,0 +1,10 @@ + ['all' => true], + Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], + Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], + Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], + Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], + Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], +]; diff --git a/config/packages/cache.yaml b/config/packages/cache.yaml new file mode 100644 index 0000000000..6899b72003 --- /dev/null +++ b/config/packages/cache.yaml @@ -0,0 +1,19 @@ +framework: + cache: + # Unique name of your app: used to compute stable namespaces for cache keys. + #prefix_seed: your_vendor_name/app_name + + # The "app" cache stores to the filesystem by default. + # The data in this cache should persist between deploys. + # Other options include: + + # Redis + #app: cache.adapter.redis + #default_redis_provider: redis://localhost + + # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) + #app: cache.adapter.apcu + + # Namespaced pools use the above "app" backend by default + #pools: + #my.dedicated.cache: null diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml new file mode 100644 index 0000000000..8fd3e1ef19 --- /dev/null +++ b/config/packages/framework.yaml @@ -0,0 +1,27 @@ +# see https://symfony.com/doc/current/reference/configuration/framework.html +framework: + annotations: + enabled: false + error_controller: CCR\Errors\ErrorController + handle_all_throwables: true + secret: '%env(APP_SECRET)%' + #csrf_protection: true + http_method_override: false + + # Enables session support. Note that the session will ONLY be started if you read or write from it. + # Remove or comment this section to explicitly disable session support. + session: + handler_id: null + cookie_secure: auto + cookie_samesite: lax + storage_factory_id: session.storage.factory.native + + #esi: true + #fragments: true + php_errors: + log: true +when@test: + framework: + test: true + session: + storage_factory_id: session.storage.factory.mock_file diff --git a/config/packages/google_recaptcha.yaml b/config/packages/google_recaptcha.yaml new file mode 100644 index 0000000000..8670b13e59 --- /dev/null +++ b/config/packages/google_recaptcha.yaml @@ -0,0 +1,21 @@ +services: + + # Inject this service in your controllers/services to verify a submitted captcha. + ReCaptcha\ReCaptcha: + arguments: + $secret: '%env(GOOGLE_RECAPTCHA_SECRET)%' + $requestMethod: '@ReCaptcha\RequestMethod' + + # Curl is set here as default transport to communicate with Google servers. + # If you do not have php-curl extension, you can change for a socket or a plain POST request. + # Check out the repository for all other request methods: + # https://github.com/google/recaptcha/tree/master/src/ReCaptcha/RequestMethod + ReCaptcha\RequestMethod: '@ReCaptcha\RequestMethod\CurlPost' + ReCaptcha\RequestMethod\CurlPost: null + ReCaptcha\RequestMethod\Curl: null + +# Uncomment this line if you want to inject the site key to all your Twig templates. +# You can also inject the "google_recaptcha_site_key" container parameter to your controllers. +#twig: +# globals: +# google_recaptcha_site_key: '%google_recaptcha_site_key%' diff --git a/config/packages/maker.yaml b/config/packages/maker.yaml new file mode 100644 index 0000000000..9f650d433a --- /dev/null +++ b/config/packages/maker.yaml @@ -0,0 +1,5 @@ +when@dev: + maker: + root_namespace: 'CCR\' + generate_final_classes: true + generate_final_entities: false diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml new file mode 100644 index 0000000000..1caa402816 --- /dev/null +++ b/config/packages/monolog.yaml @@ -0,0 +1,58 @@ +monolog: + channels: + - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists + +when@dev: + monolog: + handlers: + main: + type: stream + path: "%log_dir%/exceptions.log" + level: debug + channels: ["!event"] + console: + type: console + process_psr_3_messages: false + channels: ["!event", "!doctrine", "!console"] + +when@test: + monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + channels: ["!event"] + nested: + type: stream + path: "%log_dir%/exceptions.log" + level: debug + +when@prod: + monolog: + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + excluded_http_codes: [404, 405] + buffer_size: 50 # How many messages should be saved? Prevent memory leaks + nested: + type: stream + path: php://stderr + level: debug + formatter: monolog.formatter.json + file: + type: stream + path: '%log_dir%/exceptions.log' + level: warning + channels: ['!event'] + console: + type: console + process_psr_3_messages: false + channels: ["!event", "!doctrine"] + deprecation: + type: stream + channels: [deprecation] + path: php://stderr diff --git a/config/packages/nyholm_psr7.yaml b/config/packages/nyholm_psr7.yaml new file mode 100644 index 0000000000..ade8312498 --- /dev/null +++ b/config/packages/nyholm_psr7.yaml @@ -0,0 +1,11 @@ +services: + # Register nyholm/psr7 services for autowiring with PSR-17 (HTTP factories) + Psr\Http\Message\RequestFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\ResponseFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\ServerRequestFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\StreamFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\UploadedFileFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\UriFactoryInterface: '@nyholm.psr7.psr17_factory' + + nyholm.psr7.psr17_factory: + class: Nyholm\Psr7\Factory\Psr17Factory diff --git a/config/packages/property_info.yaml b/config/packages/property_info.yaml new file mode 100644 index 0000000000..86eedb23f3 --- /dev/null +++ b/config/packages/property_info.yaml @@ -0,0 +1,3 @@ +framework: + property_info: + diff --git a/config/packages/routing.yaml b/config/packages/routing.yaml new file mode 100644 index 0000000000..4b766ce57f --- /dev/null +++ b/config/packages/routing.yaml @@ -0,0 +1,12 @@ +framework: + router: + utf8: true + + # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. + # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands + #default_uri: http://localhost + +when@prod: + framework: + router: + strict_requirements: null diff --git a/config/packages/security.yaml b/config/packages/security.yaml new file mode 100644 index 0000000000..c670b738af --- /dev/null +++ b/config/packages/security.yaml @@ -0,0 +1,38 @@ +security: + password_hashers: + CCR\Entity\User: 'auto' + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' + providers: + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + id: CCR\Security\UsernameUserProvider + all_users: + chain: + providers: [ 'app_user_provider' ] + + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + main: + lazy: true + provider: all_users + custom_authenticators: + - CCR\Security\Authenticators\FormLoginAuthenticator + - CCR\Security\Authenticators\SimpleSamlPhpAuthenticator + switch_user: true + logout: + path: xdmod_logout + invalidate_session: true + access_denied_handler: CCR\Security\AccessDeniedHandler + entry_point: CCR\Security\Authenticators\FormLoginAuthenticator + + + # Easy way to control access for large sections of your site + # Note: Only the *first* access control that matches will be used + access_control: + - { path: ^/saml/login, roles: PUBLIC_ACCESS } + - { path: ^/saml/metadata, roles: PUBLIC_ACCESS } + # - { path: ^/, roles: PUBLIC_ACCESS} + # - { path: ^/admin, roles: ROLE_ADMIN } + # - { path: ^/profile, roles: ROLE_US1ER } diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml new file mode 100644 index 0000000000..4e25e4a5bd --- /dev/null +++ b/config/packages/twig.yaml @@ -0,0 +1,7 @@ +twig: + default_path: '%kernel.project_dir%/templates' + file_name_pattern: '*.twig' + +when@test: + twig: + strict_variables: true diff --git a/config/packages/twig_extensions.yaml b/config/packages/twig_extensions.yaml new file mode 100644 index 0000000000..da780f5fa0 --- /dev/null +++ b/config/packages/twig_extensions.yaml @@ -0,0 +1,11 @@ +services: + _defaults: + public: false + autowire: true + autoconfigure: true + + # Uncomment any lines below to activate that Twig extension + #Twig\Extensions\ArrayExtension: null + #Twig\Extensions\DateExtension: null + #Twig\Extensions\IntlExtension: null + #Twig\Extensions\TextExtension: null diff --git a/config/packages/web_profiler.yaml b/config/packages/web_profiler.yaml new file mode 100644 index 0000000000..f414c16548 --- /dev/null +++ b/config/packages/web_profiler.yaml @@ -0,0 +1,21 @@ +when@dev: + # web_profiler_wdt: + # resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' + # prefix: /_wdt + # + # web_profiler_profiler: + # resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' + # prefix: /_profiler + web_profiler: + toolbar: true + intercept_redirects: false + framework: + profiler: { only_exceptions: false } +when@test: + web_profiler: + toolbar: false + intercept_redirects: false + + framework: + profiler: { collect: false } + diff --git a/config/portal_settings.php b/config/portal_settings.php new file mode 100644 index 0000000000..6d424c7bfe --- /dev/null +++ b/config/portal_settings.php @@ -0,0 +1,19 @@ +.`. + * + * Reference: https://symfony.com/doc/current/configuration.html#accessing-configuration-parameters + */ +return static function (ContainerConfigurator $container): void { + $portalSettingsData = \xd_utilities\loadConfiguration(); + foreach ($portalSettingsData as $section => $sectionData) { + foreach ($sectionData as $key => $value) { + $id = sprintf('xdmod.portal_settings.%s.%s', $section, $key); + $container->parameters()->set($id, $value); + } + } +}; diff --git a/config/preload.php b/config/preload.php new file mode 100644 index 0000000000..a46ddd35ef --- /dev/null +++ b/config/preload.php @@ -0,0 +1,5 @@ + -

Federated Open XDMoD

-

- Federated XDMoD supports the collection and aggregation of data from a number of fully-functional and individually managed XDMoD instances into a single federated instance of XDMoD capable of displaying federation-wide metrics. - Each participating institution deploys an XDMoD instance through which local data will be collected and shipped to a central Federation Hub where it is aggregated to provide a federation-wide view of the data. - Data particular to an individual center is available from the Hub by applying filters and drill-downs. -

-

-

- -
- - - Example data flow from heterogeneous computing resources to an XDMoD federated hub. - XDMoD instances X and Y ingest data into their databases from the computing resources that they monitor. - Following ingestion on the satellite instances, job data are replicated to the federated hub's database, where they are aggregated for use in the federated XDMoD user interface. - - -
-
-

-

- A simple example use of the federated module is: - Three academic instituitions each with their own HPC resource. - Each institution has its own XDMoD instance which contains the accounting data for only their HPC resource. - These institutions federate their data to a central hub. - HPC accounting data for all three HPC resources is shown on the central hub. - This central hub can then be used to report on the combined data. -

-

- This example illistrates only one use case. - The federated module supports cloud data as well as HPC. Support for other data realms is planned. - There are no pre defined limits on the number of instances that can be part of a federation. -

-

- For more information see Section II of Federating XDMoD to Monitor Affiliated Computing Resources. -

-

- Documentation avialable at https://federated.xdmod.org. -

-

- Source code and downloads at https://github.com/ubccr/xdmod-federated. -

-$key. - * - * @param str $section the section in which the desired value resides. - * @param str $key the key under which the desired value can be found. - * @param mixed $default the default value to provide if there is nothing found. - * - * @return mixed - **/ -function getConfigValue($section, $key, $default = null) -{ - try { - $result = \xd_utilities\getConfiguration($section, $key); - } catch(\Exception $e) { - $result = $default; - } - return $result; -} - -$role = getConfigValue('federated', 'role'); -if($role === 'instance'){ - $hubUrl = getConfigValue('federated', 'huburl'); - echo '

This instance is part of a federation

'; - echo 'Federation Hub: ' . $hubUrl .''; -} -elseif ($role === 'hub'){ - $db = DB::factory('datawarehouse'); - $instanceResults = $db->query('SELECT * FROM federation_instances;'); - $instances = array(); - $lastCloudQuery = array(); - $derived = 1; - foreach ($instanceResults as $instance) { - $prefix = $instance['prefix']; - $extra = json_decode($instance['extra'], true); - $instances[$prefix] = array( - 'contact' => $extra['contact'], - 'url' => $extra['url'], - 'lastCloudEvent' => null, - 'lastJobTask' => null - ); - unset($extra['contact']); - unset($extra['url']); - $instances[$prefix]['extra'] = $extra; - array_push( - $lastCloudQuery, - '(SELECT \'' . $prefix . '\' AS prefix, FROM_UNIXTIME(event_time_ts) as event_ts FROM `' . $prefix . '-modw_cloud`.`event` ORDER BY 2 DESC LIMIT 1) `A' . $derived . '`' - ); - $derived++; - } - $lastCloudResults = $db->query('SELECT * FROM ' . implode($lastCloudQuery, ' UNION ALL SELECT * FROM ')); - foreach ($lastCloudResults as $result) { - $instances[$result['prefix']]['lastCloudEvent'] = $result['event_ts']; - } - echo '

Instances that are part of this Federation

    '; - foreach($instances as $instance){ - echo '
  • ' . $instance['url'] . '

    last event retrieved (' . $instance['lastCloudEvent'] . ')
  • '; - } - echo '
'; -} -else { - echo 'This installation is not part of a federation.'; -} diff --git a/html/about/images/Case_Western_logo.png b/html/about/images/Case_Western_logo.png deleted file mode 100644 index aedb6ae1d3..0000000000 Binary files a/html/about/images/Case_Western_logo.png and /dev/null differ diff --git a/html/about/images/SDSC_logo.jpg b/html/about/images/SDSC_logo.jpg deleted file mode 100644 index 5e0c72c9df..0000000000 Binary files a/html/about/images/SDSC_logo.jpg and /dev/null differ diff --git a/html/about/images/Tufts_logo.png b/html/about/images/Tufts_logo.png deleted file mode 100644 index 4ec9bda0e0..0000000000 Binary files a/html/about/images/Tufts_logo.png and /dev/null differ diff --git a/html/about/images/access_logo.png b/html/about/images/access_logo.png deleted file mode 100644 index ec40253d86..0000000000 Binary files a/html/about/images/access_logo.png and /dev/null differ diff --git a/html/about/links.html b/html/about/links.html deleted file mode 100644 index 311c379457..0000000000 --- a/html/about/links.html +++ /dev/null @@ -1,20 +0,0 @@ - -

Links

- - - - - - - - - - - -
- -
- -
- -
diff --git a/html/about/openxd.html b/html/about/openxd.html deleted file mode 100644 index 72175a21cd..0000000000 --- a/html/about/openxd.html +++ /dev/null @@ -1,39 +0,0 @@ - -

Open XDMoD

-
-

While initially focused on the NSF XSEDE program, an open source version of XDMoD that provides similar functionality for academic and industrial HPC centers is available and undergoing continued development, namely Open XDMoD. Open XDMoD for use by academic and industrial HPC centers is available for download through GitHub (http://open.xdmod.org).

-

Highlights include:

-
    -
  • A graphical user interface with extensive graphic and analytical capability.
  • -
  • Detailed utilization metrics including number of jobs, CPU hours, wait times, job size, etc.
  • -
  • Customizable Metric Explorer where users can generate custom plots comparing multiple metrics
  • -
  • A custom report builder for the automatic generation of detailed periodic reports.
  • -
  • Support for resource managers includes
  • -
      -
    • SLURM, SGE/UGE, PBS/TORQUE/PBS Pro, LSF
    • -
    -
  • Optional modules supported
  • - -
-
- - - - - - - - - - - - - - - -
-
Fig.1 Open Source XDMoD Summary Tab

-
Fig.2 Open Source XDMoD Usage Tab

diff --git a/html/about/presentations.html b/html/about/presentations.html deleted file mode 100644 index f63691f1fa..0000000000 --- a/html/about/presentations.html +++ /dev/null @@ -1,148 +0,0 @@ - -

Presentations

-
- -
PEARC '25
-
    -
  • Nikolay A. Simakov. "Enhancing an HPC Resources Modeling Framework with a Realistic, Slurm-Like, HPC Resource Model". Presentation available at doi:10.13140/RG.2.2.16351.98724.
  • -
-
Supercomputing 2024 (SC24), Atlanta, GA
-
    -
  • Nikolay A. Simakov. "Benchmarking and Continuous Performance Monitoring of HPC Resources using the XDMoD Application Kernel Module." SIGHPC Systems Professionals Workshop HPCSYSPROS24 at SC24. November 22, 2024. The presentation is available at doi:10.13140/RG.2.2.13362.62409.
  • -
- -
2024-12-12 Internet2 Technical Exchange: Boston, MA
-
    -
  • Jennifer Schopf, "Understanding Globus Data Transfers with NetSage"
  • -
- -
ACCESS Resource Provider Workshop September 2024
-
    -
  • Aaron Weeden, "What We Do in ACCESS Metrics"
  • -
- -
PEARC24: Providence, RI
-
    -
  • Nikolay A. Simakov, "Modeling Users on High-Performance Computing Resource"
  • -
  • Tom Furlani, "ACCESS Metrics Overview and Career Guidance"
  • -
- -
Research Computing at Smaller Institutions Conference, Swarthmore College, June 2024
-
    -
  • Joseph White, "Making the Case: Monitoring and Metrics"
  • -
- -
ACCESS Resource Provider Forum May 2024
-
    -
  • Aaron Weeden, "Plans for reporting on NAIRR Pilot usage"
  • -
- -
HPC Asia 2024: Nagoya, Japan
-
    -
  • N.A. Simakov, "First Impressions of the NVIDIA Grace CPU Superchip and NVIDIA Grace Hopper Superchip and Scientific Workloads"
  • -
- -
2023-10-26 ACCESS RP Forum (virtual)
-
    -
  • How to leverage ACCESS XDMoD to facilitate Campus Champion support for campus researchers
  • -
- -
2023-09-19 Campus Champions All Champions Call (virtual)
-
    -
  • How to leverage ACCESS XDMoD to facilitate Resource Provider Operations
  • -
- -
Metrics2023: Denver, CO
-
    -
  • Dr. Abani Patra, "Measuring Performance and Usage - Evolution of the Measuring and Monitoring of NSF Supercomputing"
  • -
  • N.A. Simakov, "Feasibility of Application-Agnostic Performance per Currency Metric on an Example of Gromacs, a Molecular Dynamics Application"
  • -
  • Aaron Weeden, "The Data Analytics Framework for XDMoD"
  • -
- -
PEARC23: Portland, OR
-
    -
  • Open OnDemand, XDMoD, and ColdFront: an HPC center management toolset (tutorial)
  • -
  • Introduction to CI usage and performance data analysis with XDMoD and the new Analytics Framework. (tutorial)
  • -
  • N.A Simakov, "The Taming of the Wolf - how to use the Ookami Cray Apollo 80 system and Fujitsu A64FX processors" (workshop)
  • -
  • Dr. Jennifer M. Schopf, Doug Southworth, "EPOC Support for Cyberinfrastructure and Data Movement" (Panel discussion)
  • -
- -
Cray User Group meeting (CUG) 2023 in Helsinki, Finland, May 7 – 11, 2023
-
    -
  • N.A. Simakov, "Benchmarking High-End ARM Systems with Scientific Applications. Performance and Energy Efficiency"
  • -
- -
ISC High Performance 2023 (ISC23): Hamburg, Germany
- - -
ARM HPC User Group (AHUG) Symposium at SC 2022
-
    -
  • N.A. Simakov, “Are we ready for broader adoption of ARM in the HPC community: Benchmarks and Applications on High-End ARM Systems with XDMoD Application Kernels”
  • -
- -
PEARC22: Boston, MA
- - -
PEARC21: (virtual)
- - -
Supercomputing 2020 (SC'20): Atlanta, GA (virtual), November 18, 2020
- - -
Gateways20: Bethesda, MD (virtual), October 13, 2020
- - -
NYSERNet 2020: (virtual), October 2, 2020
- - -
PEARC20: Portland, OR (virtual)
- - -
PEARC19: Chicago, IL
- - -
2018-09-05 Research Computing Campus Champions Presentation
- - -
SC17: Denver, CO
- - -
SC16: Salt Lake City, UT
- - -
XSEDE16: Miami, FL
- - -
XSEDE15: Saint Louis, MO
- diff --git a/html/about/roadmap.php b/html/about/roadmap.php deleted file mode 100644 index ef522ff478..0000000000 --- a/html/about/roadmap.php +++ /dev/null @@ -1,60 +0,0 @@ - - * @license https://opensource.org/licenses/LGPL-3.0 LGPL-3.0 - */ - -require_once __DIR__ . '/../../configuration/linker.php'; - -/** - * Attempt to retrieve a value from the configuration located at - * $section->$key. - * - * @param str $section the section in which the desired value resides. - * @param str $key the key under which the desired value can be found. - * @param mixed $default the default value to provide if there is nothing found. - * - * @return mixed - **/ -function getConfigValue($section, $key, $default=null) -{ - try { - $result = xd_utilities\getConfiguration($section, $key); - } catch(\Exception $e) { - $result = $default; - } - return $result; -} - -$result = array(); - -$url = getConfigValue('roadmap', 'url'); -$header = getConfigValue('roadmap', 'header', ''); - -if (!empty($header)) { - $result[]="

$header

"; -} - -if (!empty($url)) { - $result[]=" - - -
- - - - - - +define('ORGANIZATION_NAME', $org['name']); +$org_abbrev = $org['abbrev']; +if (empty($org_abbrev)) { + $org_abbrev = ORGANIZATION_NAME; +}; +define('ORGANIZATION_NAME_ABBREV', $org_abbrev); + +$hierarchy = \Configuration\XdmodConfiguration::assocArrayFactory( + 'hierarchy.json', + CONFIG_DIR +); +define('HIERARCHY_TOP_LEVEL_LABEL', $hierarchy['top_level_label']); +define('HIERARCHY_TOP_LEVEL_INFO', $hierarchy['top_level_info']); +define('HIERARCHY_MIDDLE_LEVEL_LABEL', $hierarchy['middle_level_label']); +define('HIERARCHY_MIDDLE_LEVEL_INFO', $hierarchy['middle_level_info']); +define('HIERARCHY_BOTTOM_LEVEL_LABEL', $hierarchy['bottom_level_label']); +define('HIERARCHY_BOTTOM_LEVEL_INFO', $hierarchy['bottom_level_info']); + +return function (array $context) { + return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); +}; diff --git a/html/internal_dashboard/analytics/index.php b/html/internal_dashboard/analytics/index.php deleted file mode 100644 index 4e2458f796..0000000000 --- a/html/internal_dashboard/analytics/index.php +++ /dev/null @@ -1,9 +0,0 @@ -"; - } else { - echo json_encode($response); - } - - exit; -} - - -xd_security\enforceUserRequirements(array(STATUS_LOGGED_IN, STATUS_MANAGER_ROLE), 'xdDashboardUser'); - -// ===================================================== - -$pdo = DB::factory('database'); - -// ===================================================== - -switch ($operation) { - - case 'enum_account_requests': - - $results = $pdo->query("SELECT id, first_name, last_name, organization, title, email_address, field_of_science, additional_information, time_submitted, status, comments FROM AccountRequests"); - - $response['success'] = true; - $response['count'] = count($results); - $response['response'] = $results; - - $response['md5'] = md5(json_encode($response)); - - if (isset($_POST['md5only'])) { - unset($response['count']); - unset($response['response']); - } - - break; - - case 'update_request': - - $id = \xd_security\assertParameterSet('id'); - $comments = \xd_security\assertParameterSet('comments'); - - $results = $pdo->query("SELECT id FROM AccountRequests WHERE id=:id", array('id' => $id)); - - if (count($results) == 1) { - - $pdo->execute("UPDATE AccountRequests SET comments=:comments WHERE id=:id", array('comments' => $comments, 'id' => $id)); - - $response['success'] = true; - - } else { - - $response['success'] = false; - $response['message'] = 'invalid id specified'; - - } - - break; - - case 'delete_request': - - $id_parameter = \xd_security\assertParameterSet('id', '/^\d+(,\d+)*$/'); - - $id_strings = explode(',', $id_parameter); - $ids = array_map('intval', $id_strings); - - $id_placeholders = implode(', ', array_fill(0, count($ids), '?')); - $results = $pdo->execute("DELETE FROM AccountRequests WHERE id IN ($id_placeholders)", $ids); - - $response['success'] = true; - - break; - - case 'enum_existing_users': - - $group_filter = \xd_security\assertParameterSet('group_filter'); - $role_filter = \xd_security\assertParameterSet('role_filter'); - - $context_filter = isset($_REQUEST['context_filter']) ? $_REQUEST['context_filter'] : ''; - - $results = Users::getUsers($group_filter, $role_filter, $context_filter); - $filtered = array(); - foreach ($results as $user) { - if ($user['username'] !== 'Public User') { - $filtered[] = $user; - } - } - - $response['success'] = true; - $response['count'] = count($filtered); - $response['response'] = $filtered; - - break; - - case 'enum_user_types_and_roles': - - $query = "SELECT id, type, color FROM moddb.UserTypes"; - - $results = $pdo->query($query); - - $response['user_types'] = $results; - - $query = "SELECT display AS description, acl_id AS role_id FROM moddb.acls WHERE name != 'pub' ORDER BY description"; - - $results = $pdo->query($query); - - $response['user_roles'] = $results; - - $response['success'] = true; - - break; - - case 'enum_user_visits': - case 'enum_user_visits_export': - - $timeframe = strtolower(\xd_security\assertParameterSet('timeframe')); - $user_types = explode(',', \xd_security\assertParameterSet('user_types')); - - if ($timeframe !== 'year' && $timeframe !== 'month') { - - $response['success'] = false; - $response['message'] = 'invalid value specified for the timeframe'; - - break; - - } - - $response['success'] = true; - $response['stats'] = XDStatistics::getUserVisitStats($timeframe, $user_types); - - if ($operation == 'enum_user_visits_export') { - - header("Content-type: application/xls"); - header("Content-Disposition:attachment;filename=\"xdmod_visitation_stats_by_$timeframe.csv\""); - - if (isset($response['stats'][0])) { - print implode(',', array_keys($response['stats'][0])) . "\n"; - } - - $previous_timeframe = ''; - - foreach ($response['stats'] as $entry) { - - if ($previous_timeframe !== $entry['timeframe']) { - - $previous_timeframe = $entry['timeframe']; - print "\n"; - - } - - if ($entry['user_type'] == 700) { - - $entry['user_type'] = 'XSEDE'; - - $u = explode(';', $entry['username']); - - $entry['username'] = $u[1]; - - } - - print implode(',', $entry) . "\n"; - - } - - exit; - - } - - break; - - - case 'ak_arr': - - $start_date = $_REQUEST['start_date']; - $end_date = $_REQUEST['end_date']; - - $response['success'] = true; - $resource['response'] = array(array('x' => array(1, 2, 3), 'y' => array(5, 2, 1))); - $resource['count'] = count($response['response']); - - - break; - - default: - - $response['success'] = false; - $response['message'] = 'operation not recognized'; - - break; - -}//switch - -// ===================================================== - -print json_encode($response); diff --git a/html/internal_dashboard/controllers/dashboard.php b/html/internal_dashboard/controllers/dashboard.php deleted file mode 100644 index 9aa8def3d5..0000000000 --- a/html/internal_dashboard/controllers/dashboard.php +++ /dev/null @@ -1,10 +0,0 @@ -registerOperation('get_menu'); -$controller->invoke('REQUEST', 'xdDashboardUser'); - diff --git a/html/internal_dashboard/controllers/dashboard/get_menu.php b/html/internal_dashboard/controllers/dashboard/get_menu.php deleted file mode 100644 index f5b3c25943..0000000000 --- a/html/internal_dashboard/controllers/dashboard/get_menu.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ - -try { - $config = \Configuration\XdmodConfiguration::assocArrayFactory( - 'internal_dashboard.json', - CONFIG_DIR - ); - - $returnData = array( - 'success' => true, - 'response' => $config['menu'], - ); - - $returnData['count'] = count($returnData['response']); - -} catch (Exception $e) { - $returnData = array( - 'success' => false, - 'message' => $e->getMessage(), - ); -} - -echo json_encode($returnData); - diff --git a/html/internal_dashboard/controllers/log.php b/html/internal_dashboard/controllers/log.php deleted file mode 100644 index 788fdf6e67..0000000000 --- a/html/internal_dashboard/controllers/log.php +++ /dev/null @@ -1,12 +0,0 @@ -registerOperation('get_summary'); -$controller->registerOperation('get_messages'); -$controller->registerOperation('get_levels'); -$controller->invoke('REQUEST', 'xdDashboardUser'); - diff --git a/html/internal_dashboard/controllers/log/get_levels.php b/html/internal_dashboard/controllers/log/get_levels.php deleted file mode 100644 index 4b1681b170..0000000000 --- a/html/internal_dashboard/controllers/log/get_levels.php +++ /dev/null @@ -1,34 +0,0 @@ - - */ - -try { - - $returnData = array( - 'success' => true, - 'response' => array( - array('id' => \CCR\Log::EMERG, 'name' => 'Emergency'), - array('id' => \CCR\Log::ALERT, 'name' => 'Alert'), - array('id' => \CCR\Log::CRIT, 'name' => 'Critical'), - array('id' => \CCR\Log::ERR, 'name' => 'Error'), - array('id' => \CCR\Log::WARNING, 'name' => 'Warning'), - array('id' => \CCR\Log::NOTICE, 'name' => 'Notice'), - array('id' => \CCR\Log::INFO, 'name' => 'Info'), - array('id' => \CCR\Log::DEBUG, 'name' => 'Debug'), - ), - ); - - $returnData['count'] = count($returnData['response']); - -} catch (Exception $e) { - $returnData = array( - 'success' => false, - 'message' => $e->getMessage(), - ); -} - -echo json_encode($returnData); - diff --git a/html/internal_dashboard/controllers/log/get_messages.php b/html/internal_dashboard/controllers/log/get_messages.php deleted file mode 100644 index f152cc182b..0000000000 --- a/html/internal_dashboard/controllers/log/get_messages.php +++ /dev/null @@ -1,98 +0,0 @@ - - */ - -use CCR\DB; - -try { - - $pdo = DB::factory('logger'); - - $sql = ' - SELECT id, logtime, ident, priority, message - FROM log_table - '; - - $clauses = array(); - $params = array(); - - if (isset($_REQUEST['ident'])) { - $clauses[] = 'ident = ?'; - $params[] = $_REQUEST['ident']; - } - - if (isset($_REQUEST['logLevels']) && is_array($_REQUEST['logLevels'])) { - $clauses[] = 'priority IN (' . implode(',', - array_pad(array(), count($_REQUEST['logLevels']), '?')) . ')'; - $params = array_merge($params, $_REQUEST['logLevels']); - } - - if (isset($_REQUEST['only_most_recent']) && $_REQUEST['only_most_recent']) { - if (!isset($_REQUEST['ident'])) { - throw new Exception('"ident" required'); - } - - $summary = Log\Summary::factory($_REQUEST['ident']); - - if (null !== ($startRowId = $summary->getProcessStartRowId())) { - $clauses[] = 'id >= ?'; - $params[] = $startRowId; - } - - if (null !== ($endRowId = $summary->getProcessEndRowId())) { - $clauses[] = 'id <= ?'; - $params[] = $endRowId; - } - } else { - if (isset($_REQUEST['start_date'])) { - $clauses[] = 'logtime >= ?'; - $params[] = $_REQUEST['start_date'] . ' 00:00:00'; - } - - if (isset($_REQUEST['end_date'])) { - $clauses[] = 'logtime <= ?'; - $params[] = $_REQUEST['end_date'] . ' 23:59:59'; - } - } - - if (count($clauses)) { - $sql .= ' WHERE ' . implode(' AND ', $clauses); - } - - $sql .= ' ORDER BY id DESC'; - - if (isset($_REQUEST['start']) && isset($_REQUEST['limit'])) { - $sql .= sprintf( - ' LIMIT %d, %d', - $_REQUEST['start'], - $_REQUEST['limit'] - ); - } - - $returnData = array( - 'success' => true, - 'response' => $pdo->query($sql, $params), - ); - - $sql = 'SELECT COUNT(*) AS count FROM log_table'; - - if (count($clauses)) { - $sql .= ' WHERE ' . implode(' AND ', $clauses); - } - - list($countRow) = $pdo->query($sql, $params); - - $returnData['count'] = $countRow['count']; - -} catch (Exception $e) { - $returnData = array( - 'success' => false, - 'message' => $e->getMessage(), - ); -} - -echo json_encode($returnData); - diff --git a/html/internal_dashboard/controllers/log/get_summary.php b/html/internal_dashboard/controllers/log/get_summary.php deleted file mode 100644 index 39c29155e9..0000000000 --- a/html/internal_dashboard/controllers/log/get_summary.php +++ /dev/null @@ -1,26 +0,0 @@ - - */ - -try { - - $summary = Log\Summary::factory($_REQUEST['ident']); - - $returnData = array( - 'success' => true, - 'response' => array($summary->getData()), - 'count' => 1, - ); - -} catch (Exception $e) { - $returnData = array( - 'success' => false, - 'message' => $e->getMessage(), - ); -} - -echo json_encode($returnData); - diff --git a/html/internal_dashboard/controllers/mailer.php b/html/internal_dashboard/controllers/mailer.php deleted file mode 100644 index beb2e3eaae..0000000000 --- a/html/internal_dashboard/controllers/mailer.php +++ /dev/null @@ -1,106 +0,0 @@ -apply(array( - 'version' => $version, - 'contact_email' => $contact_email, - 'organization' => ORGANIZATION_NAME, - 'maintainer_signature' => MailWrapper::getMaintainerSignature(), - 'date' => date('l, j F'), - 'site_title' => \xd_utilities\getConfiguration('general', 'title'), - 'site_address' => $site_address, - 'product_name' => MailWrapper::getProductName(), - )); - - $response['success'] = true; - $response['content'] = $template->getContents(); - - break; - case 'enum_target_addresses': - $group_filter = \xd_security\assertParameterSet('group_filter'); - $acl_filter = \xd_security\assertParameterSet('role_filter'); - - list($query, $params) = \xd_dashboard\listUserEmailsByGroupAndAcl($group_filter, $acl_filter); - - $results = $pdo->query($query, $params); - - $addresses = array(); - - foreach ($results as $r) { - $addresses[] = $r['email_address']; - } - - sort($addresses); - - $response['success'] = true; - $response['count'] = count($addresses); - $response['response'] = $addresses; - - break; - case 'send_plain_mail': - $response['success'] = true; - - $title = \xd_utilities\getConfiguration('general', 'title'); - - // Send a copy of the email to the contact page recipient. - $response['status'] = MailWrapper::sendMail(array( - 'body' => \xd_security\assertParameterSet('message', '/.*/', false), - 'subject' => "[$title] " . \xd_security\assertParameterSet('subject'), - 'toAddress' => \xd_utilities\getConfiguration('general', 'contact_page_recipient'), - 'toName' => 'Undisclosed Recipients', - 'fromAddress' => \xd_utilities\getConfiguration('general', 'contact_page_recipient'), - 'fromName' => $title, - 'bcc' => \xd_security\assertParameterSet('target_addresses') - )); - break; - default: - $response['success'] = false; - $response['message'] = "Operation '$operation' not recognized"; - break; -} - -print json_encode($response); - diff --git a/html/internal_dashboard/controllers/pseudo_login.php b/html/internal_dashboard/controllers/pseudo_login.php deleted file mode 100644 index 59fdf9c592..0000000000 --- a/html/internal_dashboard/controllers/pseudo_login.php +++ /dev/null @@ -1,105 +0,0 @@ -postLogin(); - - $redirect_url = str_replace('internal_dashboard/controllers/pseudo_login.php', '', getAbsoluteURL()); - - header("Location: $redirect_url"); - - exit; - - }//if (uid set) - -?> - - - - - - - - - - - - query("SELECT id, username, first_name, last_name FROM moddb.Users ORDER BY last_name"); - - print ""; - print "\n"; - - $rIndex = 0; - - foreach ($result as $r) { - - $bgColor = ($rIndex++ % 2 == 0) ? '#eef' : '#fff'; - - $formal_name = $r['last_name'].', '.$r['first_name']; - $username = $r['username']; - - if (strpos($username, ';') !== false) { - - list($xsede_username, $dummy) = explode(';', $username); - $username = $xsede_username." (XSEDE)"; - - } - - $user_id = $r['id']; - $login_link = "Login as this user"; - - print "\n"; - - }//foreach - - print "
NameUsername 
"; - print implode('', array($formal_name, $username, $login_link)); - print "
"; - - ?> - - - - diff --git a/html/internal_dashboard/controllers/summary.php b/html/internal_dashboard/controllers/summary.php deleted file mode 100644 index 8be202f12d..0000000000 --- a/html/internal_dashboard/controllers/summary.php +++ /dev/null @@ -1,12 +0,0 @@ -registerOperation('get_config'); -$controller->registerOperation('get_portlets'); -$controller->invoke('REQUEST', 'xdDashboardUser'); - diff --git a/html/internal_dashboard/controllers/summary/get_config.php b/html/internal_dashboard/controllers/summary/get_config.php deleted file mode 100644 index 56fd1091d4..0000000000 --- a/html/internal_dashboard/controllers/summary/get_config.php +++ /dev/null @@ -1,64 +0,0 @@ - - */ - -use Log\Summary; - -try { - $config = \Configuration\XdmodConfiguration::assocArrayFactory( - 'internal_dashboard.json', - CONFIG_DIR - ); - - $summaries = array(); - - foreach ($config['summary'] as $summary) { - - // Add an empty config if none is found. - if (!isset($summary['config'])) { - $summary['config'] = array(); - } - - // Add log config. - if ($summary['class'] === 'XDMoD.Log.TabPanel') { - $logList = array(); - - foreach ($config['logs'] as $log) { - $logSummary = Summary::factory($log['ident']); - - if ($logSummary->getProcessStartRowId() === null) { - continue; - } - - $logList[] = array( - 'id' => $log['ident'] . '-log-panel', - 'ident' => $log['ident'], - 'title' => $log['title'], - ); - } - - $summary['config']['logConfigList'] = $logList; - } - - $summaries[] = $summary; - } - - $returnData = array( - 'success' => true, - 'response' => $summaries, - ); - - $returnData['count'] = count($returnData['response']); - -} catch (Exception $e) { - $returnData = array( - 'success' => false, - 'message' => $e->getMessage(), - ); -} - -echo json_encode($returnData); - diff --git a/html/internal_dashboard/controllers/summary/get_portlets.php b/html/internal_dashboard/controllers/summary/get_portlets.php deleted file mode 100644 index 22291d3e5a..0000000000 --- a/html/internal_dashboard/controllers/summary/get_portlets.php +++ /dev/null @@ -1,62 +0,0 @@ - - */ - -use Log\Summary; - -try { - $config = \Configuration\XdmodConfiguration::assocArrayFactory( - 'internal_dashboard.json', - CONFIG_DIR - ); - - $portlets = array(); - - foreach ($config['portlets'] as $portlet) { - - // Add an empty config if none is found. - if (!isset($portlet['config'])) { - $portlet['config'] = array(); - } - - $portlets[] = $portlet; - } - - // Add log portlets. - foreach ($config['logs'] as $log) { - $logSummary = Summary::factory($log['ident'], TRUE); - - if ($logSummary->getProcessStartRowId() === null) { continue; } - - $portlets[] = array( - 'class' => 'XDMoD.Log.SummaryPortlet', - 'config' => array( - 'ident' => $log['ident'], - 'title' => $log['title'], - 'linkPath' => array( - 'log-tab-panel', - $log['ident'] . '-log-panel', - ), - ), - ); - } - - $returnData = array( - 'success' => true, - 'response' => $portlets, - ); - - $returnData['count'] = count($returnData['response']); - -} catch (Exception $e) { - $returnData = array( - 'success' => false, - 'message' => $e->getMessage(), - ); -} - -echo json_encode($returnData); - diff --git a/html/internal_dashboard/controllers/user.php b/html/internal_dashboard/controllers/user.php deleted file mode 100644 index 26488847f7..0000000000 --- a/html/internal_dashboard/controllers/user.php +++ /dev/null @@ -1,10 +0,0 @@ -registerOperation('get_summary'); -$controller->invoke('REQUEST', 'xdDashboardUser'); - diff --git a/html/internal_dashboard/controllers/user/get_summary.php b/html/internal_dashboard/controllers/user/get_summary.php deleted file mode 100644 index 35d2ccbb98..0000000000 --- a/html/internal_dashboard/controllers/user/get_summary.php +++ /dev/null @@ -1,52 +0,0 @@ - - */ - -use CCR\DB; - -try { - - $pdo = DB::factory('database'); - - $sql = 'SELECT COUNT(*) AS count FROM moddb.Users'; - list($userCountRow) = $pdo->query($sql); - - // TODO: Refactor these queries. - $sql = ' - SELECT COUNT(DISTINCT user_id) AS count - FROM moddb.SessionManager - WHERE DATEDIFF(NOW(), FROM_UNIXTIME(init_time)) < 7 - '; - list($last7DaysRow) = $pdo->query($sql); - - $sql = ' - SELECT COUNT(DISTINCT user_id) AS count - FROM moddb.SessionManager - WHERE DATEDIFF(NOW(), FROM_UNIXTIME(init_time)) < 30 - '; - list($last30DaysRow) = $pdo->query($sql); - - $returnData = array( - 'success' => true, - 'response' => array( - array( - 'user_count' => $userCountRow['count'], - 'logged_in_last_7_days' => $last7DaysRow['count'], - 'logged_in_last_30_days' => $last30DaysRow['count'], - ) - ), - 'count' => 1, - ); - -} catch (Exception $e) { - $returnData = array( - 'success' => false, - 'message' => $e->getMessage(), - ); -} - -echo json_encode($returnData); - diff --git a/html/internal_dashboard/css/management.css b/html/internal_dashboard/css/management.css deleted file mode 100644 index da60049dcd..0000000000 --- a/html/internal_dashboard/css/management.css +++ /dev/null @@ -1,72 +0,0 @@ -.dashboard_user_stats_timeframe .x-form-check-wrap { - padding-left: 10px; -} - -.btn_refresh -{ - background-image: url('../images/icon_refresh.png') !important; -} - -.btn_delete, -.general_btn_close -{ - background-image: url('../images/icon_delete.png') !important; -} - -.btn_edit -{ - background-image: url('../images/icon_edit.png') !important; -} - -.btn_init_dialog -{ - background-image: url('../images/icon_dialog.png') !important; -} - -.update_highlight -{ - background-color: #eaf945; -} - -.btn_login_as -{ - background-image: url('../images/icon_login.png') !important; -} - -/* ------ Current Users Section Stylings ------- */ - -.btn_email -{ - background-image: url('../images/icon_email.png') !important; -} - -/* --------------------------------------------- */ - -.btn_group -{ - background-image: url('../images/icon_group.png') !important; -} - - -.btn_role -{ - background-image: url('../images/icon_role.png') !important; -} - -.selected_menu_item -{ - color: #00f; -} - -/* ------ Recipient Verification Window Stylings ------- */ - -.btn_email_send -{ - background-image: url('../images/icon_email_send.png') !important; -} - -.btn_email_cancel -{ - background-image: url('../images/icon_email_cancel.png') !important; -} - diff --git a/html/internal_dashboard/index.php b/html/internal_dashboard/index.php deleted file mode 100644 index f287cc199e..0000000000 --- a/html/internal_dashboard/index.php +++ /dev/null @@ -1,224 +0,0 @@ - - - - - - "."\n"; - } - ?> - - - XDMoD Internal Dashboard - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Ext.onReady(function () { - new XDMoD.AppKernel.InstanceWindow({instanceId:$instance_id}).show(); -}, window, true); - -END; - } - } - ?> - - - - diff --git a/html/internal_dashboard/js/dashboard.js b/html/internal_dashboard/js/dashboard.js deleted file mode 100644 index 869c657226..0000000000 --- a/html/internal_dashboard/js/dashboard.js +++ /dev/null @@ -1,39 +0,0 @@ -var current_users; - -var actionLogout = function () { - Ext.Ajax.request({ - url: 'controllers/controller.php', - params: {operation: 'logout'}, - method: 'POST', - callback: function(options, success, response) { - if (success) { - success = CCR.checkJSONResponseSuccess(response); - } - - if (!success) { - CCR.xdmod.ui.presentFailureResponse(response, { - title: 'XDMoD Dashboard', - wrapperMessage: 'There was a problem connecting to the dashboard service provider.' - }); - return; - } - - location.href = 'index.php'; - }//callback - });//Ext.Ajax.request -};//actionLogout - -// ------------------------------------------------ - -Ext.onReady(function () { - var factory = new XDMoD.Dashboard.Factory(); - - factory.load(function (items) { - new XDMoD.Dashboard.Viewport({ items: items }); - }); - - // Allowing functions since the 'item' function is ridiculous and looks to - // be broken by design. - Ext.ComponentMgr.all.allowFunctions = true; -}, window, true); - diff --git a/html/internal_dashboard/splash.php b/html/internal_dashboard/splash.php deleted file mode 100644 index ba62bb6863..0000000000 --- a/html/internal_dashboard/splash.php +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - XDMoD Internal Dashboard - - - - - - - -
- - '.$reject_response.''; - } - ?> - -

-

- -
- - - - - - - - - - - - - - - - - - - - -
Please Sign In Below
Username: - -
Password: - -
- -
- -
- -
- - - diff --git a/html/internal_dashboard/user_check.php b/html/internal_dashboard/user_check.php deleted file mode 100644 index 2614374c06..0000000000 --- a/html/internal_dashboard/user_check.php +++ /dev/null @@ -1,63 +0,0 @@ -postLogin(); - - $_SESSION['xdDashboardUser'] = $user->getUserID(); -} - -// Check that the user has been set in the session. -if (!isset($_SESSION['xdDashboardUser'])){ - denyWithMessage(''); - exit; -} - -// Retrieve user data. -try { - $user = XDUser::getUserByID($_SESSION['xdDashboardUser']); -} catch(Exception $e) { - denyWithMessage('There was a problem initializing your account.'); - exit; -} - -// Check that the user exists. -if (!isset($user)) { - - // There is an issue with the account (most likely deleted while the - // user was logged in, and the user refreshed the entire site) - session_destroy(); - header("Location: splash.php"); - exit; -} - -// Check that the user has access to the internal dashboard. -if ($user->isManager() == false) { - denyWithMessage('You are not allowed access to this resource.'); - exit; -} - -/** - * Deny the user access and display a message. - * - * @param string $message - */ -function denyWithMessage($message) -{ - $reject_response = $message; - - include 'splash.php'; - exit; -} diff --git a/html/password_reset.php b/html/password_reset.php deleted file mode 100644 index d87fae054c..0000000000 --- a/html/password_reset.php +++ /dev/null @@ -1,185 +0,0 @@ - - array('regexp' => RESTRICTION_RID))); - -if ($rid === false) { - $validationCheck = array( - 'status' => INVALID, - 'user_first_name' => 'INVALID', - 'user_id' => INVALID - ); -} else { - $validationCheck = XDUser::validateRID($rid); -} - - - // ------------------------------- - - if ($validationCheck['status'] == INVALID) { - -?> - - - - - - - - - <?php print $page_title; ?> - - - - - - - -
- -
- -

- - The page you are trying to access has already expired.

- If you still need to reset your password, visit the login page and click on Problem Logging In? below the login prompt. -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - <?php print "$page_title: ".ucfirst($mode); ?> Password - - - - - - - - - - - - -
- -
- -

- Welcome, . To your password, supply a new password below and click on Update.

- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - -
Your Password
Password: - - 5 characters min.
  - - - -
password not specified
-
Password Again: - - 5 characters min.
- -
-
- -
- -
- - - - diff --git a/html/report_image_renderer.php b/html/report_image_renderer.php deleted file mode 100644 index 4aba5716de..0000000000 --- a/html/report_image_renderer.php +++ /dev/null @@ -1,164 +0,0 @@ - array( - 'filter' => FILTER_VALIDATE_REGEXP, - 'options' => array('regexp' => ReportGenerator::REPORT_CHART_TYPE_REGEX) - ), - 'ref' => array( - 'filter' => FILTER_VALIDATE_REGEXP, - 'options' => array('regexp' => ReportGenerator::REPORT_CHART_REF_REGEX) - ), - 'did' => array( - 'filter' => FILTER_VALIDATE_REGEXP, - 'options' => array('regexp' => ReportGenerator::REPORT_CHART_DID_REGEX) - ), - 'start' => array( - 'filter' => FILTER_VALIDATE_REGEXP, - 'options' => array('regexp' => ReportGenerator::REPORT_DATE_REGEX) - ), - 'end' => array( - 'filter' => FILTER_VALIDATE_REGEXP, - 'options' => array('regexp' => ReportGenerator::REPORT_DATE_REGEX) - ), -); - -try { - $request = Request::createFromGlobals(); - $user = Authentication::authenticateUser($request); - - $request = filter_var_array($_REQUEST, $filters, false); - - if ($user === null) { - throw new AccessDeniedHttpException('User not authenticated'); - } - - if (!isset($request['type'])) { - throw new Exception("Thumbnail type not set"); - } - - if (!isset($request['ref'])) { - throw new Exception("Thumbnail reference not set"); - } - - switch ($request['type']) { - case 'chart_pool': - case 'volatile': - $num_matches = preg_match('/^(\d+);(\d+)$/', $request['ref'], $matches); - - if ($num_matches == 0) { - throw new Exception("Invalid thumbnail reference set"); - } - - $user_id = $matches[1]; - - if (isset($request['start']) && isset($request['end'])) { - $insertion_rank = array( - 'rank' => $matches[2], - 'start_date' => $request['start'], - 'end_date' => $request['end'], - 'did' => isset($request['did']) ? $request['did'] : '', - ); - } else { - $insertion_rank = array( - 'rank' => $matches[2], - 'did' => isset($request['did']) ? $request['did'] : '', - ); - } - - break; - - case 'report': - $num_matches = preg_match('/^((\d+)-(.+));(\d+)$/', $request['ref'], $matches); - - if ($num_matches == 0) { - throw new Exception("Invalid thumbnail reference set"); - } - - $user_id = $matches[2]; - $insertion_rank = array('report_id' => $matches[1], 'ordering' => $matches[4]); - break; - - case 'cached': - $num_matches = preg_match('/^((\d+)-(.+));(\d+)$/', $request['ref'], $matches); - - if ($num_matches == 0) { - throw new Exception("Invalid thumbnail reference set"); - } - - if (!isset($request['start']) || !isset($request['end'])) { - throw new Exception("Start and end dates not set"); - } - - $valid_start = preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $request['start']); - $valid_end = preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $request['end']); - - if (($valid_start * $valid_end) == 0) { - throw new Exception("Invalid start and/or end date supplied"); - } - - $user_id = $matches[2]; - - $insertion_rank = array( - 'report_id' => $matches[1], - 'ordering' => $matches[4], - 'start_date' => $request['start'], - 'end_date' => $request['end'], - ); - break; - - default: - throw new Exception("Invalid thumbnail type value supplied: " . $request['type']); - break; - - } // switch($request['type']) - - if ($user_id !== $user->getUserID()) { - throw new AccessDeniedHttpException('Invalid user id'); - } - - $rm = new XDReportManager($user); - - header("Content-Type: image/png"); - - $blob = $rm->fetchChartBlob($request['type'], $insertion_rank); - - $image_data_header = substr($blob, 0, 8); - - if ($image_data_header != "\x89PNG\x0d\x0a\x1a\x0a") { - throw new Exception($blob); - } - - if (in_array(md5($blob), $emptyBlobs)) { - readfile(dirname(__FILE__) . '/gui/images/report_thumbnail_no_data.png'); - exit; - } - - print $blob; - -} catch (Exception $e) { - header("Content-Type: image/png"); - $unique_id = uniqid(); - $im = imagecreatefrompng(dirname(__FILE__) . '/gui/images/report_thumbnail_error.png'); - imagestring($im, 5, 20, 505, 'Error Code: ' . $unique_id, imagecolorallocate($im, 100, 100, 100)); - imagepng($im); - - // RE-throwing this exception will allow exceptions.log to record the exception message - throw new UniqueException($unique_id, $e); -} diff --git a/html/rest/index.php b/html/rest/index.php deleted file mode 100644 index 3a89990507..0000000000 --- a/html/rest/index.php +++ /dev/null @@ -1,25 +0,0 @@ -run(); diff --git a/html/rest/maintenance.php b/html/rest/maintenance.php deleted file mode 100644 index 38bb92e646..0000000000 --- a/html/rest/maintenance.php +++ /dev/null @@ -1,9 +0,0 @@ -start(); } } - return $user; -} - -/** - * This is merely to check if a dashboard user has logged in (and not - * make use of the respective XDUser object) - * - * @return \XDUser - */ -function assertDashboardUserLoggedIn() -{ - try { - return getDashboardUser(); - } catch (SessionExpiredException $see) { - // TODO: Refactor generic catch block below to handle specific exceptions, - // which would allow this block to be removed. - throw $see; - } catch (\Exception $e) { - \xd_controller\returnJSON(array( - 'success' => false, - 'status' => $e->getMessage(), - )); - exit; - } -} - -/** - * @return \XDUser An instance of XDUser pertaining to the dashboard - * user. - * - * @throws \Exception If: - * - The session variable pertaining to the dashboard user does not - * exist. - * - The user_id stored in the session variable does not map to a - * valid XDUser. - * - The user does not have manager privileges. - */ -function getDashboardUser() -{ - if (!isset($_SESSION['xdDashboardUser'])) { - throw new \SessionExpiredException('Dashboard session expired'); - } - - $user = \XDUser::getUserByID($_SESSION['xdDashboardUser']); - - if ($user == NULL) { - throw new \Exception('User does not exist'); - } - - if ($user->isManager() == false) { - throw new \Exception('Permissions do not allow you to access the dashboard'); - } - - return $user; -} - -/** - * @return \XDUser - * - * @throws \Exception - */ -function getLoggedInUser() -{ - - if (!isset($_SESSION['xdUser'])) { - throw new \SessionExpiredException(); - } - - $user = \XDUser::getUserByID($_SESSION['xdUser']); - - if ($user == NULL) { - throw new \Exception('User does not exist'); - } - - return $user; -} - -/** - * @return \XDUser - * - * @throws \Exception - */ -function getInternalUser() -{ - - if ( - isset($_SERVER['REMOTE_ADDR']) - && $_SERVER['REMOTE_ADDR'] == '127.0.0.1' - && isset($_REQUEST['user_id']) - ) { - $user = \XDUser::getUserByID($_REQUEST['user_id']); - - if ($user == NULL) { - throw new \Exception('Internal user does not exist'); - } - } else { - throw new \Exception('Internal user not specified'); - } - return $user; } /** - * @param array $requirements - * @param string $session_variable - */ -function enforceUserRequirements($requirements, $session_variable = 'xdUser') -{ - $returnData = array(); - - if (in_array(STATUS_LOGGED_IN, $requirements)) { - if (!isset($_SESSION[$session_variable])) { - throw new \SessionExpiredException(); - } - - $user = \XDUser::getUserByID($_SESSION[$session_variable]); - - if ($user == NULL) { - $returnData['status'] = 'user_does_not_exist'; - $returnData['success'] = false; - $returnData['totalCount'] = 0; - $returnData['message'] = 'user_does_not_exist'; - $returnData['data'] = array(); - \xd_controller\returnJSON($returnData); - } - - // Manager subsumes 'Science Advisory Board Member' role - if ($user->isManager()) { - \xd_utilities\remove_element_by_value($requirements, SAB_MEMBER); - } - - if (in_array(SAB_MEMBER, $requirements)) { - - // This user must be a member of the Science Advisory Board - if (!$user->hasAcl('sab')) { - $returnData['status'] = 'not_sab_member'; - $returnData['success'] = false; - $returnData['totalCount'] = 0; - $returnData['message'] = 'not_sab_member'; - $returnData['data'] = array(); - \xd_controller\returnJSON($returnData); - } - } - - if (in_array(STATUS_MANAGER_ROLE, $requirements)) { - if (!($user->isManager())) { - $returnData['status'] = 'not_a_manager'; - $returnData['success'] = false; - $returnData['totalCount'] = 0; - $returnData['message'] = 'not_a_manager'; - $returnData['data'] = array(); - \xd_controller\returnJSON($returnData); - } - } - - if (in_array(STATUS_CENTER_DIRECTOR_ROLE, $requirements)) { - if (!$user->hasAcl(ROLE_ID_CENTER_DIRECTOR)) { - $returnData['status'] = 'not_a_center_director'; - $returnData['success'] = false; - $returnData['totalCount'] = 0; - $returnData['message'] = 'not_a_center_director'; - $returnData['data'] = array(); - \xd_controller\returnJSON($returnData); - } - } - } -} - -/** - * Ensures that all of the $_REQUEST[keys] in $required_params conform - * to their respective patterns (e.g. $required_params - * = array('uid' => RESTRICTION_UID) : $_REQUEST['uid'] has to comply - * with the pattern in RESTRICTION_UID - * - * If $enforce_all is set to 'false', then secureCheck will return an - * integer indicating how many of the params qualify (this is used for - * cases in which at least one parameter is required, but not all) - * - * @param array $required_params - * @param string $m - * @param bool $enforce_all - */ -function secureCheck(&$required_params, $m, $enforce_all = true) -{ - - // ${'_'.$m}['param'] <-- should be working, but doesn't inside this - // function - - $qualifyingParams = 0; - - if ($m == 'GET') { $param_array = $_GET; } - if ($m == 'POST') { $param_array = $_POST; } - if ($m == 'REQUEST') { $param_array = $_REQUEST; } - - foreach ($required_params as $param => $pattern) { - if (!isset($param_array[$param])) { - if ($enforce_all) { return false; } - if (!$enforce_all) { continue; } - } - - $param_array[$param] - = preg_replace('/\s+/', ' ', $param_array[$param]); - - if (preg_match($pattern, $param_array[$param]) == 0) { - if ($enforce_all) { return false; } - if (!$enforce_all) { continue; } - } - - $qualifyingParams++; - } - - if ($enforce_all) { return true; } - if (!$enforce_all) { return $qualifyingParams; } -} - -/** - * @param array $requiredParams - */ -function assertParametersSet($requiredParams = array()) -{ - foreach ($requiredParams as $k => $v) { - if (!is_int($k)) { - - // $k represents the name of the param - // $v represents the format of the value that param must conform - // to (a regex) - $param_name = $k; - $pattern = $v; - } else { - - // $v represents the name of the param - $param_name = $v; - $pattern = '/.*/'; - } - - assertParameterSet($param_name, $pattern); - } -} - -/** - * Provides a checkstop when a required argument has not been supplied - * in a web request (using GET or POST). - * - * @param string $param_name Parameter name. - * @param string $pattern Pattern parameter must match. - * @param bool $compress_whitespace True if any whitespace in the - * parameter value should be replaced with a single space - * (default: true). - * - * @return string The parameter value. - */ -function assertParameterSet( - $param_name, - $pattern = '/.*/', - $compress_whitespace = true -) { - if (!isset($_REQUEST[$param_name])) { - \xd_response\presentError("'$param_name' not specified."); - } - - $param_value = $_REQUEST[$param_name]; - - if ($compress_whitespace) { - $param_value = preg_replace('/\s+/', ' ', $param_value); - } - - $match = preg_match($pattern, $param_value); - - if ($match === false) { - \xd_response\presentError("Failed to assert '$param_name'."); - } elseif ($match == 0) { - \xd_response\presentError("Invalid value specified for '$param_name'."); - } - - return $param_value; -} - -/** - * Assert that a request parameter is set and is also a valid email address. - * - * @param string $param_name Parameter name. - * @return string The parameter value. + * Wrapper for the session_start that ensures that the secure + * cookie flag is set for the session cookie. */ -function assertEmailParameterSet($param_name) +function start_session() { - if (!isset($_REQUEST[$param_name])) { - \xd_response\presentError("'$param_name' not specified."); - } - - $param_value = $_REQUEST[$param_name]; - - if (!isEmailValid($param_value)) { - \xd_response\presentError("Failed to assert '$param_name'."); + switch (session_status()) { + case PHP_SESSION_NONE: + $cookieParams = session_get_cookie_params(); + session_set_cookie_params( + $cookieParams['lifetime'], + $cookieParams['path'], + $cookieParams['domain'], + true + ); + SessionSingleton::initSession(); + case PHP_SESSION_ACTIVE: + case PHP_SESSION_DISABLED: + default: } - return $param_value; -} - -/** - * Determine if an email address is valid. - * - * @param string $email Email address to validate. - * @return bool True if the email address is valid. - */ -function isEmailValid($email) -{ - $validator = new \Egulias\EmailValidator\EmailValidator(); - return $validator->isValid($email); } diff --git a/libraries/utilities.php b/libraries/utilities.php index 111a06f00b..218e0beb89 100644 --- a/libraries/utilities.php +++ b/libraries/utilities.php @@ -397,11 +397,15 @@ function checkForCenterLogo($apply_css = true) * \filter_var($value, $filter, $options) */ -function filter_var($value, $filter = FILTER_DEFAULT, $options = null) +function filter_var($value, $filter = FILTER_DEFAULT, $options = null): mixed { - return ( FILTER_VALIDATE_BOOLEAN == $filter && false === $value - ? false - : \filter_var($value, $filter, $options) ); + if (FILTER_VALIDATE_BOOLEAN === $filter && false === $value) { + return false; + } + if (isset($options) && (is_int($options) || is_array($options))) { + return \filter_var($value, $filter, $options); + } + return \filter_var($value, $filter); } /** @@ -414,7 +418,7 @@ function filter_var($value, $filter = FILTER_DEFAULT, $options = null) * @return A fully qualified path, with the base path prepended to a relative path */ -function qualify_path($path, $base_path) +function qualify_path(string $path, string $base_path) { if ( 0 !== strpos($path, DIRECTORY_SEPARATOR) && null !== $base_path && "" != $base_path ) { $path = $base_path . DIRECTORY_SEPARATOR . $path; @@ -440,7 +444,10 @@ function resolve_path($path) // If we don't limit to filly qualified paths then relative paths such as "../../foo" // are not properly resolved. - if ( 0 !== strpos($path, DIRECTORY_SEPARATOR) ) { + if (!isset($path)) { + return null; + } + if (!str_starts_with($path, DIRECTORY_SEPARATOR)) { return $path; } diff --git a/open_xdmod/build_scripts/templates/install.template b/open_xdmod/build_scripts/templates/install.template index 246e0d52b1..80c22611ed 100755 --- a/open_xdmod/build_scripts/templates/install.template +++ b/open_xdmod/build_scripts/templates/install.template @@ -495,7 +495,7 @@ function substitutePaths($dirs) $fileDirRegexGroup = '(__DIR__|dirname\s*\(\s*__FILE__\s*\))'; substituteInDir($destDir . $dirs['bin'], array( - "#${fileDirRegexGroup}\s*\.\s*'/\.\./configuration/linker\.php'#" + "#{$fileDirRegexGroup}\s*\.\s*'/\.\./configuration/linker\.php'#" => "'" . $dirs['data'] . "/configuration/linker.php'", '/__XDMOD_SHARE_PATH__/' => $dirs['data'], '/__XDMOD_LIB_PATH__/' => $dirs['lib'], @@ -504,9 +504,9 @@ function substitutePaths($dirs) )); substituteInDir($destDir . $dirs['lib'], array( - "#${fileDirRegexGroup}\s*\.\s*'/\.\./html/tmp'#" + "#{$fileDirRegexGroup}\s*\.\s*'/\.\./html/tmp'#" => "'" . $dirs['data'] . "/html/tmp'", - "#${fileDirRegexGroup}\s*\.\s*'/\.\./configuration/linker\.php'#" + "#{$fileDirRegexGroup}\s*\.\s*'/\.\./configuration/linker\.php'#" => "'" . $dirs['data'] . "/configuration/linker.php'", )); diff --git a/open_xdmod/modules/xdmod/build.json b/open_xdmod/modules/xdmod/build.json index 22d6f344d5..dd3613ec1d 100644 --- a/open_xdmod/modules/xdmod/build.json +++ b/open_xdmod/modules/xdmod/build.json @@ -28,23 +28,28 @@ ], "exclude_patterns": [ "#/\\.#", + "#\\.eslintrc\\.json#", "#xdmod-.*\\.rpm$#", "#xdmod-.*\\.tar\\.gz$#", "#^\\/html\\/gui\\/lib\\/extjs\\/examples\\/[A-t,v-z].*#", "#^\\/html\\/gui\\/lib\\/extjs\\/resources\\/images\\/[a,h-z].*#", "#^\\/html\\/gui\\/lib\\/extjs\\/resources\\/.*\\.swf#", - "#^\\/configuration\\/.+\\..+\\.template$#" + "#^\\/configuration\\/.+\\..+\\.template$#", + "#\\/var\\/.*#" ] }, "file_maps": { "data": [ "classes", "etl", - "html", "libraries", "templates", "tools", "vendor", + "src", + "html", + "config", + "var", { "configuration/constants.php": true }, { "configuration/linker.php" : true } ], @@ -106,11 +111,6 @@ "pre_build": [ "rm -rf vendor/", "composer install", - "sed -i 's/SimpleSAML_Error_Assertion::installHandler();//g' vendor/simplesamlphp/simplesamlphp/www/_include.php", - "patch vendor/simplesamlphp/simplesamlphp/www/errorreport.php < open_xdmod/modules/xdmod/assets/simplesamlphp-CVE-2020-5225.patch", - "patch vendor/simplesamlphp/simplesamlphp/www/module.php < open_xdmod/modules/xdmod/assets/simplesamlPHP-CVE-2020-5301.patch", - "patch vendor/simplesamlphp/simplesamlphp/lib/SimpleSAML/Utils/HTTP.php < open_xdmod/modules/xdmod/assets/simplesamlphp-SSPSA_201907-01_HTTP.patch", - "patch vendor/simplesamlphp/simplesamlphp/modules/core/www/postredirect.php < open_xdmod/modules/xdmod/assets/simplesamlphp-SSPSA_201907-01_postredirect.patch", "user_manual_builder/setup.sh", "user_manual_builder/build_user_manual.sh --builddir user_manual_builder/ --destdir html/user_manual/" ] diff --git a/open_xdmod/modules/xdmod/xdmod.spec.in b/open_xdmod/modules/xdmod/xdmod.spec.in index c37642cc27..42712cb1e2 100644 --- a/open_xdmod/modules/xdmod/xdmod.spec.in +++ b/open_xdmod/modules/xdmod/xdmod.spec.in @@ -12,7 +12,7 @@ BuildRoot: %(mktemp -ud %{_tmppath}/%{name}-%{version}__PRERELEASE__-%{relea BuildArch: noarch BuildRequires: php-cli Requires: httpd mod_ssl -Requires: php >= 7.4 php-cli php-mysqlnd php-pdo php-gd php-xml php-mbstring php-zip php-posix +Requires: php >= 8.2 php-cli php-mysqlnd php-pdo php-gd php-xml php-mbstring php-zip php-posix Requires: php-pecl-apcu php-json Requires: libreoffice-writer Requires: chromium-headless >= 111 @@ -63,6 +63,10 @@ for file in exceptions.log query.log; do chown apache:xdmod %{_localstatedir}/log/%{name}/$file chmod 0660 %{_localstatedir}/log/%{name}/$file done + +# Ensure the var directory is owned by apache so it can be written to. +chown apache:xdmod %{_datadir}/%{name}/var + if [ "$1" -ge 2 ]; then echo "Run xdmod-upgrade to complete the Open XDMoD upgrade process." echo "Refer to http://open.xdmod.org/upgrade.html for more details." @@ -76,10 +80,12 @@ rm -rf $RPM_BUILD_ROOT %defattr(0750,root,xdmod,-) %{_bindir}/%{name}-* %{_bindir}/acl-* +%{_bindir}/console %defattr(-,root,root,-) %{_libdir}/%{name}/ %{_datadir}/%{name}/ + %{_docdir}/%{name}-%{version}__PRERELEASE__/ %dir %attr(0770,apache,xdmod) %{_localstatedir}/log/%{name} @@ -92,7 +98,7 @@ rm -rf $RPM_BUILD_ROOT %config(noreplace) %{_sysconfdir}/%{name}/etl/ %config(noreplace) %{_sysconfdir}/logrotate.d/%{name} %config(noreplace) %{_sysconfdir}/cron.d/%{name} -%config(noreplace) %{_datadir}/%{name}/html/robots.txt +%config(noreplace) %{_datadir}/%{name}/config/ %dir %attr(0570,apache,xdmod) %{xdmod_export_dir} diff --git a/src/Command/UpdateSSOReferrerCommand.php b/src/Command/UpdateSSOReferrerCommand.php new file mode 100644 index 0000000000..55ebbe7b9b --- /dev/null +++ b/src/Command/UpdateSSOReferrerCommand.php @@ -0,0 +1,66 @@ +addArgument('url', InputArgument::REQUIRED, 'The url to use to trigger SSO authentication.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $url = $input->getArgument('url'); + if (!filter_var($url, FILTER_VALIDATE_URL)) { + $output->writeln("value provided must be a valid url."); + return Command::INVALID; + } + + $projectDir = $this->parameters->get('kernel.project_dir'); + $configDir = "$projectDir/config"; + $servicesFilePath = "$configDir/services.yaml"; + $servicesFileContents = Yaml::parseFile($servicesFilePath); + + if (!array_key_exists('parameters', $servicesFileContents)) { + $output->writeln('Unable to find `parameters` property in services.yaml. Unable to continue.'); + return Command::INVALID; + } + if (!array_key_exists('sso', $servicesFileContents['parameters'])) { + $output->writeln('Unable to find `sso` property in services.yaml. Unable to continue.'); + return Command::INVALID; + } + if (!array_key_exists('auth_referrer', $servicesFileContents['parameters']['sso'])) { + $output->writeln('Unable to find `auth_referrer` property in services.yaml. Unable to continue.'); + return Command::INVALID; + } + + $servicesFileContents['parameters']['sso']['auth_referrer'] = $url; + file_put_contents($servicesFilePath, Yaml::dump($servicesFileContents, 10, 4, Yaml::DUMP_OBJECT_AS_MAP )); + + return Command::SUCCESS; + } +} diff --git a/src/Controller/.gitignore b/src/Controller/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Controller/AboutController.php b/src/Controller/AboutController.php new file mode 100644 index 0000000000..f506e0f73d --- /dev/null +++ b/src/Controller/AboutController.php @@ -0,0 +1,164 @@ +render('twig/about/xdmod.html.twig', [ + 'xdmod_version' => \xd_versioning\getPortalVersion(true) + ]); + } + + /** + * @return Response + */ + #[Route('/about/open_xdmod', methods: ["GET"])] + #[Route('/about/openxd.html', methods: ["GET"])] + public function openXdmod(): Response + { + return $this->render('twig/about/open_xdmod.html.twig'); + } + + /** + * @return Response + */ + #[Route('/about/supremm', methods: ['GET'])] + #[Route('/about/supremm.html', methods: ['GET'])] + public function supremm(): Response + { + return $this->render('twig/about/supremm.html.twig'); + } + + /** + * @return Response + * @throws Exception if unable to retrieve a connection to the 'datawarehouse' DB. + */ + #[Route('/about/federated', methods: ["GET"])] + #[Route('/about/federated.html', methods: ["GET"])] + public function federated(): Response + { + $parameters = []; + $federatedRole = $this->getConfigValue('federated', 'role'); + $parameters['federated_role'] = $federatedRole; + + if ($federatedRole === 'instance') { + $parameters['hub_url'] = $this->getConfigValue('federated', 'huburl'); + } elseif ($federatedRole === 'hub') { + $db = DB::factory('datawarehouse'); + $instanceResults = $db->query('SELECT * FROM federation_instances;'); + + $instances = []; + $lastCloudQuery = []; + $derived = 1; + foreach ($instanceResults as $instance) { + $prefix = $instance['prefix']; + $extra = json_decode($instance['extra'], true); + $instances[$prefix] = [ + 'contact' => $extra['contact'], + 'url' => $extra['url'], + 'lastCloudEvent' => null, + 'lastJobTask' => null + ]; + unset($extra['contact']); + unset($extra['url']); + $instances[$prefix]['extra'] = $extra; + array_push( + $lastCloudQuery, + '(SELECT \'' . $prefix . '\' AS prefix, FROM_UNIXTIME(event_time_ts) as event_ts FROM `' . $prefix . '-modw_cloud`.`event` ORDER BY 2 DESC LIMIT 1) `A' . $derived . '`' + ); + $derived++; + } + $lastCloudResults = $db->query('SELECT * FROM ' . implode(' UNION ALL SELECT * FROM ', $lastCloudQuery)); + foreach ($lastCloudResults as $result) { + $instances[$result['prefix']]['lastCloudEvent'] = $result['event_ts']; + } + + $parameters['instances'] = $instances; + } + + return $this->render('twig/about/federated.html.twig', $parameters); + } + + /** + * @return Response + */ + #[Route('/about/roadmap', methods: ['GET'])] + #[Route('/about/roadmap.html', methods: ["GET"])] + public function roadmap(): Response + { + $header = $this->getConfigValue('roadmap', 'header'); + $url = $this->getConfigValue('roadmap', 'url'); + return $this->render('twig/about/roadmap.html.twig', [ + 'header' => $header, + 'url' => $url + ]); + } + + /** + * @return Response + */ + #[Route('/about/team', methods: ['GET'])] + #[Route('/about/team.html', methods: ['GET'])] + public function team(): Response + { + return $this->render('twig/about/team.html.twig'); + } + + /** + * @return Response + */ + #[Route('/about/publications', methods: ['GET'])] + #[Route('/about/publications.html', methods: ['GET'])] + public function publications(): Response + { + return $this->render('twig/about/publications.html.twig'); + } + + /** + * @return Response + */ + #[Route('/about/links', methods: ['GET'])] + #[Route('/about/links.html', methods: ['GET'])] + public function links(): Response + { + return $this->render('twig/about/links.html.twig'); + } + + /** + * @return Response + */ + #[Route('/about/release_notes/xdmod', methods: ['GET'])] + public function releaseNotes(): Response + { + return $this->render("twig/about/xdmod_release_notes.html.twig"); + } + + /** + * @param Request $request + * @return Response + */ + #[Route('/about/presentations', methods: ['GET'])] + #[Route('/about/presentations.html', methods: ['GET'])] + public function teamPresentations(Request $request): Response + { + return $this->render('twig/about/presentations.html.twig'); + } +} diff --git a/src/Controller/AccountController.php b/src/Controller/AccountController.php new file mode 100644 index 0000000000..bff9a956a7 --- /dev/null +++ b/src/Controller/AccountController.php @@ -0,0 +1,97 @@ + '.*'])] +class AccountController extends BaseController +{ + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route("/requests", methods: ["POST"])] + public function getRequests(Request $request): Response + { + $pdo = DB::factory('database'); + $md5Only = $this->getBooleanParam($request, 'md5only', false, false); + + $results = $pdo->query("SELECT id, first_name, last_name, organization, title, email_address, field_of_science, additional_information, time_submitted, status, comments FROM AccountRequests"); + + $response['success'] = true; + $response['count'] = count($results); + $response['response'] = $results; + + $response['md5'] = md5(json_encode($response)); + + if ($md5Only) { + unset($response['count']); + unset($response['response']); + } + + return $this->json($response); + } + + /** + * + * @param Request $request + * @param string $requestId + * @return Response + * @throws Exception + */ + #[Route("/{requestId}", methods: ["PUT"])] + public function updateRequest(Request $request, string $requestId): Response + { + $comments = $this->getStringParam($request, 'comments', true); + $pdo = DB::factory('database'); + + $results = $pdo->query('SELECT id FROM AccountRequests WHERE id=:id', ['id' => $requestId]); + + // Check to see if we have an AccountRequest that matches the provided $requestId before updating it. + if (count($results) == 1) { + $pdo->execute('UPDATE AccountRequests SET comments=:comments WHERE id=:id', ['comments' => $comments, 'id' => $requestId]); + $response['success'] = true; + } else { + $response['success'] = false; + $response['message'] = 'invalid id specified'; + } + + return $this->json($response); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route("", methods: ["DELETE"])] + public function deleteRequest(Request $request): Response + { + $requestIds = $this->getStringParam($request, 'id', true, null, '/^\d+(,\d+)*$/'); + $ids = array_map('intval', explode(',', $requestIds)); + + $queryPlaceholders = implode(', ', array_fill(0, count($ids), '?')); + $query = "DELETE FROM AccountRequests WHERE id IN ($queryPlaceholders)"; + + $pdo = DB::factory('database'); + $pdo->execute($query, $ids); + + return $this->json(['success' => true]); + } + + +} diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php new file mode 100644 index 0000000000..1cb360c5c3 --- /dev/null +++ b/src/Controller/AdminController.php @@ -0,0 +1,78 @@ + '.*'], methods: ['POST'])] + public function resetUserTourViewed(Request $request): Response + { + $this->authorize($request, ['mgr']); + + $viewedTour = $this->getIntParam($request, 'viewedTour', true); + $selectedUser = XDUser::getUserByID( + $this->getIntParam($request, 'uid', true) + ); + + if (!isset($selectedUser)) { + throw new BadRequestHttpException('User not found'); + } + + if (!in_array($viewedTour, [0, 1])) { + throw new BadRequestHttpException('Invalid data parameter'); + } + + $storage = new \UserStorage($selectedUser, 'viewed_user_tour'); + $upserted = $storage->upsert(0, ['viewedTour' => $viewedTour]); + + if (!isset($upserted)) { + $this->logger->error( + sprintf( + 'reset_user_tour_viewed failed for %s (%s)', + $selectedUser->getUsername(), + $selectedUser->getUserID() + ) + ); + + return $this->json([ + [ + 'success' => false, + 'total' => 0, + 'message' => 'An error has occurred while updating this user, please contact support.' + ] + ]); + } + + return $this->json( + [ + 'success' => true, + 'total' => 1, + 'message' => 'This user will be now be prompted to view the New User Tour the next time they visit XDMoD' + ] + ); + } + +} diff --git a/src/Controller/AuthenticationController.php b/src/Controller/AuthenticationController.php new file mode 100644 index 0000000000..782b0551f1 --- /dev/null +++ b/src/Controller/AuthenticationController.php @@ -0,0 +1,144 @@ + '.*'], methods: ['POST'])] + #[Route('/login', name: 'xdmod_new_login', methods: ['POST'])] + public function login(): NotFoundHttpException + { + throw new NotFoundHttpException(); + } + + /** + * This route is responsible for any logic that may need to be executed when a user is logged out. Currently, the + * actual heavy lifting of logging out is done by the configuration in `config/packages/security.yaml`. This function + * just ensures that we clean up our custom Session. + * + * @param Request $request + * @return Response + */ + #[Route('/rest/logout', name: 'xdmod_logout', methods: ['POST', 'GET'])] + #[Route('/logout', name: 'xdmod_new_logout', methods: ['POST'])] + #[Route('/rest/auth/logout', name: 'xdmod_rest_auth_logout', methods: ['POST'])] + public function logout(Request $request): Response + { + $token = $request->getSession()->get('xdmod_token'); + \XDSessionManager::logoutUser($token); + $request->getSession()->invalidate(); + + $response = $this->redirectToRoute('xdmod_home'); + $response->headers->removeCookie('xdmod_token'); + return $response; + } + + /** + * Return an IDP redirect URL for SSO login + * + * @param Request $request + * + * @return Response + */ + #[Route('{prefix}auth/idpredirect', name: 'idp_redirect', requirements: ['prefix' => '.*'], methods: ['GET'])] + public function idpRedirect(Request $request): Response + { + $returnTo = $this->getStringParam($request, 'returnTo', true); + + $request->getSession()->set('_security.main.target_path', $returnTo); + + $auth = new \Authentication\SAML\XDSamlAuthentication(); + $redirectUrl = $auth->getLoginURL($returnTo); + if ($redirectUrl === false ) { + return $this->json(buildError(new \Exception('SSO not configured.'))); + } + + return new Response($redirectUrl, Response::HTTP_OK, ['Content-Type' => 'text/plain']); + } + + + /** + * If a JupyterHub is configured, redirect to it with a new JSON Web Token in a cookie. + * + * @param Request $request + * @return RedirectResponse to the configured JupyterHub root if the user is + * authenticated, otherwise to the sign-in + * screen. + * @throws Exception if a JupyterHub is not configured. + */ + #[Route('{prefix}jwt-redirect', requirements: ['prefix' => '.*'], methods: ['GET'])] + public function redirectWithJwt(Request $request): Response + { + try { + $jupyterhub_url = $this->parameters->get('xdmod.portal_settings.jupyterhub.url'); + } catch (Exception $e) { + throw new HttpException(501, 'JupyterHub not configured.'); + } + try { + $user = $this->authorize($request); + } catch (UnauthorizedHttpException $e) { + return new RedirectResponse('/#jwt-redirect'); + } + list($jwt, $expiration) = JsonWebToken::encode($user->getUsername()); + $cookie = new Cookie( + 'xdmod_jwt', + $jwt, + $expiration, + '/', // path + null, // domain + true, // secure + true // httpOnly + ); + $response = new RedirectResponse($jupyterhub_url); + $response->headers->setCookie($cookie); + return $response; + } +} + diff --git a/src/Controller/BaseController.php b/src/Controller/BaseController.php new file mode 100644 index 0000000000..111be3660d --- /dev/null +++ b/src/Controller/BaseController.php @@ -0,0 +1,800 @@ +logger = $logger; + $this->twig = $twig; + $this->tokenHelper = $tokenHelper; + $this->parameters = $parameters; + } + + + /** + * Will attempt to authorize the provided users' roles against the provided array of role requirements. + * + * If the user is not authorized, an exception will be thrown. Otherwise, the function will simply return the + * authorized user. + * + * @param Request $request the current HTTP request object. + * @param array $requiredAcls either an array of Acl objects or their equivalent string representations that are + * required for access to a given feature. + * @param bool $anyAcl default false. If true then the requesting user will be considered authorized if there + * is any overlap in the requirements and the users currently assigned acls. If false, + * the requesting user will only be considered authorized if they have *all* of the + * specified $requiredAcls. + * + * @return XDUser the currently logged in, authorized user. + * + * @throws UnauthorizedHttpException if no requirements are provided and there is no currently logged in user or if + * requirements are provided but not met by the public user. + * @throws AccessDeniedHttpException if the currently logged in user is unable to fulfill the provided requirements. + * @throws Exception if any of the values supplied within $requirements are not valid Acls objects or string + * representations of Acl objects. + */ + public function authorize(Request $request, array $requiredAcls = [], bool $anyAcl = false): XDUser + { + + $user = $this->getXDUser($request->getSession()); + + // If role requirements were not given, then the only check to perform + // is that the user is not a public user. + $isPublicUser = $user->isPublicUser(); + if (empty($requiredAcls) && $isPublicUser) { + throw new UnauthorizedHttpException('xdmod', self::EXCEPTION_MESSAGE); + } + + if ($anyAcl) { + $authorized = count(array_intersect($user->getAclNames(), $requiredAcls)) > 0; + } else { + $authorized = $user->hasAcls($requiredAcls); + } + + if (!$authorized && !$isPublicUser) { + throw new AccessDeniedHttpException(self::EXCEPTION_MESSAGE); + } elseif (!$authorized && $isPublicUser) { + throw new UnauthorizedHttpException('xdmod', self::EXCEPTION_MESSAGE); + } + + // Return the successfully-authorized user. + return $user; + } + + /** + * Retrieve the XDMoD user from a request object. + * + * @param Request $request The request to retrieve a user from. + * @return XDUser The user who made the request. + */ + protected function getUserFromRequest(Request $request) + { + return $request->attributes->get(BaseController::USER_ATTRIBUTE_KEY); + } + + /** + * @param SessionInterface $session + * @return XDUser + * @throws Exception + */ + protected function getXDUser(SessionInterface $session): XDUser + { + $symfonyUser = $this->getUser(); + if (!isset($symfonyUser)) { + if ($session->has('xdUser')) { + $xdUser = XDUser::getUserByID($session->get('xdUser')); + } elseif ($session->has('xdmod_token')) { + $xdUser = XDUser::getUserByToken($session->get('xdmod_token')); + } else { + if (!$session->has('public_session_token')) { + $session->set('public_session_token', 'public-' . microtime(true) . '-' . uniqid()); + } + $xdUser = XDUser::getPublicUser(); + } + } else { + $xdUser = XDUser::getUserByUserName($symfonyUser->getUserIdentifier()); + } + + if (!$xdUser->isPublicUser()) { + $session->set('xdUser', $xdUser->getUserID()); + } + return $xdUser; + } + + /** + * @param Request $request + * @param string[] $failover_methods + * @return XDUser + * @throws \SessionExpiredException + */ + protected function detectUser(Request $request, array $failover_methods = []): XDUser + { + $session = $request->getSession(); + try { + $user = $this->getLoggedInUser($session); + } catch (Exception $e) { + if (count($failover_methods) == 0) { + // Previously: Exception with 'Session Expired', No Logged In User code + throw new \SessionExpiredException(); + } + + $isPublicUser = $this->getBooleanParam($request, 'public_user'); + switch ($failover_methods[0]) { + case XDUser::PUBLIC_USER: + if ($isPublicUser || $session->has('public_session_token')) { + return XDUser::getPublicUser(); + } else { + // Previously: Exception with 'Session Expired', No Public User code + throw new \SessionExpiredException($e->getMessage()); + } + break; + case XDUser::INTERNAL_USER: + try { + return $this->getInternalUser($request); + } catch (Exception $e) { + if ( + isset($failover_methods[1]) + && $failover_methods[1] == XDUser::PUBLIC_USER + ) { + if ($isPublicUser || $session->has('public_session_token')) { + return XDUser::getPublicUser(); + } else { + // Previously: Exception with 'Session Expired', No Public User code + throw new \SessionExpiredException(); + } + } else { + // Previously: Exception with 'Session Expired', No Internal User code + throw new \SessionExpiredException(); + } + } + default: + // Previously: Exception with 'Session Expired', No Logged In User code + throw new \SessionExpiredException(); + } + } + + return $user; + } + + /** + * Ported from libraries/security.php::getLoggedInUser, modified to use Symfony Session as opposed to the + * SessionSingleton. + * + * @param Session $session + * + * @return XDUser + * + * @throws Exception if no 'xdUser' session parameter exists. + * @throws Exception if unable to find a record in moddb.Users for the id present in the 'xdUser' session parameter. + */ + protected function getLoggedInUser(Session $session): XDUser + { + // This is where the + $sessionUserId = $session->get('xdUser'); + if (empty($sessionUserId)) { + throw new Exception('Session Expired', 2); + } + $user = XDUser::getUserByID($sessionUserId); + + if ($user == NULL) { + throw new Exception('User does not exist'); + } + + return $user; + } + + + /** + * @param Request $request + * @return XDUser + * @throws Exception if there is no record in moddb.Users for the value of the user_id request param. + * @throws Exception if there is no user_id request param. + */ + protected function getInternalUser(Request $request): XDUser + { + $userId = $request->get('user_id'); + + if ( + $request->server->has('REMOTE_ADDR') + && $request->server->get('REMOTE_ADDR') == '127.0.0.1' + && isset($userId) + ) { + $user = XDUser::getUserByID($userId); + + if ($user == NULL) { + throw new Exception('Internal user does not exist'); + } + } else { + throw new Exception('Internal user not specified'); + } + + return $user; + } + + + /** + * Attempt to get a parameter value from a request and filter it. + * + * @param Request $request The request to extract the parameter from. + * @param string $name The name of the parameter. + * @param bool $mandatory If true, an exception will be thrown if + * the parameter is missing from the request. + * @param mixed $default The value to return if the parameter was not + * specified and the parameter is not mandatory. + * @param int $filterId The ID of the filter to use. See filter_var. + * @param mixed $filterOptions The options to use with the filter. + * The filter should be configured so that + * it returns null if conversion is not + * successful. See filter_var. + * @param string $expectedValueType The expected type for the value. + * This is used purely for errors thrown + * when the parameter value is invalid. + * @return mixed If available and valid, the parameter value. + * Otherwise, if it is missing and not mandatory, + * the given default. + * + * @throws BadRequestHttpException If the parameter was not available + * and the parameter was deemed mandatory, + * or if the parameter value is not valid + * according to the given filter. + */ + private function getParam( + Request $request, + string $name, + bool $mandatory, + $default, + int $filterId, + $filterOptions, + string $expectedValueType, + bool $compressWhitespace = true + ) + { + // If the parameter was not present, throw an exception if it was + // mandatory and return the default if it was not. + // Attempt to extract the parameter value from the request. + $value = $request->get($name); + $originalValueType = get_debug_type($value); + + if ($value === null) { + if ($mandatory) { + throw new BadRequestHttpException("$name is a required parameter."); + } else { + return $default; + } + } + + + // This is to accommodate the functionality from \xd_security\assertParameterSet that wasn't already provided + // by this function. + if ($expectedValueType === 'string' && $compressWhitespace) { + $value = preg_replace('/\s+/', ' ', $value); + } + + // Run the found parameter value through the given filter. + $value = filter_var($value, $filterId, $filterOptions); + $valueType = get_debug_type($value); + + if ($value === null || + ($originalValueType === 'array' && $value === false) || + ($expectedValueType === 'string' && $valueType !== 'string' && $value !== false) || + ($expectedValueType === 'Unix timestamp' && $valueType !== 'DateTime' && $value !== false) || + ($expectedValueType === 'ISO 8601 Date' && $valueType !== 'DateTime' && $value !== false) || + ($expectedValueType === 'integer' && $valueType !== 'int' && $value !== false) || + ($expectedValueType === 'float' && $valueType !== 'float' && $value !== false) + ) { + throw new BadRequestHttpException("Invalid value for $name. Must be a(n) $expectedValueType."); + } + + // If the value is invalid, throw an exception. + if ($value === false && $expectedValueType !== 'boolean' && $originalValueType !== 'bool') { + // This happens when filtering a value doesn't match a regexp. + throw new BadRequestHttpException("Invalid $name"); + } + + // Return the filtered value. + return $value; + } + + /** + * Attempt to get an integer parameter value from a request. + * + * @param Request $request The request to extract the parameter from. + * @param string $name The name of the parameter. + * @param bool $mandatory (Optional) If true, an exception will be + * thrown if the parameter is missing from the + * request. (Defaults to false.) + * @param mixed $default (Optional) The value to return if the + * parameter was not specified and the parameter + * is not mandatory. (Defaults to null.) + * @return mixed If available and valid, the parameter value + * as an integer. Otherwise, if it is missing + * and not mandatory, the given default. + * + * @throws BadRequestHttpException If the parameter was not available + * and the parameter was deemed mandatory, + * or if the parameter value could not be + * converted to an integer. + */ + protected function getIntParam( + Request $request, + string $name, + bool $mandatory = false, + $default = null + ) + { + return $this->getParam( + $request, + $name, + $mandatory, + $default, + FILTER_VALIDATE_INT, + [ + 'options' => [ + 'default' => null, + ], + ], + 'integer' + ); + } + + /** + * Attempt to get a float parameter value from a request. + * + * @param Request $request The request to extract the parameter from. + * @param string $name The name of the parameter. + * @param bool $mandatory (Optional) If true, an exception will be + * thrown if the parameter is missing from the + * request. (Defaults to false.) + * @param mixed $default (Optional) The value to return if the + * parameter was not specified and the parameter + * is not mandatory. (Defaults to null.) + * @return mixed If available and valid, the parameter value + * as a float. Otherwise, if it is missing + * and not mandatory, the given default. + * + * @throws BadRequestHttpException If the parameter was not available + * and the parameter was deemed mandatory, + * or if the parameter value could not be + * converted to a float. + */ + protected function getFloatParam( + Request $request, + string $name, + bool $mandatory = false, + $default = null + ) + { + return $this->getParam( + $request, + $name, + $mandatory, + $default, + FILTER_VALIDATE_FLOAT, + [ + 'options' => [ + 'default' => null, + ], + ], + 'float' + ); + } + + /** + * Attempt to get a string parameter value from a request. + * + * @param Request $request The request to extract the parameter from. + * @param string $name The name of the parameter. + * @param bool $mandatory (Optional) If true, an exception will be + * thrown if the parameter is missing from the + * request. (Defaults to false.) + * @param mixed $default (Optional) The value to return if the + * parameter was not specified and the parameter + * is not mandatory. (Defaults to null.) + * @return mixed If available and valid, the parameter value + * as a string. Otherwise, if it is missing + * and not mandatory, the given default. + * + * @throws BadRequestHttpException If the parameter was not available + * and the parameter was deemed mandatory. + */ + protected function getStringParam( + Request $request, + string $name, + bool $mandatory = false, + $default = null, + string $pattern = null, + bool $compressWhitespace = true + ) + { + if (!isset($pattern)) { + return $this->getParam( + $request, + $name, + $mandatory, + $default, + FILTER_DEFAULT, + [], + 'string', + $compressWhitespace + ); + } else { + return $this->getParam( + $request, + $name, + $mandatory, + $default, + FILTER_VALIDATE_REGEXP, + ['options' => ['regexp' => $pattern]], + 'string', + $compressWhitespace + ); + } + } + + protected function getEmailParam(Request $request, string $name, bool $mandatory = false, $default = null) + { + return $this->getParam( + $request, + $name, + $mandatory, + $default, + FILTER_CALLBACK, + ['options' => function ($value) { + $validator = new EmailValidator(); + if ($validator->isValid($value, new RFCValidation())) { + return $value; + } + return null; + }], + 'email', + false + ); + } + + /** + * Attempt to get a boolean parameter value from a request. + * + * @param Request $request The request to extract the parameter from. + * @param string $name The name of the parameter. + * @param bool $mandatory (Optional) If true, an exception will be + * thrown if the parameter is missing from the + * request. (Defaults to false.) + * @param mixed $default (Optional) The value to return if the + * parameter was not specified and the parameter + * is not mandatory. (Defaults to null.) + * @return mixed If available and valid, the parameter value + * as a boolean. Otherwise, if it is missing + * and not mandatory, the given default. + * + * @throws BadRequestHttpException If the parameter was not available + * and the parameter was deemed mandatory, + * or if the parameter value could not be + * converted to a boolean. + */ + protected function getBooleanParam( + Request $request, + string $name, + bool $mandatory = false, + $default = null + ) + { + return $this->getParam( + $request, + $name, + $mandatory, + $default, + FILTER_CALLBACK, + [ + 'options' => function ($value) { + // Run the found parameter value through a boolean filter. + $filteredValue = filter_var( + $value, + FILTER_VALIDATE_BOOLEAN, + [ + 'flags' => FILTER_NULL_ON_FAILURE, + ] + ); + + // If the filter converted the string, return the boolean. + if ($filteredValue !== null) { + return $filteredValue; + } + + // Check the value against 'y' for true and 'n' for false. + $lowercaseValue = strtolower($value); + if ($lowercaseValue === 'y') { + return true; + } + if ($lowercaseValue === 'n') { + return false; + } + + // Return null if all conversion attempts failed. + return null; + }, + ], + 'boolean' + ); + } + + /** + * Attempt to get a date parameter value from a request where it is + * submitted as a Unix timestamp. + * + * @param Request $request The request to extract the parameter from. + * @param string $name The name of the parameter. + * @param bool $mandatory (Optional) If true, an exception will be + * thrown if the parameter is missing from the + * request. (Defaults to false.) + * @param mixed $default (Optional) The value to return if the + * parameter was not specified and the parameter + * is not mandatory. (Defaults to null.) + * @return mixed If available and valid, the parameter value + * as a DateTime. Otherwise, if it is missing + * and not mandatory, the given default. + * + * @throws BadRequestHttpException If the parameter was not available + * and the parameter was deemed mandatory, + * or if the parameter value could not be + * converted to a DateTime. + */ + protected function getDateTimeFromUnixParam( + Request $request, + string $name, + bool $mandatory = false, + $default = null + ) + { + return $this->getParam( + $request, + $name, + $mandatory, + $default, + FILTER_CALLBACK, + [ + 'options' => function ($value) { + $value_dt = \DateTime::createFromFormat('U', $value); + if ($value_dt === false) { + return null; + } + return $value_dt; + }, + ], + 'Unix timestamp' + ); + } + + /** + * Attempt to get a date parameter value from a request where it is + * submitted as a ISO 8601 (YYYY-MM-DD) date. + * + * @param Request $request The request to extract the parameter from. + * @param string $name The name of the parameter. + * @param bool $mandatory (Optional) If true, an exception will be + * thrown if the parameter is missing from the + * request. (Defaults to false.) + * @param mixed $default (Optional) The value to return if the + * parameter was not specified and the parameter + * is not mandatory. (Defaults to null.) + * @return mixed If available and valid, the parameter value + * as a DateTime. Otherwise, if it is missing + * and not mandatory, the given default. + * + * @throws BadRequestHttpException If the parameter was not available + * and the parameter was deemed mandatory, + * or if the parameter value could not be + * converted to a DateTime. + */ + protected function getDateFromISO8601Param( + Request $request, + string $name, + bool $mandatory = false, + $default = null + ) + { + return $this->getParam( + $request, + $name, + $mandatory, + $default, + FILTER_CALLBACK, + [ + 'options' => function ($value) { + return self::filterDate($value); + }, + ], + 'ISO 8601 Date' + ); + } + + /** + * @param Request $request + * @return void + */ + protected function verifyCaptcha(Request $request) + { + $captchaSiteKey = $this->getParameter('xdmod.portal_settings.mailer.captcha_public_key'); + $captchaSecret = $this->getParameter('xdmod.portal_settings.mailer.captcha_private_key'); + + $user = $this->getUserFromRequest($request); + + if ('' !== $captchaSiteKey && '' !== $captchaSecret && !isset($user)) { + $gCaptchaResponse = $request->get('g-recaptcha-response'); + if (!isset($gCaptchaResponse)) { + throw new BadRequestHttpException('Recaptcha information not specified'); + } + $recaptcha = new \ReCaptcha\ReCaptcha($captchaSecret); + $resp = $recaptcha->verify($gCaptchaResponse, $_SERVER['REMOTE_ADDR']); + if (!$resp->isSuccess()) { + $errors = $resp->getErrorCodes(); + throw new BadRequestHttpException(sprintf('You must enter the words in the Recaptcha box properly. %s', print_r($errors, true))); + } + } + } + + /** + * @param string $section + * @param string $key + * @param $default + * @return string|null + */ + protected function getConfigValue(string $section, string $key, $default = null): ?string + { + try { + $result = \xd_utilities\getConfiguration($section, $key); + } catch (\Exception $e) { + $result = $default; + } + return $result; + } + + protected function getFeatures() + { + $features = \xd_utilities\getConfigurationSection('features'); + + // Convert array values to boolean + array_walk($features, function (&$v) { + $v = ($v == 'on'); + }); + return $features; + } + + /** + * @param Request $request + * @return \XDUser + * @throws BadRequestHttpException if the provided token is empty, or there is not a provided token. + * @throws \Exception if the user's token from the db does not validate against the provided token. + */ + protected function authenticateToken($request) + { + // NOTE: While we prefer token's to be pulled from the 'Authorization' header, we also support a fallback lookup + // to the request's query params. + $authorizationHeader = $request->headers->get('Authorization'); + if (empty($authorizationHeader) || strpos($authorizationHeader, Tokens::HEADER_KEY) === false) { + $rawToken = $request->get(Tokens::HEADER_KEY); + } else { + $rawToken = substr($authorizationHeader, strpos($authorizationHeader, Tokens::HEADER_KEY) + strlen(Tokens::HEADER_KEY) + 1); + } + if (empty($rawToken)) { + throw new UnauthorizedHttpException( + Tokens::HEADER_KEY, + 'No token provided.', + null, + 0 + ); + } + + + // We expect the token to be in the form /^(\d+).(.*)$/ so just make sure it at least has the required delimiter. + $delimPosition = strpos($rawToken, Tokens::DELIMITER); + if ($delimPosition === false) { + throw new UnauthorizedHttpException( + Tokens::HEADER_KEY, + 'Invalid token.' + ); + } + + $userId = substr($rawToken, 0, $delimPosition); + $token = substr($rawToken, $delimPosition + 1); + + return $this->tokenHelper->authenticate($userId, $token); + } + + /** + * Attempts to convert the provided $value into an instance of DateTime by using the provided $format. If $value is + * unable to be converted into a valid DateTime or if warnings are generated during the process it will be filtered + * and null returned. + * + * @param string $value the date to be validated against the provided $format. Ex: 2027-08-15 + * @param string $format the format to be used when converting the string $value to an instance of DateTime + * + * @return DateTime|null If the creation of a DateTime was successful without warning then an instance of DateTime + * will be returned, else null; + */ + private static function filterDate(string $value, string $format = 'Y-m-d'): ?DateTime + { + $dateTime = DateTime::createFromFormat($format, $value); + + $lastErrors = DateTime::getLastErrors(); + + /* For PHP versions less than 8.2.0 $lastErrors will always be an array w/ the properties: + * warning_count, warnings, error_count, and errors. For versions >= 8.2.0, it will return false if + * there are no errors else it will return as it did pre-8.2.0. + * + * The below `if` statement takes this into account by ensuring that we specifically check for when + * $value_dt is not false ( i.e. is a DateTime object ) but we do have 1 or more warnings which + * indicates that the value of $value_dt is most likely not what it's expected to be. + * + * Example: parsing the date `2024-01-99` results in a $value_dt of: + * DateTime('2024-04-08') + * and a $lastError of: + * [ + * 'warning_count' => 1, + * 'warnings' => [ + * 10 => 'The parsed date was invalid' + * ], + * 'error_count' => 0, + * 'errors' => [] + * ] + */ + if ($dateTime === false || (is_array($lastErrors) && $lastErrors['warning_count'] > 0)) { + return null; + } + return $dateTime; + } +} diff --git a/src/Controller/ChartPoolController.php b/src/Controller/ChartPoolController.php new file mode 100644 index 0000000000..2b6bbc53a7 --- /dev/null +++ b/src/Controller/ChartPoolController.php @@ -0,0 +1,115 @@ +authorize($request); + } catch (Exception $e) { + return $this->json(buildError(new \SessionExpiredException()), 401); + } + + $operation = $this->getStringParam($request, 'operation'); + if (empty($operation)) { + return $this->json(buildError('operation_not_defined')); + } + + try { + switch ($operation) { + case 'add_to_queue': + return $this->addToQueue($request, $user); + case 'remove_from_queue': + return $this->removeFromQueue($request, $user); + } + } catch(\Exception $e) { + return $this->json(buildError($e)); + } + + return $this->json(buildError('invalid_operation_specified')); + } + + /** + * @param Request $request + * @param XDUser $user + * @return Response + * @throws Exception + */ + private function addToQueue(Request $request, XDUser $user): Response + { + $chartTitle = $this->getStringParam($request, 'chart_title', false, 'Untitled Chart'); + $chartId = $this->getStringParam($request, 'chart_id'); + + /* this is freaking ugly, but it's here so that we can maintain the same expected test output. */ + if (is_null($chartId)) { + return $this->json(buildError("A chart identifier must be specified")); + } elseif ($chartId === '') { + return $this->json(buildError("Invalid value specified for 'chart_id'.")); + } elseif (empty($chartId)){ + return $this->json(buildError("A chart identifier must be specified")); + } + $chartDrillDetails = $this->getStringParam($request, 'chart_drill_details'); + $chartDateDesc = $this->getStringParam($request, 'chart_date_desc'); + + $chart_pool = new XDChartPool($user); + + try { + $chart_pool->addChartToQueue( + $chartId, + $chartTitle, + $chartDrillDetails, + $chartDateDesc + ); + } catch (Exception $e) { + return $this->json(buildError($e->getMessage())); + } + + return $this->json([ + 'success' => true, + 'action' => 'add' + ]); + } + + /** + * @param Request $request + * @param XDUser $user + * @return Response + * @throws Exception + */ + private function removeFromQueue(Request $request, XDUser $user): Response + { + $chart_pool = new XDChartPool($user); + + $chartTitle = $this->getStringParam($request, 'chart_title', false, 'Untitled Chart'); + $chartId = str_replace('title=' . $chartTitle, 'title=' . urlencode($chartTitle), $this->getStringParam($request, 'chart_id', true)); + + $chart_pool->removeChartFromQueue($chartId); + return $this->json([ + 'success' => true, + 'action' => 'remove' + ]); + } + +} diff --git a/classes/Rest/Controllers/DashboardControllerProvider.php b/src/Controller/DashboardController.php similarity index 66% rename from classes/Rest/Controllers/DashboardControllerProvider.php rename to src/Controller/DashboardController.php index ffef4bda29..c93409c5cc 100644 --- a/classes/Rest/Controllers/DashboardControllerProvider.php +++ b/src/Controller/DashboardController.php @@ -1,49 +1,36 @@ '.*'])] +class DashboardController extends BaseController { - /** - * @see BaseControllerProvider::setupRoutes - */ - public function setupRoutes(Application $app, ControllerCollection $controller) - { - $root = $this->prefix; - $class = get_class($this); - $controller->get("$root/components", "$class::getComponents"); - - $controller->post("$root/layout", "$class::setLayout"); - $controller->delete("$root/layout", "$class::resetLayout"); - - $controller->get("$root/rolereport", "$class::getRoleReport"); - $controller->get("$root/savedchartsreports", "$class::getSavedChartsReports"); - - $controller->post("$root/viewedUserTour", "$class::setViewedUserTour"); - $controller->get("$root/viewedUserTour", "$class::getViewedUserTour"); - - $controller->get("$root/statistics", "$class::getStatistics"); - - } - - /* + /** * Get the column layout manager for the user * - * @return \CCR\ColumnLayout + * @param XDUser $user + * @return ColumnLayout */ - private function getLayout($user) + private function getLayout(XDUser $user): ColumnLayout { $defaultLayout = null; $defaultColumnCount = 2; @@ -57,10 +44,14 @@ private function getLayout($user) } } - return new \CCR\ColumnLayout($defaultColumnCount, $defaultLayout); + return new ColumnLayout($defaultColumnCount, $defaultLayout); } - private function getConfigVariables($user) + /** + * @param XDUser $user + * @return array + */ + private function getConfigVariables(XDUser $user): array { $person_id = $user->getPersonID(true); $obj_warehouse = new \XDWarehouse(); @@ -88,13 +79,19 @@ private function getConfigVariables($user) * - If a user chart has the same name as a chart in the role configuration * then its settings will be used in place of the role chart. */ - const TOP_COMPONENT = 't.'; - const CHART_COMPONENT = 'c.'; - const NON_CHART_COMPONENT = 'p.'; + private const TOP_COMPONENT = 't.'; + private const CHART_COMPONENT = 'c.'; + private const NON_CHART_COMPONENT = 'p.'; - public function getComponents(Request $request, Application $app) + /** + * @param Request $request + * @return Response + * @throws Exception if the user for this request does not have a user id. + */ + #[Route('/components', methods: ['GET'])] + public function getComponents(Request $request): Response { - $user = $this->getUserFromRequest($request); + $user = $this->getXDUser($request->getSession()); $dashboardComponents = array(); @@ -137,10 +134,10 @@ public function getComponents(Request $request, Application $app) } $dashboardComponents[$chartLocation] = array( - 'name' => $componentType . $component['name'], - 'type' => $component['type'], - 'config' => isset($component['config']) ? $component['config'] : array(), - 'column' => $column + 'name' => $componentType . $component['name'], + 'type' => $component['type'], + 'config' => isset($component['config']) ? $component['config'] : array(), + 'column' => $column ); } } @@ -181,7 +178,7 @@ public function getComponents(Request $request, Application $app) ksort($dashboardComponents); - return $app->json(array( + return $this->json(array( 'success' => true, 'total' => count($dashboardComponents), 'portalConfig' => array('columns' => $layout->getColumnCount()), @@ -190,10 +187,16 @@ public function getComponents(Request $request, Application $app) } /** - * set the layout metadata + * Set the layout metadata * + * @param Request $request + * @return Response + * @throws BadRequestHttpException if the data parameter is not present and does not contain a layout and columns + * property. + * @throws Exception if there is a problem authorizing the current user. */ - public function setLayout(Request $request, Application $app) + #[Route('/layout', methods: ['POST'])] + public function setLayout(Request $request): Response { $user = $this->authorize($request); @@ -205,7 +208,7 @@ public function setLayout(Request $request, Application $app) $storage = new \UserStorage($user, 'summary_layout'); - return $app->json(array( + return $this->json(array( 'success' => true, 'total' => 1, 'data' => $storage->upsert(0, $content) @@ -215,8 +218,12 @@ public function setLayout(Request $request, Application $app) /** * clear the layout metadata * + * @param Request $request + * @return Response + * @throws Exception if there is a problem authorizing the current user. */ - public function resetLayout(Request $request, Application $app) + #[Route('/layout', methods: ['DELETE'])] + public function resetLayout(Request $request): Response { $user = $this->authorize($request); @@ -224,16 +231,22 @@ public function resetLayout(Request $request, Application $app) $storage->del(); - return $app->json(array( + return $this->json(array( 'success' => true, 'total' => 1 )); } - /* - * Set value for if a user should view the help tour or not - */ - public function setViewedUserTour(Request $request, Application $app) + /** + * Set value for if a user should view the help tour or not + * + * @param Request $request + * @return Response + * @throws BadRequestHttpException + * @throws Exception + */ + #[Route('/viewedUserTour', methods: ['POST'])] + public function setViewedUserTour(Request $request): Response { $user = $this->authorize($request); $viewedTour = $this->getIntParam($request, 'viewedTour', true); @@ -244,17 +257,22 @@ public function setViewedUserTour(Request $request, Application $app) $storage = new \UserStorage($user, 'viewed_user_tour'); - return $app->json(array( + return $this->json(array( 'success' => true, 'total' => 1, 'msg' => $storage->upsert(0, ['viewedTour' => $viewedTour]) )); } - /** * Get charts based on role. - **/ - public function getRoleReport(Request $request, Application $app) + * + * @param Request $request + * @return Response + * @throws NotFoundHttpException + * @throws Exception if there is a problem authorizing the current user. + */ + #[Route('/rolereport', methods: ['GET'])] + public function getRoleReport(Request $request): Response { $user = $this->authorize($request); $role = $user->getMostPrivilegedRole()->getName(); @@ -269,13 +287,13 @@ public function getRoleReport(Request $request, Application $app) $userReport = $report; } } - if (is_null($userReport)){ - $availTemplates = $rm->enumerateReportTemplates(array($role), 'Dashboard Tab Report'); + if (is_null($userReport)) { + $availTemplates = $rm::enumerateReportTemplates([$role], 'Dashboard Tab Report'); if (empty($availTemplates)) { throw new NotFoundHttpException("No dashboard tab report template available for $role"); } - $template = $rm->retrieveReportTemplate($user, $availTemplates[0]['id']); + $template = $rm::retrieveReportTemplate($user, $availTemplates[0]['id']); $template->buildReportFromTemplate($_REQUEST, $report_id_suffix); $reports = $rm->fetchReportTable(); foreach ($reports as &$report) { @@ -286,10 +304,10 @@ public function getRoleReport(Request $request, Application $app) } $data = $rm->loadReportData($userReport['report_id']); $count = 0; - foreach($data['queue'] as $queue) { + foreach ($data['queue'] as $queue) { $chart_id = explode("&", $queue['chart_id']); $chart_id_parsed = array(); - foreach($chart_id as $value) { + foreach ($chart_id as $value) { list($key, $value) = explode("=", $value); $key = urldecode($key); $value = urldecode($value); @@ -305,30 +323,42 @@ public function getRoleReport(Request $request, Application $app) $data['queue'][$count]['chart_id'] = $chart_id_parsed; $count++; } - return $app->json(array( + return $this->json(array( 'success' => true, 'total' => count($data), 'data' => $data )); } } - /* - * Get stored value for if a user should view the help tour or not - */ - public function getViewedUserTour(Request $request, Application $app) + + /** + * Get stored value for if a user should view the help tour or not + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/viewedUserTour', methods: ['GET'])] + public function getViewedUserTour(Request $request): Response { $user = $this->authorize($request); $storage = new \UserStorage($user, 'viewed_user_tour'); - return $app->json(array( + return $this->json(array( 'success' => true, 'total' => 1, 'data' => $storage->get() )); } + /** * Get saved charts and reports. - **/ - public function getSavedChartsReports(Request $request, Application $app) + * + * @param Request $request + * @return Response + * @throws Exception if there is a problem authorizing the current user. + */ + #[Route('/savedchartsreports', methods: ['GET'])] + public function getSavedChartReports(Request $request): Response { $user = $this->authorize($request); if (isset($user)) { @@ -359,7 +389,7 @@ public function getSavedChartsReports(Request $request, Application $app) $tmp['config'] = $report['report_id']; $data[] = $tmp; } - return $app->json(array( + return $this->json(array( 'success' => true, 'total' => count($data), 'data' => $data @@ -367,17 +397,17 @@ public function getSavedChartsReports(Request $request, Application $app) } } - /* + /** * Retrieve summary statistics * * @param Request $request - * @param Application $app - * @return \Symfony\Component\HttpFoundation\JsonResponse + * @return Response * @throws Exception */ - public function getStatistics(Request $request, Application $app) + #[Route('/statistics', methods: ['GET'])] + public function getStatistics(Request $request): Response { - $user = $this->getUserFromRequest($request); + $user = $this->getXDUser($request->getSession()); $aggregationUnit = $request->get('aggregation_unit', 'auto'); @@ -417,14 +447,67 @@ public function getStatistics(Request $request, Application $app) $mostPrivileged = $user->getMostPrivilegedRole()->getName(); $formats = $rawRoles['roles'][$mostPrivileged]['statistics_formats']; - return $app->json( + return $this->json( array( 'totalCount' => 1, 'success' => true, 'message' => '', 'formats' => $formats, - 'data' => array($result) + 'data' => [$result] ) ); } + + + + /** + * Checks that the `$[start|end]Date` values are valid ( `Y-m-d` ) dates and that `$startDate` + * is before `$endDate`. + * + * @param string $startDate the beginning of the date range. + * @param string $endDate the end of the date range. + * @throws BadRequestHttpException if either start or end dates are not provided in the format + * `Y-m-d`, or if the start date is after the end date. + */ + protected function checkDateRange($startDate, $endDate) + { + $startTimestamp = $this->getTimestamp($startDate, 'start_date'); + $endTimestamp = $this->getTimestamp($endDate, 'end_date'); + + if ($startTimestamp > $endTimestamp) { + throw new BadRequestHttpException('Start Date must not be after End Date'); + } + } + + /** + * Attempt to convert the provided string $date value into an equivalent unix timestamp (int). + * + * @param string $date The value to be converted into a DateTime. + * @param string $paramName 'date', The name of the parameter to be included in the exception + * message if validation fails. + * @param string $format 'Y-m-d', The format that `$date` should be in. + * @return int created from the provided `$date` value. + * @throws BadRequestHttpException if the date is not in the form `Y-m-d`. + */ + protected function getTimestamp($date, $paramName = 'date', $format = 'Y-m-d') + { + $parsed = date_parse_from_format($format, $date); + + if ($parsed['year'] === false || $parsed['month'] === false || $parsed['day'] === false) { + throw new BadRequestHttpException("Unable to parse $paramName"); + } + $date = mktime( + $parsed['hour'] !== false ? $parsed['hour'] : 0, + $parsed['minute'] !== false ? $parsed['minute'] : 0, + $parsed['second'] !== false ? $parsed['second'] : 0, + $parsed['month'], + $parsed['day'], + $parsed['year'] + ); + if ($date === false || $parsed['error_count'] > 0) { + throw new BadRequestHttpException("Unable to parse $paramName"); + } + + return $date; + } } diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php new file mode 100644 index 0000000000..f2812ceb72 --- /dev/null +++ b/src/Controller/HomeController.php @@ -0,0 +1,372 @@ + [ + 'entityId', + 'singleSignOnService' => [ + 'url', + 'binding' + ], + 'singleLogoutService' => [ + 'url', + 'binding' + ] + ], + 'sp' => [ + 'entityId', + 'assertionConsumerService' => [ + 'url', + 'binding' + ], + 'singleLogoutService' => [ + 'url', + 'binding' + ] + ] + ]; + + /** + * This route serves XDMoD + * + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/', name: 'xdmod_home', methods: ['GET', 'OPTIONS'])] + public function index(Request $request): Response + { + + if ($request->getMethod() === 'OPTIONS') { + // We don't need to send anything back for a CORS pre-flight + return new Response(); + } + + $session = $request->getSession(); + $returnTo = $session->get('_security.main.target_path'); + if (!empty($returnTo)) { + $returnTo = urldecode($returnTo); + $url = $this->generateUrl('xdmod_home'); + $this->logger->warning('redirecting to', ["$returnTo"]); + $session->set('_security.main.target_path', null); + $response = new RedirectResponse("$returnTo"); + return $response; + } + + $user = $this->getXDUser($session); + $userLoggedIn = $session->has('xdUser') && !$user->isPublicUser(); + + $realms = array_reduce(Realms::getRealms(), function ($carry, Realm $item) { + $carry [] = $item->getName(); + return $carry; + }, []); + + $features = $this->getFeatures(); + + $isSSOConfigured = false; + $ssoLoginLink = []; + try { + $auth = new XDSamlAuthentication(); + $isSSOConfigured = $auth->isSamlConfigured(); + $ssoLoginLink = $auth->getLoginLink(); + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), [$e]); + } + + try { + $db = DB::factory('database'); + $personInfo = $db->query( + 'SELECT first_name, last_name FROM modw.person p WHERE p.id = :person_id', + [':person_id' => $user->getPersonID()] + ); + } catch (\Exception $e) { + $personInfo = [ + [ + 'first_name' => 'Unknown', + 'last_name' => 'Unknown' + ] + ]; + } + + // JupyterHub Config + $jupyterIsEnabled = false; + $jupyterHubURL = ''; + try { + $jupyterHubURL = $this->parameters->get('xdmod.portal_settings.jupyterhub.url'); + } catch (\Exception $e) { + + } + + try { + + $ssoShowLocalLogin = filter_var( + $this->parameters->get('xdmod.portal_settings.sso.show_local_login'), + FILTER_VALIDATE_BOOLEAN + ); + } catch (Exception $e) { + $ssoShowLocalLogin = false; + } + + try { + $ssoDirectLink = filter_var( + $this->parameters->get('xdmod.portal_settings.sso.direct_link'), + FILTER_VALIDATE_BOOLEAN + ); + } catch(Exception $e) { + $ssoDirectLink = false; + } + + + $params = [ + 'user_logged_in' => $userLoggedIn, + 'user' => $user, + 'person_name' => sprintf('%s, %s', $personInfo[0]['last_name'], $personInfo[0]['first_name']), + 'title' => $this->parameters->get('xdmod.portal_settings.general.title'), + 'keywords' => 'xdmod, xsede, analytics, metrics on demand, hpc, visualization, statistics, reporting, auditing, nsf, resources, resource providers', + 'description' => 'XSEDE Metrics on Demand (XDMoD) is a comprehensive auditing framework for XSEDE, the follow-on to NSF\'s TeraGrid program. XDMoD provides detailed information on resource utilization and performance across all resource providers.', + 'extjs_path' => 'gui/lib', + 'extjs_version' => 'extjs', + 'rest_token' => $user->getToken(), + 'colors' => json_encode(json_decode(file_get_contents(CONFIG_DIR . '/colors1.json'), true)), + 'rest_url' => sprintf( + '%s%s', + $this->parameters->get('xdmod.portal_settings.rest.base'), + $this->parameters->get('xdmod.portal_settings.rest.version') + ), + 'realms' => $realms, + 'tech_support_recipient' => $this->parameters->get('xdmod.portal_settings.general.tech_support_recipient'), + 'xdmod_portal_version' => \xd_versioning\getPortalVersion(), + 'xdmod_portal_version_short' => \xd_versioning\getPortalVersion(true), + 'disabled_menus' => json_encode(Acls::getDisabledMenus($user, $realms)), + 'ORGANIZATION_NAME' => 'organization_name', + 'ORGANIZATION_NAME_ABBREV' => 'organization_abbrev', + 'captcha_site_key' => $this->getCaptchaSiteKey($user), + 'xdmod_features' => json_encode($features), + 'timezone' => date_default_timezone_get(), + 'isCenterDirector' => $user->hasAcl('cd'), + 'is_logged_in' => !$user->isPublicUser(), + 'is_public_user' => $user->isPublicUser(), + 'user_dashboard' => isset($features['user_dashboard']) && filter_var($features['user_dashboard'], FILTER_VALIDATE_BOOLEAN), + 'all_user_roles' => json_encode($user->enumAllAvailableRoles()), + 'raw_data_realms' => json_encode($this->getRawDataRealms($user)), + 'use_center_logo' => false, + 'asset_paths' => Assets::generateAssetTags('portal'), + 'profile_editor_init_flag' => $this->getProfileEditorInitFlag($user), + 'no_script_message' => $this->getNoScriptMessage('XDMoD requires JavaScript, which is currently disabled in your browser.'), + 'org_name' => ORGANIZATION_NAME, + 'is_sso_configured' => $isSSOConfigured, + 'sso_login_link' => json_encode($ssoLoginLink), + 'sso_show_local_login' => $ssoShowLocalLogin, + 'sso_direct_link' => $ssoDirectLink, + 'is_jupyter_configured' => $jupyterIsEnabled, + 'jupyter_hub_url' => $jupyterHubURL, + 'error_codes' => \XDError::getErrorCodes() + ]; + + $logoData = $this->getLogoData(); + if ($logoData !== null) { + list($logoWidth, $imgData) = $logoData; + $params['use_center_logo'] = true; + $params['logo_width'] = $logoWidth; + $params['img_data'] = $imgData; + } + + return $this->render('twig/index.html.twig', $params); + } + + + /** + * @param $user + * @return array + */ + private function getRawDataRealms($user): array + { + return array_map( + function ($item) { + return $item['name']; + }, + \DataWarehouse\Access\RawData::getRawDataRealms($user) + ); + } + + public function getCaptchaSiteKey(XDUser $user) + { + $result = ''; + + if ($user->isPublicUser()) { + $captchaSiteKey = $this->parameters->get('xdmod.portal_settings.mailer.captcha_public_key'); + $captchaSecret = $this->parameters->get('xdmod.portal_settings.mailer.captcha_private_key'); + if ('' !== $captchaSiteKey && '' !== $captchaSecret) { + $result = $captchaSiteKey; + } + } + + return $result; + } + + + public function getLogoData() + { + try { + $logo = $this->parameters->get('xdmod.portal_settings.general.center_logo'); + $logo_width = $this->parameters->get('xdmod.portal_settings.general.center_logo_width'); + + $logo_width = intval($logo_width); + + if (strlen($logo) > 0 && $logo[0] !== '/') { + $logo = __DIR__ . '/' . $logo; + } + + if (file_exists($logo)) { + $img_data = base64_encode(file_get_contents($logo)); + return [ + $logo_width, + $img_data + ]; + } + } catch (Exception $e) { + } + + return null; + } + + private function getProfileEditorInitFlag(XDUser $user) + { + $profile_editor_init_flag = ''; + $usersFirstLogin = ($user->getCreationTimestamp() == $user->getUpdateTimestamp() && !$user->isPublicUser()); + + // If the user logging in is an XSEDE/Single Sign On user, they may or may not have + // an e-mail address set. The logic below assists in presenting the Profile Editor + // with the appropriate (initial) view + $userEmail = $user->getEmailAddress(); + $userEmailSpecified = ($userEmail != NO_EMAIL_ADDRESS_SET && !empty($userEmail)); + if ($user->isSSOUser() === true || $usersFirstLogin) { + + // NOTE: $_SESSION['suppress_profile_autoload'] will be set only upon update of the user's profile (see respective REST call) + $session = SessionSingleton::getSession(); + $suppressProfileAutoload = $session->get('suppress_profile_autoload'); + if ($usersFirstLogin && $userEmailSpecified && (!isset($suppressProfileAutoload) && $user->getUserType() != 50)) { + // If the user is logging in for the first time and does have an e-mail address set + // (due to it being specified in the XDcDB), welcome the user and inform them they + // have an opportunity to update their e-mail address. + + $profile_editor_init_flag = 'XDMoD.ProfileEditorConstants.WELCOME_EMAIL_CHANGE'; + + } elseif ($usersFirstLogin && !$userEmailSpecified) { + // If the user is logging in for the first time and does *not* have an e-mail address set, + // welcome the user and inform them that he/she needs to set an e-mail address. + + $profile_editor_init_flag = 'XDMoD.ProfileEditorConstants.WELCOME_EMAIL_NEEDED'; + + } + } + if (!$userEmailSpecified) { + // Regardless of whether the user is logging in for the first time or not, the lack of + // an e-mail address requires attention + $profile_editor_init_flag = 'XDMoD.ProfileEditorConstants.EMAIL_NEEDED'; + } + + return $profile_editor_init_flag; + } + + public function getNoScriptMessage($message, $exception_message = '', $include_structure_tags = false) + { + + if (!empty($exception_message)) { + $exception_message = '

(' . $exception_message . ')'; + } + + $message = '
' . + '
' . + '' . + '

' . + $message . + $exception_message . + '
'; + + if ($include_structure_tags) { + $message = '' . $message . ''; + } + + return $message; + } + + /** + * SSO is considered setup + * @return bool + */ + private function isSSOSetup(array $ssoSettings): bool + { + return $this->validate( + self::REQUIRED_SAML_SETTINGS, + $ssoSettings + ); + } + + /** + * Validates the provided $settings against the $required structure. This function only validates that + * keys are present and have non-empty values. + * + * @param array $required + * @param array $settings + * @return bool + */ + private function validate(array $required, array $settings): bool + { + foreach ($required as $key => $values) { + // We need to account for PHP's wonderful dual-index arrays, and since $settings is expected + // to be indexed by string we translate the $required indexes to their string counterpart here. + if (is_numeric($key) && is_string($values)) { + $key = $values; + } + + // the following logic goes something like: + // If: + // - The required key exists in $settings + // - AND The required key is a string + // - AND The value for the given key in $settings is non-empty + // - OR - + // If: + // - The required key exists in $settings + // - AND the $required values are an array ( aka, we must go deeper ) + // - AND and it's value in $settings is non-empty + // - AND the validation of the levels below this one are valid + // THEN continue the validation + // ELSE it's invalid + if (array_key_exists($key, $settings) && is_string($values) && !empty($settings[$key]) || + (array_key_exists($key, $settings) && is_array($values) && !empty($settings[$key]) && $this->validate($values, $settings[$key]))) { + continue; + } + return false; + } + // If we've gotten this far then the settings must be valid. + return true; + } +} + diff --git a/src/Controller/InternalDashboard/InternalDashboardController.php b/src/Controller/InternalDashboard/InternalDashboardController.php new file mode 100644 index 0000000000..8bc49ab760 --- /dev/null +++ b/src/Controller/InternalDashboard/InternalDashboardController.php @@ -0,0 +1,495 @@ +logout(false); + return $this->redirect('/internal_dashboard'); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/internal_dashboard')] + public function index(Request $request): Response + { + $user = $this->getXDUser($request->getSession()); + + $hasAppKernels = false; + $instanceId = null; + if (\xd_utilities\getConfiguration('features', 'appkernels') == 'on') { + $op = $request->get('op'); + if ($op === 'ak_instance') { + $hasAppKernels = true; + $instanceId = $request->get('instance_id'); + } + } + + $parameters = [ + 'user' => $user, + 'has_app_kernels' => $hasAppKernels, + 'ak_instance_id' => $instanceId, + 'extjs_path' => 'gui/lib', + 'extjs_version' => 'extjs', + 'rest_token' => $user->getToken(), + 'rest_url' => sprintf( + '%s%s', + \xd_utilities\getConfiguration('rest', 'base'), + \xd_utilities\getConfiguration('rest', 'version') + ), + 'xdmod_features' => json_encode($this->getFeatures()), + 'is_logged_in' => !$user->isPublicUser(), + 'is_public_user' => $user->isPublicUser(), + 'asset_paths' => Assets::generateAssetTags('internal_dashboard'), + 'error_codes' => \XDError::getErrorCodes() + ]; + + if ($user->isPublicUser()) { + return $this->render('twig/internal_dashboard_login.html.twig', $parameters); + } else { + if (!$user->hasAcl('mgr')) { + return $this->redirect($this->generateUrl('xdmod_home')); + } + return $this->render('twig/internal_dashboard.html.twig', $parameters); + } + } + + /** + * + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/controllers/dashboard.php', methods: ['POST'])] + public function dashboardIndex(Request $request): Response + { + $operation = $request->get('operation'); + if (empty($operation)) { + return $this->json(buildError('operation_not_defined')); + } + try { + switch ($operation) { + case 'get_menu': + return $this->getMenus($request); + } + } catch (\Exception $e) { + return $this->json(buildError($e)); + } + + return $this->json(buildError('invalid_operation_specified')); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/internal_dashboard/menus', methods: ['POST'])] + public function getMenus(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + + $config = \Configuration\XdmodConfiguration::assocArrayFactory( + 'internal_dashboard.json', + CONFIG_DIR + ); + + return $this->json([ + 'success' => true, + 'response' => $config['menu'], + 'count' => count($config['menu']) + ]); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/internal_dashboard/controllers/user.php', methods: ['POST'])] + public function userController(Request $request): Response + { + $operation = $request->get('operation'); + if (empty($operation)) { + return $this->json(buildError('operation_not_defined')); + } + try { + switch ($operation) { + case 'get_summary': + return $this->getUserSummary($request); + } + } catch(\Exception $e) { + return $this->json(buildError($e)); + } + + return $this->json(buildError('invalid_operation_specified')); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/internal_dashboard/users/summary')] + public function getUserSummary(Request $request): Response + { + $pdo = DB::factory('database'); + + $sql = 'SELECT COUNT(*) AS count FROM moddb.Users'; + list($userCountRow) = $pdo->query($sql); + + // TODO: Refactor these queries. + $sql = ' + SELECT COUNT(DISTINCT user_id) AS count + FROM moddb.SessionManager + WHERE DATEDIFF(NOW(), FROM_UNIXTIME(init_time)) < 7 + '; + list($last7DaysRow) = $pdo->query($sql); + + $sql = ' + SELECT COUNT(DISTINCT user_id) AS count + FROM moddb.SessionManager + WHERE DATEDIFF(NOW(), FROM_UNIXTIME(init_time)) < 30 + '; + list($last30DaysRow) = $pdo->query($sql); + + $returnData = [ + 'success' => true, + 'response' => [ + [ + 'user_count' => $userCountRow['count'], + 'logged_in_last_7_days' => $last7DaysRow['count'], + 'logged_in_last_30_days' => $last30DaysRow['count'], + ] + ], + 'count' => 1, + ]; + return $this->json($returnData); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route("/internal_dashboard/controllers/controller.php", name: "legacy_internal_dashboard_controllers", methods: ['POST', 'GET'])] + public function controllers(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + + $operation = $this->getStringParam($request, 'operation'); + if (empty($operation)) { + return $this->json(buildError('operation_not_defined')); + } + + switch ($operation) { + case 'enum_account_requests': + return $this->enumAccountRequests($request); + case 'update_request': + return $this->updateRequest($request); + case 'delete_request': + return $this->deleteRequest($request); + case 'enum_existing_users': + return $this->enumExistingUsers($request); + case 'enum_user_types_and_roles': + return $this->enumUserTypesAndRoles($request); + case 'enum_user_visits': + case 'enum_user_visits_export': + return $this->enumUserVisits($request, $operation); + case 'ak_rr': + return $this->akrr($request); + case 'logout': + return $this->redirectToRoute('xdmod_logout'); + } + return $this->json(buildError('invalid_operation_specified')); + } + + /** + * Code Ported from `html/internal_dashboard/controllers/controller.php` + * + * Enumerates the current Requests for an XDMoD Account. + * + * @param Request $request + * @return Response + * @throws Exception if unable to retrieve a connection to the database. + */ + private function enumAccountRequests(Request $request): Response + { + $md5Only = $this->getBooleanParam($request, 'md5only'); + + $pdo = DB::factory('database'); + $sql = <<query($sql); + + $data = [ + 'success' => true, + 'count' => count($results), + 'response' => $results + ]; + + if (isset($md5Only) && $md5Only) { + unset($data['count']); + unset($data['response']); + } + + return $this->json($data); + } + + /** + * Code Ported from `html/internal_dashboard/controllers/controller.php` + * + * @param Request $request + * @return Response + * @throws Exception + */ + private function updateRequest(Request $request): Response + { + $id = $this->getStringParam($request, 'id', true); + $comments = $this->getStringParam($request, 'comments', true); + + $pdo = DB::factory('database'); + + $data = ['success' => false, 'message' => 'invalid id specified']; + + $results = $pdo->query('SELECT id FROM AccountRequests WHERE id=:id', ['id' => $id]); + if (count($results) == 1) { + $pdo->execute('UPDATE AccountRequests SET comments=:comments WHERE id=:id', [ + 'comments' => $comments, + 'id' => $id + ]); + $data = ['success' => true]; + } + + return $this->json($data); + } + + /** + * Code Ported from `html/internal_dashboard/controllers/controller.php` + * + * @param Request $request + * @return Response + * @throws Exception + */ + private function deleteRequest(Request $request): Response + { + $idParam = $this->getStringParam($request, 'id', true, null, '/^\d+(,\d+)*$/'); + + $pdo = DB::factory('database'); + + $ids = array_map('intval', explode(',', $idParam)); + $idPlaceholders = implode(', ', array_fill(0, count($ids), '?')); + $pdo->execute("DELETE FROM AccountRequests WHERE id IN ($idPlaceholders)", $ids); + + return $this->json(['success' => true]); + } + + /** + * Code Ported from `html/internal_dashboard/controllers/controller.php` + * + * NOTE: there is a duplicate function UserAdminController::enumExistingUsers, this one can be removed when we are + * able to discontinue the old API layout. + * + * @param Request $request + * @return Response + * @throws Exception + */ + private function enumExistingUsers(Request $request): Response + { + $groupFilter = $this->getStringParam($request, 'group_filter'); + $roleFilter = $this->getStringParam($request, 'role_filter'); + $contextFilter = $this->getStringParam($request, 'context_filter', false, ''); + + $results = Users::getUsers($groupFilter, $roleFilter, $contextFilter); + $filtered = []; + foreach ($results as $user) { + if ($user['username'] !== 'Public User') { + $filtered[] = $user; + } + } + $data = [ + 'success' => true, + 'count' => count($filtered), + 'response' => $filtered + ]; + return new Response(json_encode($data)); + } + + /** + * Code Ported from `html/internal_dashboard/controllers/controller.php` + * + * @param Request $request + * @return Response + * @throws Exception + */ + private function enumUserTypesAndRoles(Request $request): Response + { + $data = ['success' => true]; + $pdo = DB::factory('database'); + + $query = 'SELECT id, type, color FROM moddb.UserTypes'; + $userTypes = $pdo->query($query); + $data['user_types'] = $userTypes; + + $query = "SELECT display AS description, acl_id AS role_id FROM moddb.acls WHERE name != 'pub' ORDER BY description"; + $userRoles = $pdo->query($query); + $data['user_roles'] = $userRoles; + $response = new Response(json_encode($data)); + $response->headers->set('Content-Type', 'text/html; charset=UTF-8'); + return $response; + } + + /** + * Code Ported from `html/internal_dashboard/controllers/controller.php` + * + * @param Request $request + * @param string $operation + * @return Response + * @throws Exception + */ + private function enumUserVisits(Request $request, string $operation): Response + { + $timeframe = strtolower($this->getStringParam($request, 'timeframe')); + $userTypes = explode(',', $this->getStringParam($request, 'user_types')); + $logger = $this->logger; + if (!in_array($timeframe, ['year', 'month'])) { + return new Response(json_encode([ + 'success' => false, + 'message' => 'invalid value specified for the timeframe' + ])); + } + + $data = [ + 'success' => true, + 'stats' => \XDStatistics::getUserVisitStats($timeframe, $userTypes) + ]; + + if ($operation === 'enum_user_visits_export') { + $response = new StreamedResponse(function () use ($data, $logger) { + $outputStream = fopen('php://output', 'wb'); + + $content = array_map( + function ($item) { + return implode(',', $item); + }, + $data['stats'] + ); + + // Add the header row. + array_unshift($content, implode(',', UserVisitController::$columns)); + + $written = fwrite( + $outputStream, + sprintf("%s\n", implode("\n", $content)) + ); + if ($written === false) { + $logger->error('Unable to write bytes to output stream'); + exit(1); + } + + $flushed = fflush($outputStream); + if ($flushed === false) { + $logger->error('Unable to flush output stream'); + exit(1); + } + + $closed = fclose($outputStream); + if ($closed === false) { + $logger->error('Unable to close output stream'); + exit(1); + } + }); + + $response->headers->set('Content-Type', 'application/xls'); + $response->headers->set( + 'Content-Disposition', + HeaderUtils::makeDisposition( + HeaderUtils::DISPOSITION_ATTACHMENT, + "xdmod_visitation_stats_by_$timeframe.csv" + ) + ); + + return $response; + } + + return new Response(json_encode($data)); + } + + /** + * Code Ported from `html/internal_dashboard/controllers/controller.php` + * + * TODO: Probable end up removing this function as it doesn't look like it's used. + * + * @param Request $request + * @return Response + */ + private function akrr(Request $request): Response + { + $data = ['success' => true]; + + $startDate = $this->getStringParam($request, 'start_date'); + $endDate = $this->getStringParam($request, 'end_date'); + + $testData = [['x' => [1, 2, 3], 'y' => [5, 2, 1]]]; + + $data['response'] = $testData; + $data['count'] = count($testData); + + return $this->json($data); + } + +} diff --git a/src/Controller/InternalDashboard/LogController.php b/src/Controller/InternalDashboard/LogController.php new file mode 100644 index 0000000000..3f474d0601 --- /dev/null +++ b/src/Controller/InternalDashboard/LogController.php @@ -0,0 +1,192 @@ +get('operation'); + if (empty($operation)) { + return $this->json(buildError('operation_not_defined')); + } + try { + switch ($operation) { + case 'get_levels': + return $this->getLevels($request); + case 'get_summary': + return $this->getSummary($request); + case 'get_messages': + return $this->getMessages($request); + } + } catch(\Exception $e) { + return $this->json(buildError($e)); + } + + return $this->json(buildError('invalid_operation_specified')); + } + + /** + * + * @param Request $request + * @return Response + */ + #[Route('{prefix}internal_dashboard/logs/levels', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function getLevels(Request $request): Response + { + $levels = [ + ['id' => \CCR\Log::EMERG, 'name' => 'Emergency'], + ['id' => \CCR\Log::ALERT, 'name' => 'Alert'], + ['id' => \CCR\Log::CRIT, 'name' => 'Critical'], + ['id' => \CCR\Log::ERR, 'name' => 'Error'], + ['id' => \CCR\Log::WARNING, 'name' => 'Warning'], + ['id' => \CCR\Log::NOTICE, 'name' => 'Notice'], + ['id' => \CCR\Log::INFO, 'name' => 'Info'], + ['id' => \CCR\Log::DEBUG, 'name' => 'Debug'], + ]; + + return $this->json([ + 'success' => true, + 'response' => $levels, + 'count' => count($levels) + ]); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}internal_dashboard/logs/messages', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function getMessages(Request $request): Response + { + $pdo = DB::factory('logger'); + + $sql = ' + SELECT id, logtime, ident, priority, message + FROM log_table + '; + + $clauses = array(); + $params = array(); + + $ident = $this->getStringParam($request, 'ident'); + + if (isset($ident)) { + $clauses[] = 'ident = ?'; + $params[] = $ident; + } + + $logLevels = $request->get( 'logLevels'); + if (isset($logLevels) && is_array($logLevels)) { + $clauses[] = sprintf( + 'priority IN (%s)', + implode(',', array_pad([], count($logLevels), '?')) + ); + $params = array_merge($params, $logLevels); + } + + $onlyMostRecent = $this->getBooleanParam($request, 'only_most_recent'); + if (isset($onlyMostRecent) && $onlyMostRecent) { + if (!isset($ident)) { + throw new Exception('"ident" required'); + } + + $summary = \Log\Summary::factory($ident); + + if (null !== ($startRowId = $summary->getProcessStartRowId())) { + $clauses[] = 'id >= ?'; + $params[] = $startRowId; + } + + if (null !== ($endRowId = $summary->getProcessEndRowId())) { + $clauses[] = 'id <= ?'; + $params[] = $endRowId; + } + } else { + $startDate = $this->getStringParam($request, 'start_date'); + if (isset($startDate)) { + $clauses[] = 'logtime >= ?'; + $params[] = $startDate . ' 00:00:00'; + } + + $endDate = $this->getStringParam($request, 'end_date'); + if (isset($endDate)) { + $clauses[] = 'logtime <= ?'; + $params[] = $endDate . ' 23:59:59'; + } + } + + if (count($clauses)) { + $sql .= ' WHERE ' . implode(' AND ', $clauses); + } + + $sql .= ' ORDER BY id DESC'; + + $start = $this->getIntParam($request, 'start'); + $limit = $this->getIntParam($request, 'limit'); + if (isset($start) && isset($limit)) { + $sql .= sprintf( + ' LIMIT %d, %d', + $start, + $limit + ); + } + + $returnData = [ + 'success' => true, + 'response' => $pdo->query($sql, $params), + ]; + + $sql = 'SELECT COUNT(*) AS count FROM log_table'; + + if (count($clauses)) { + $sql .= ' WHERE ' . implode(' AND ', $clauses); + } + + list($countRow) = $pdo->query($sql, $params); + + $returnData['count'] = $countRow['count']; + + return $this->json($returnData); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}internal_dashboard/logs/summary', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function getSummary(Request $request): Response + { + $ident = $this->getStringParam($request, 'ident', true); + $summary = \Log\Summary::factory($ident); + return $this->json([ + 'success' => true, + 'response' => [$summary->getData()], + 'count' => 1 + ]); + } +} diff --git a/src/Controller/InternalDashboard/MailerController.php b/src/Controller/InternalDashboard/MailerController.php new file mode 100644 index 0000000000..f57d4db1f6 --- /dev/null +++ b/src/Controller/InternalDashboard/MailerController.php @@ -0,0 +1,118 @@ +denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + + $operation = $this->getStringParam($request, 'operation'); + + if (empty($operation)) { + return $this->json(buildError('operation_not_defined')); + } + + switch ($operation) { + case 'enum_target_addresses': + return $this->enumTargetAddresses($request); + case 'send_plain_mail': + return $this->sendPlainMail($request); + } + return $this->json(buildError('invalid_operation_specified')); + } + + /** + * This is a straight port of `internal_dashboard/controllers/mailer.php` w/ enum_target_addresses operation. + * + * @param Request $request + * @return Response + * @throws Exception + */ + private function enumTargetAddresses(Request $request): Response + { + $groupFilter = $this->getStringParam($request, 'group_filter'); + if (is_null($groupFilter)) { + $groupFilter = $this->getIntParam($request, 'group_filter'); + if (is_null($groupFilter)) { + return $this->json(buildError("'group_filter' not specified.")); + } + } + + $aclFilter = $this->getStringParam($request, 'role_filter'); + if (is_null($aclFilter)) { + $aclFilter = $this->getIntParam($request, 'role_filter'); + if (is_null($aclFilter)) { + return $this->json(buildError("'role_filter' not specified.")); + } + } + + + list($query, $params) = \xd_dashboard\listUserEmailsByGroupAndAcl($groupFilter, $aclFilter); + + $db = DB::factory('database'); + $results = $db->query($query, $params); + + $addresses = array(); + + foreach ($results as $r) { + $addresses[] = $r['email_address']; + } + + sort($addresses); + + return $this->json([ + 'success' => true, + 'count' => count($addresses), + 'response' => $addresses + ]); + } + + /** + * This is just a straight port of `internal_dashboard/controllers/mailer.php` w/ operation send_plain_mail. + * + * @param Request $request + * @return Response + * @throws Exception + */ + private function sendPlainMail(Request $request): Response + { + $title = \xd_utilities\getConfiguration('general', 'title'); + $contactPageRecipient = \xd_utilities\getConfiguration('general', 'contact_page_recipient'); + + $message = $this->getStringParam($request, 'message', true, null, '/.*/', false); + $subject = $this->getStringParam($request, 'subject', true); + $targetAddresses = $this->getStringParam($request, 'target_addresses'); + + MailWrapper::sendMail([ + 'body' => $message, + 'subject' => "[$title] " . $subject, + 'toAddress' => $contactPageRecipient, + 'toName' => 'Undisclosed Recipients', + 'fromAddress' => $contactPageRecipient, + 'fromName' => $title, + 'bcc' => $targetAddresses + ]); + + return $this->json([ + 'success' => true + ]); + } +} diff --git a/src/Controller/InternalDashboard/SABUserController.php b/src/Controller/InternalDashboard/SABUserController.php new file mode 100644 index 0000000000..cb1bfd47e1 --- /dev/null +++ b/src/Controller/InternalDashboard/SABUserController.php @@ -0,0 +1,112 @@ +denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + + $operation = $this->getStringParam($request, 'operation', true); + switch ($operation) { + case 'enum_tg_users': + return $this->enumTgUsers($request); + case 'assign_assumed_person': + case 'get_mapping': + /* these operations are not currently used. */ + break; + } + return $this->json(['success' => false, 'message' => 'invalid operation specified']); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + private function enumTgUsers(Request $request): Response + { + $start = $this->getIntParam($request, 'start', true); + $limit = $this->getIntParam($request, 'limit'); + $searchMode = $this->getStringParam($request, 'search_mode', true, null, RESTRICTION_SEARCH_MODE); + $piOnly = $this->getStringParam($request, 'pi_only', true, null, RESTRICTION_YES_NO); + $usePiFilter = $piOnly === 'y'; + + $query = $this->getStringParam($request, 'query'); + $userManagement = $this->getStringParam($request, 'userManagement'); + + $user = $this->getXDUser($request->getSession()); + + $universityId = null; + if ($user->hasAcl(ROLE_ID_CAMPUS_CHAMPION) && !isset($userManagement)) { + $universityId = Acls::getDescriptorParamValue($user, ROLE_ID_CAMPUS_CHAMPION, 'provider'); + } + + $searchMethod = null; + if ($searchMode === 'formal_name') { + $searchMethod = FORMAL_NAME_SEARCH; + } elseif ($searchMode === 'username') { + $searchMethod = USERNAME_SEARCH; + } + $xdw = new XDWarehouse(); + list($userCount, $users) = $xdw->enumerateGridUsers( + $searchMethod, + $start, + $limit, + $query, + $usePiFilter, + $universityId + ); + + $entry_id = 0; + + $userEntries = []; + foreach ($users as $currentUser) { + $entry_id++; + + if ($searchMethod == FORMAL_NAME_SEARCH) { + $personName = $currentUser['long_name']; + $personID = $currentUser['id']; + } elseif ($searchMethod == USERNAME_SEARCH) { + $personName = $currentUser['absusername']; + + // Append the absusername to the id so that each entry is guaranteed + // to have a unique identifier (needed for dependent ExtJS combobox + // (TGUserDropDown.js) to work properly regarding selections). + $personID = $currentUser['id'] . ';' . $currentUser['absusername']; + } + + $userEntries[] = [ + 'id' => $entry_id, + 'person_id' => $personID, + 'person_name' => $personName + ]; + } + + $data = [ + 'success' => true, + 'status' => 'success', + 'message' => 'success', + 'total_user_count' => $userCount, + 'users' => $userEntries, + ]; + return $this->json($data); + } +} diff --git a/src/Controller/InternalDashboard/SummaryController.php b/src/Controller/InternalDashboard/SummaryController.php new file mode 100644 index 0000000000..8bc683b91c --- /dev/null +++ b/src/Controller/InternalDashboard/SummaryController.php @@ -0,0 +1,313 @@ +getCharts($request); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/internal_dashboard/controllers/summary.php')] + public function index(Request $request): Response + { + $operation = $this->getStringParam($request, 'operation'); + if (empty($operation)) { + return $this->json(buildError('operation_not_defined')); + } + try { + switch ($operation) { + case 'get_config': + return $this->getConfig($request); + case 'get_portlets': + return $this->getPortlets($request); + } + } catch(\Exception $e) { + return $this->json(buildError($e)); + } + return $this->json(buildError('invalid_operation_specified')); + } + + /** + * @throws Exception + */ + #[Route('{prefix}summary/configs', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function getConfig(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + + $config = XdmodConfiguration::assocArrayFactory( + 'internal_dashboard.json', + CONFIG_DIR + ); + + $summaries = []; + + foreach ($config['summary'] as $summary) { + + // Add an empty config if none is found. + if (!isset($summary['config'])) { + $summary['config'] = []; + } + + // Add log config. + if ($summary['class'] === 'XDMoD.Log.TabPanel') { + $logList = []; + + foreach ($config['logs'] as $log) { + $logSummary = Summary::factory($log['ident']); + + if ($logSummary->getProcessStartRowId() === null) { + continue; + } + + $logList[] = [ + 'id' => $log['ident'] . '-log-panel', + 'ident' => $log['ident'], + 'title' => $log['title'], + ]; + } + + $summary['config']['logConfigList'] = $logList; + } + + $summaries[] = $summary; + } + + return $this->json([ + 'success' => true, + 'response' => $summaries, + 'count' => count($summaries) + ]); + } + + /** + * @throws Exception + */ + #[Route('{prefix}summary/portlets', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function getPortlets(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + + $config = XdmodConfiguration::assocArrayFactory( + 'internal_dashboard.json', + CONFIG_DIR + ); + + $portlets = []; + + foreach ($config['portlets'] as $portlet) { + + // Add an empty config if none is found. + if (!isset($portlet['config'])) { + $portlet['config'] = []; + } + + $portlets[] = $portlet; + } + + // Add log portlets. + foreach ($config['logs'] as $log) { + $logSummary = Summary::factory($log['ident'], true); + + if ($logSummary->getProcessStartRowId() === null) { + continue; + } + + $portlets[] = [ + 'class' => 'XDMoD.Log.SummaryPortlet', + 'config' => [ + 'ident' => $log['ident'], + 'title' => $log['title'], + 'linkPath' => [ + 'log-tab-panel', + $log['ident'] . '-log-panel', + ], + ], + ]; + } + + return $this->json([ + 'success' => true, + 'response' => $portlets, + 'count' => count($portlets) + ]); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}summary/charts', requirements: ['prefix' => '.*'], methods: ['GET'])] + public function getCharts(Request $request): Response + { + $user = $this->getUser(); + if (null === $user) { + $user = XDUser::getPublicUser(); + } else { + $user = XDUser::getUserByUserName($user->getUserIdentifier()); + } + + $debugLevel = abs($this->getIntParam($request, 'debug_level', false, 0)); + $startDate = $this->getStringParam($request, 'start_date', true); + $endDate = $this->getStringParam($request, 'end_date', true); + $aggregationUnit = lcfirst($this->getStringParam($request, 'aggregation_unit', false, 'auto')); + $rawFilters = $this->getStringParam($request, 'filters'); + $publicUser = $this->getBooleanParam($request, 'public_user'); + + $rawParameters = []; + if (isset($rawFilters)) { + $filters = json_decode($rawFilters); + foreach ($filters->data as $filter) { + $key = sprintf('%s_filter', $filter->dimension_id); + $valueId = $filter->value_id; + if (!isset($rawParameters[$key])) { + $rawParameters[$key] = $valueId; + } else { + $rawParameters[$key] .= ',' . $valueId; + } + } + } + + $enabledRealms = Realms::getEnabledRealms(); + if (in_array('Jobs', $enabledRealms)) { + $query_descripter = new \User\Elements\QueryDescripter('Jobs', 'none'); + + // This try/catch block is intended to replace the "Base table or + // view not found: 1146 Table 'modw_aggregates.jobfact_by_day' + // doesn't exist" error message with something more informative for + // Open XDMoD users. + + try { + $query = new \DataWarehouse\Query\AggregateQuery( + 'Jobs', + $aggregationUnit, + $startDate, + $endDate, + 'none', + 'all', + $query_descripter->pullQueryParameters($rawParameters) + ); + + // this is used later on down the function. + $result = $query->execute(); + } catch (PDOException $e) { + if ($e->getCode() === '42S02' && strpos($e->getMessage(), 'modw_aggregates.jobfact_by_') !== false) { + $msg = 'Aggregate table not found, have you ingested your data?'; + throw new Exception($msg); + } else { + throw $e; + } + } + } + + $mostPrivilegedAcl = Acls::getMostPrivilegedAcl($user); + + $rolesConfig = \Configuration\XdmodConfiguration::assocArrayFactory('roles.json', CONFIG_DIR); + $roles = $rolesConfig['roles']; + + $mostPrivilegedAclName = $mostPrivilegedAcl->getName(); + $mostPrivilegedAclSummaryCharts = $roles['default']['summary_charts']; + + if (isset($roles[$mostPrivilegedAclName]['summary_charts'])) { + $mostPrivilegedAclSummaryCharts = $roles[$mostPrivilegedAclName]['summary_charts']; + } + + $summaryCharts = []; + foreach ($mostPrivilegedAclSummaryCharts as $chart) { + $realm = $chart['data_series']['data'][0]['realm']; + if (!in_array($realm, $enabledRealms)) { + continue; + } + $chart['preset'] = true; + + $summaryCharts[] = json_encode($chart); + } + + if (!isset($publicUser) || !$publicUser) { + $queryStore = new \UserStorage($user, 'queries_store'); + $queries = $queryStore->get(); + + if ($queries != NULL) { + foreach ($queries as $i => $query) { + if (isset($query['config'])) { + + $queryConfig = json_decode($query['config']); + + $name = isset($query['name']) ? $query['name'] : null; + + if (isset($name)) { + if (preg_match('/summary_(?P\S+)/', $query['name'], $matches) > 0) { + $queryConfig->summary_index = $matches['index']; + } else { + $queryConfig->summary_index = $query['name']; + } + } + + if (property_exists($queryConfig, 'summary_index') + && isset($queryConfig->summary_index) + && isset($queryConfig->featured) + && $queryConfig->featured + ) { + if (isset($summaryCharts[$queryConfig->summary_index])) { + $queryConfig->preset = true; + } + $summaryCharts[$queryConfig->summary_index] = json_encode($queryConfig); + } + } + } + } + } + + foreach ($summaryCharts as $i => $summaryChart) { + $summaryChartObject = json_decode($summaryChart); + $summaryChartObject->index = $i; + $summaryCharts[$i] = json_encode($summaryChartObject); + } + ksort($summaryCharts, SORT_STRING); + + $result['charts'] = json_encode(array_values($summaryCharts)); + + return $this->json([ + 'totalCount' => 1, + 'success' => true, + 'message' => '', + 'data' => [$result] + ]); + } +} diff --git a/src/Controller/InternalDashboard/UserAdminController.php b/src/Controller/InternalDashboard/UserAdminController.php new file mode 100644 index 0000000000..d90a1a10ab --- /dev/null +++ b/src/Controller/InternalDashboard/UserAdminController.php @@ -0,0 +1,977 @@ +passwordResetService = $passwordResetService; + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/controllers/user_admin.php')] + public function index(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + + $operation = $this->getStringParam($request, 'operation'); + if (empty($operation)) { + return $this->json(buildError('operation_not_defined')); + } + + // This try-catch ensures that any exceptions generated by the operations below are handled like they would have + // been when this was a controller. + try { + switch ($operation) { + case 'create_user': + return $this->createUser($request); + case 'delete_user': + return $this->deleteUser($request); + case 'empty_report_image_cache': + return $this->emptyReportImageCache($request); + case 'enum_institutions': + return $this->enumInstitutions($request); + case 'enum_exception_email_addresses': + return $this->enumExceptionEmailAddresses($request); + case 'enum_resource_providers': + return $this->enumResourceProviders($request); + case 'enum_user_types': + return $this->enumUserTypes($request); + case 'enum_roles': + return $this->enumRoles($request); + case 'get_user_details': + $userId = $this->getStringParam($request, 'uid', true, null, RESTRICTION_UID); + return $this->getUserDetails($request, $userId); + case 'list_users': + return $this->listUsers($request); + case 'pass_reset': + return $this->passwordReset($request); + case 'search_users': + return $this->searchForUsers($request); + case 'update_user': + return $this->updateUser($request); + default: + return $this->json(buildError('invalid_operation_specified')); + } + } catch (\Exception $e) { + return $this->json(buildError($e)); + } + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}internal_dashboard/users', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function listUsers(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + $xda = new XDAdmin(); + + $group = $this->getIntParam($request, 'group'); + $userListing = $xda->getUserListing($group); + + $users = []; + foreach ($userListing as $currentUser) { + + $userData = explode(';', $currentUser['username']); + if ($userData[0] !== 'Public User') { + $userEntry = [ + 'id' => $currentUser['id'], + 'username' => $userData[0], + 'first_name' => $currentUser['first_name'], + 'last_name' => $currentUser['last_name'], + 'account_is_active' => $currentUser['account_is_active'], + 'last_logged_in' => $this->parseMicrotime($currentUser['last_logged_in']) + ]; + + $users[] = $userEntry; + } + } + + return $this->json([ + 'success' => true, + 'status' => 'success', + 'users' => $users + ]); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}internal_dashboard/users/metadata', requirements: ['prefix' => '.*'], methods: ['GET'])] + public function getUserMetadata(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + + $pdo = DB::factory('database'); + + $userTypes = $pdo->query('SELECT id, type, color FROM moddb.UserTypes'); + $acls = $pdo->query("SELECT display AS description, acl_id AS role_id FROM moddb.acls WHERE name != 'pub' ORDER BY description"); + + return $this->json([ + 'success' => true, + 'user_types' => $userTypes, + 'user_roles' => $acls + ]); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}internal_dashboard/users/create', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function createUser(Request $request): Response + { + $this->logger->warning('[start] Creating User'); + + try { + $userName = $this->getStringParam($request, 'username', true, null, RESTRICTION_USERNAME); + } catch (BadRequestHttpException $e) { + $message = $e->getMessage(); + if (str_contains($message, 'is a required parameter')) { + return $this->json(buildError("'username' not specified."), 400); + } else { + return $this->json(buildError("Invalid value specified for 'username'."), 400); + } + } + + try { + $firstName = $this->getStringParam($request, 'first_name', true, null, RESTRICTION_FIRST_NAME); + }catch (BadRequestHttpException $e) { + $message = $e->getMessage(); + if (str_contains($message, 'is a required parameter')) { + return $this->json(buildError("'first_name' not specified."), 400); + } else { + return $this->json(buildError("Invalid value specified for 'first_name'."), 400); + } + } + + try { + $lastName = $this->getStringParam($request, 'last_name', true, null, RESTRICTION_LAST_NAME); + } catch (BadRequestHttpException $e) { + $message = $e->getMessage(); + if (str_contains($message, 'is a required parameter')) { + return $this->json(buildError("'last_name' not specified."), 400); + } else { + return $this->json(buildError("Invalid value specified for 'last_name'."), 400); + } + } + + try { + $userType = intval($this->getStringParam($request, 'user_type', true, null, RESTRICTION_GROUP)); + } catch (BadRequestHttpException $e) { + $message = $e->getMessage(); + if (str_contains($message, 'is a required parameter')) { + return $this->json(buildError("'user_type' not specified."), 400); + } else { + return $this->json(buildError("Invalid value specified for 'user_type'."), 400); + } + } + + try { + $institution = intval($this->getStringParam($request, 'institution', true, null, RESTRICTION_INSTITUTION)); + } catch (BadRequestHttpException $e) { + $message = $e->getMessage(); + if (str_contains($message, 'is a required parameter')) { + return $this->json(buildError("'institution' not specified."), 400); + } else { + return $this->json(buildError("Invalid value specified for 'institution'."), 400); + } + } + + + try { + $personAssignment = intval($this->getStringParam($request, 'assignment', true, null, RESTRICTION_ASSIGNMENT)); + } catch (BadRequestHttpException $e) { + $message = $e->getMessage(); + if (str_contains($message, 'is a required parameter')) { + return $this->json(buildError("'assignment' not specified."), 400); + } else { + return $this->json(buildError("Invalid value specified for 'assignment'."), 400); + } + } + + try { + $emailAddress = $this->getEmailParam($request, 'email_address', true); + } catch (BadRequestHttpException $e) { + $message = $e->getMessage(); + if (str_contains($message, 'is a required parameter')) { + return $this->json(buildError("'email_address' not specified."), 400); + } else { + return $this->json(buildError("Failed to assert 'email_address'."), 400); + } + } + + try { + $acls = json_decode($this->getStringParam($request, 'acls', true), true); + } catch (BadRequestHttpException $e) { + $message = $e->getMessage(); + if (str_contains($message, 'is a required parameter')) { + return $this->json(buildError("Acl information is required"), 400); + } else { + return $this->json(buildError("Invalid value specified for 'acls'."), 400); + } + } + + $sticky = $this->getBooleanParam($request, 'sticky', false, false); + + // Ensure that we have at least on acl for the new user. + if (empty($acls)) { + return $this->json(buildError('Acl information is required'), 400); + } + // Checking for an acl set that only contains feature acls. + // Feature acls are acls that only provide access to an XDMoD feature and + // are not used for data access. + if (!$this->hasDataAcls($acls)) { + return $this->json(buildError('Please include a non-feature acl ( i.e. User, PI etc. )'), 400); + } + + $tempPassword = $this->generateTempPassword(); + + $newUser = new \XDUser( + $userName, + $tempPassword, + $emailAddress, + $firstName, + '', + $lastName, + array_keys($acls), + ROLE_ID_USER, + $institution, + $personAssignment, + [], + $sticky + ); + $newUser->setUserType($userType); + $newUser->saveUser(); + + foreach ($acls as $acl => $centers) { + // Now that the user has been updated, We need to check if they have been assigned any + // 'center' acls. If they have and if an 'institution' has been provided ( it should have + // been ) then we need to call `setOrganizations` so that the user_acl_group_by_parameters + // table is updated accordingly. + if (in_array($acl, ['cd', 'cs'])) { + $newUser->setOrganizations( + [ + $institution => [ + 'primary' => 1, + 'active' => 1 + ] + ], + $acl + ); + } + } + + // 'institution' now corresponds to a Users organization and will always be present, not only + // when a user has been assigned the campus champion acl. This means we need to update the logic + // that gates the `setInstitution` function call to include a check if the user has been + // assigned the Campus Champion acl. + if (in_array(ROLE_ID_CAMPUS_CHAMPION, array_keys($acls))) { + $newUser->setInstitution($institution); + } + + list($subject, $emailBody) = $this->generateNewUserEmail($newUser); + MailWrapper::sendMail([ + 'body' => $emailBody, + 'subject' => $subject, + 'toAddress' => $emailAddress + ]); + $this->logger->warning('[done] Creating User'); + return $this->json([ + 'success' => true, + 'user_type' => $userType, + 'message' => sprintf('User %s created successfully', $userName) + ]); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}internal_dashboard/users/update', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function updateUser(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $currentUser = $this->authorize($request, ['mgr']); + + $userId = intval($this->getStringParam($request, 'uid', true, null, RESTRICTION_UID)); + $userToUpdate = \XDUser::getUserByID($userId); + if (!isset($userToUpdate)) { + return $this->json([ + 'success' => false, + 'status' => 'user_does_not_exist' + ]); + } + + + $potentialParameters = [ + 'first_name' => $this->getStringParam($request, 'first_name', false, null, RESTRICTION_FIRST_NAME), + 'last_name' => $this->getStringParam($request, 'last_name', false, null, RESTRICTION_LAST_NAME), + 'user_type' => $this->getIntParam($request, 'user_type'), + 'institution' => $this->getIntParam($request, 'institution'), + 'person' => $this->getIntParam($request, 'assigned_user'), + 'is_active' => $this->getBooleanParam($request, 'is_active') + ]; + + $qualifyingParameters = array_filter( + $potentialParameters, + function ($value) { + return isset($value); + } + ); + + $acls = null; + $aclsRaw = $this->getStringParam($request, 'acls'); + if (isset($aclsRaw)) { + $acls = json_decode($aclsRaw, true); + if (count($acls) < 1) { + return $this->json(buildError('Acl information is required')); + } + } + + // If we're updating ourselves we need to ensure a few things... + if ($currentUser->getUserID() === $userToUpdate->getUserID()) { + + // Make sure that we're not trying to disable ourselves. + if (isset($qualifyingParameters['is_active']) && !$qualifyingParameters['is_active']) { + return $this->json([ + 'success' => false, + 'status' => 'You are not allowed to disable your own account.' + ]); + } + + // Check to make sure that we're not trying to revoke our own manager access. + if (isset($acls)) { + if (!array_key_exists(ROLE_ID_MANAGER, $acls)) { + return $this->json([ + 'success' => false, + 'status' => 'You are not allowed to revoke manager access from yourself.' + ]); + } + } + } + + if (isset($qualifyingParameters['first_name'])) { + $userToUpdate->setFirstName($qualifyingParameters['first_name']); + } + + if (isset($qualifyingParameters['last_name'])) { + $userToUpdate->setLastName($qualifyingParameters['last_name']); + } + + $emailAddress = $this->getEmailParam($request, 'email_address', true); + + // Make sure that if we're anything other than an SSO User that we cannot remove our email address. + if ($userToUpdate->getUserType() !== SSO_USER_TYPE && strlen($emailAddress) < 1) { + return $this->json([ + 'success' => true, + 'status' => 'This XDMoD user must have an e-mail address set.' + ]); + } + $userToUpdate->setEmailAddress($emailAddress); + + if (isset($qualifyingParameters['person'])) { + $userToUpdate->setPersonID($qualifyingParameters['person']); + } + + if (isset($qualifyingParameters['is_active'])) { + $userToUpdate->setAccountStatus($qualifyingParameters['is_active']); + } + + // If we're trying to update the user's type, only non-SSO users can do so. + if (isset($qualifyingParameters['user_type'])) { + if ($userToUpdate->getUserType() !== SSO_USER_TYPE) { + $userToUpdate->setUserType($qualifyingParameters['user_type']); + } + } + + $sticky = $this->getBooleanParam($request, 'sticky'); + if (isset($sticky)) { + $userToUpdate->setSticky($sticky); + } + + $originalAcls = $userToUpdate->getAcls(true); + if (isset($acls)) { + if (!$this->hasDataAcls($acls)) { + return $this->json(buildError('Please include a non-feature acl ( i.e. User, PI etc. )')); + } + // first clear the updated user's acls + $userToUpdate->setAcls([]); + foreach ($acls as $aclName => $centers) { + $acl = Acls::getAclByName($aclName); + $userToUpdate->addAcl($acl); + } + } else { + return $this->json(buildError('Acl information is required.')); + } + + if (isset($qualifyingParameters['institution'])) { + $userToUpdate->setOrganizationID($qualifyingParameters['institution']); + $oldCampusChampion = in_array(ROLE_ID_CAMPUS_CHAMPION, $originalAcls); + $newCampusChampion = in_array(ROLE_ID_CAMPUS_CHAMPION, array_keys($acls)); + + if ($newCampusChampion && !$oldCampusChampion) { + $userToUpdate->setInstitution($qualifyingParameters['institution']); + } elseif (!$newCampusChampion && $oldCampusChampion) { + $userToUpdate->disassociateWithInstitution(); + } + } + + // We've updated everything that we need to, now we can save. + try { + $userToUpdate->saveUser(); + + // Now that the user has been saved, clear their organizations + $userToUpdate->setOrganizations([], ROLE_ID_CENTER_DIRECTOR); + $userToUpdate->setOrganizations([], ROLE_ID_CENTER_STAFF); + + // and add the new ones. + foreach ($acls as $aclName => $centers) { + if (in_array($aclName, ['cd', 'cs']) && isset($qualifyingParameters['institution'])) { + $userToUpdate->setOrganizations( + [ + $qualifyingParameters['institution'] => [ + 'primary' => 1, + 'active' => 1 + ] + ], + $aclName + ); + } + } + } catch (Exception $exception) { + return $this->json([ + 'success' => false, + 'status' => $exception->getMessage() + ]); + } + + $userName = $userToUpdate->getUsername(); + return $this->json([ + 'success' => true, + 'status' => sprintf( + '%sUser %s updated successfully', + $userToUpdate->isSSOUser() ? 'Single Sine On' : '', + $userName + ), + 'username' => $userName, + 'user_type' => (string) $userToUpdate->getUserType() # JS code expects a string encoded value + ]); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}internal_dashboard/users/search', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function searchForUsers(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + + $searchCriteria = json_decode($this->getStringParam($request, 'search_crit', true), true); + + $datawarehouse = new \XDWarehouse(); + $users = $datawarehouse->searchUsers($searchCriteria); + + return $this->json([ + 'success' => true, + 'data' => $users, + 'total' => count($users) + ]); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}internal_dashboard/users/password', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function passwordReset(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + + $userId = $this->getStringParam($request, 'uid', true, null, RESTRICTION_UID); + + $userToContact = XDUser::getUserByID($userId); + if ($userToContact === null) { + return $this->json([ + 'success' => false, + 'status' => 'user_does_not_exist' + ]); + } + + $this->passwordResetService->sendPasswordResetEmail($userToContact); + + $message = sprintf('Password reset e-mail sent to user %s', $userToContact->getUsername()); + return $this->json([ + 'success' => true, + 'message' => $message, + 'status' => $message + ]); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}internal_dashboard/users/institutions', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function enumInstitutions(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + + $query = $this->getStringParam($request, 'query'); + $xdAdmin = new \XDAdmin(); + + $institutions = $xdAdmin->enumerateInstitutions($query); + + // If there are no organizations for the provided query, then by default retrieve / return the full list of + // organizations. + $institutionCount = count($institutions); + if (count($institutions) === 0) { + $institutions = $xdAdmin->enumerateInstitutions(); + } + + return $this->json([ + 'success' => true, + 'status' => 'success', + 'total_institution_count' => $institutionCount, + 'institutions' => $institutions + ]); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}internal_dashboard/users/roles', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function enumRoles(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + + $xdAdmin = new \XDAdmin(); + $roles = $xdAdmin->enumerateAcls(); + + $roleEntries = []; + foreach ($roles as $currentRole) { + // requiresCenter can only be true iff the current install supports + // multiple service providers. + if ($currentRole['name'] !== 'pub') { + $roleEntries[] = [ + 'acl' => $currentRole['display'], + 'acl_id' => $currentRole['name'], + 'include' => false, + 'primary' => false, + 'displays_center' => false, + 'requires_center' => false + ]; + } + } + return $this->json([ + 'success' => true, + 'status' => 'success', + 'acls' => $roleEntries + ]); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}internal_dashboard/users/types', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function enumUserTypes(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + + $xdAdmin = new \XDAdmin(); + $userTypes = $xdAdmin->enumerateUserTypes(); + + $userTypeEntries = []; + foreach ($userTypes as $type) { + $userTypeEntries[] = [ + 'id' => $type['id'], + 'type' => $type['type'], + ]; + } + $data = [ + 'success' => true, + 'status' => 'success', + 'user_types' => $userTypeEntries + ]; + return $this->json($data); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}internal_dashboard/users/providers', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function enumResourceProviders(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + + $xdAdmin = new \XDAdmin(); + $resourceProviders = $xdAdmin->enumerateResourceProviders(); + + $providers = []; + foreach ($resourceProviders as $provider) { + $providers[] = [ + 'id' => $provider['id'], + 'organization' => $provider['organization'] . ' (' . $provider['name'] . ')', + 'include' => false + ]; + } + + return $this->json([ + 'status' => 'success', + 'success' => true, + 'providers' => $providers + ]); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}internal_dashboard/users/emails/exceptions', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function enumExceptionEmailAddresses(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + + $xdAdmin = new \XDAdmin(); + $emailAddresses = $xdAdmin->enumerateExceptionEmailAddresses(); + + return $this->json([ + 'success' => true, + 'status' => 'success', + 'email_addresses' => $emailAddresses + ]); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}internal_dashboard/users/reports/images/cache', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function emptyReportImageCache(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + + $userId = $this->getStringParam($request, 'uid', true, null, RESTRICTION_UID); + $targetUser = XDUser::getUserByID($userId); + if (!isset($targetUser)) { + return $this->json(buildError('user_does_not_exist')); + } + + $chart_pool = new \XDChartPool($targetUser); + $chart_pool->emptyCache(); + + $report_manager = new \XDReportManager($targetUser); + $report_manager->emptyCache(); + $report_manager->flushReportImageCache(); + + return $this->json([ + 'success' => true, + 'message' => sprintf( + 'The report image cache for user %s has been emptied', + $targetUser->getUsername() + ) + ]); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}internal_dashboard/users/delete', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function deleteUser(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $requestingUser = $this->authorize($request, ['mgr']); + + $userId = $this->getStringParam($request, 'uid', true, null, RESTRICTION_UID); + $targetUser = XDUser::getUserByID($userId); + if (!isset($targetUser)) { + return $this->json(buildError('user_does_not_exist')); + } + + if ($requestingUser->getUsername() === $targetUser->getUsername()) { + return $this->json(buildError('You are not allowed to delete your own account.')); + } + + // Remove all entries in this user's profile + $profile = $targetUser->getProfile(); + $profile->clear(); + + $statusPrefix = $targetUser->isSSOUser() ? 'Single Sign On ' : ''; + $displayUsername = $targetUser->getUsername(); + + $targetUser->removeUser(); + + return $this->json([ + 'success' => true, + 'message' => sprintf( + '%sUser %s deleted from the portal', + $statusPrefix, + $displayUsername + ) + ]); + } + + /** + * @param Request $request + * @param int|string $userId + * @return Response + * @throws Exception + */ + #[Route('{prefix}internal_dashboard/users/{userId}', requirements: ['userId' => '\d+', 'prefix' => '.*'], methods: ['POST'])] + public function getUserDetails(Request $request, $userId): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + + $selected_user = XDUser::getUserByID($userId); + + if ($selected_user === null) { + return $this->json(buildError('user_does_not_exist')); + } + + // ----------------------------- + + $userDetails = []; + + $userDetails['username'] = $selected_user->getUsername(); + $userDetails['formal_name'] = $selected_user->getFormalName(); + + $userDetails['time_created'] = $selected_user->getCreationTimestamp(); + $userDetails['time_updated'] = $selected_user->getUpdateTimestamp(); + $userDetails['time_last_logged_in'] = $selected_user->getLastLoginTimestamp(); + + $userDetails['email_address'] = $selected_user->getEmailAddress(); + + if ($userDetails['email_address'] == NO_EMAIL_ADDRESS_SET) { + $userDetails['email_address'] = ''; + } + + $userDetails['assigned_user_id'] = $selected_user->getPersonID(true); + + $userDetails['institution'] = $selected_user->getOrganizationID(); + + $userDetails['user_type'] = $selected_user->getUserType(); + + $obj_warehouse = new XDWarehouse(); + + $userDetails['institution_name'] = $obj_warehouse->resolveInstitutionName($userDetails['institution']); + + $userDetails['assigned_user_name'] = $obj_warehouse->resolveName($userDetails['assigned_user_id']); + + if ($userDetails['assigned_user_name'] == NO_MAPPING) { + $userDetails['assigned_user_name'] = ''; + } + + $userDetails['is_active'] = $selected_user->getAccountStatus() ? 'active' : 'disabled'; + $userDetails['sticky'] = $selected_user->isSticky(); + + $acls = Acls::listUserAcls($selected_user); + $populatedAcls = array_reduce( + $acls, + function ($carry, $item) use ($selected_user) { + $aclName = $item['name']; + $aclCenters = []; + if ($item['requires_center'] === true) { + $aclCenters = Acls::getDescriptorParamValues( + $selected_user, + $aclName, + 'provider' + ); + } + + $carry[$aclName] = $aclCenters; + + return $carry; + }, + [] + ); + + $userDetails['acls'] = $populatedAcls; + + return $this->json([ + 'success' => true, + 'status' => 'success', + 'user_information' => $userDetails + ]); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}internal_dashboard/users/existing', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function enumExistingUsers(Request $request): Response + { + $group_filter = $this->getStringParam($request, 'group_filter'); + $role_filter = $this->getStringParam($request, 'role_filter'); + $context_filter = $this->getStringParam($request, 'context_filter', false, ''); + + $results = Users::getUsers($group_filter, $role_filter, $context_filter); + $filtered = []; + foreach ($results as $user) { + if ($user['username'] !== 'Public User') { + $filtered[] = $user; + } + } + + return $this->json([ + 'success' => true, + 'count' => count($filtered), + 'response' => $filtered + ]); + } + + /** + * @return string + */ + private function generateTempPassword(): string + { + $password_chars = 'abcdefghijklmnopqrstuvwxyz!@#$%-_=+ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'; + $max_password_chars_index = strlen($password_chars) - 1; + $password = ''; + for ($i = 0; $i < CHARLIM_PASSWORD; $i++) { + $password .= $password_chars[mt_rand(0, $max_password_chars_index)]; + } + return $password; + } + + /** + * @param array $acls + * @return bool + */ + private function hasDataAcls(array $acls): bool + { + $aclNames = []; + $featureAcls = Acls::getAclsByTypeName('feature'); + $tabAcls = Acls::getAclsByTypeName('tab'); + $uiOnlyAcls = array_merge($featureAcls, $tabAcls); + if (count($uiOnlyAcls) > 0) { + $aclNames = array_reduce( + $uiOnlyAcls, + function ($carry, Acl $item) { + $carry [] = $item->getName(); + return $carry; + }, + [] + ); + } + $diff = array_diff(array_keys($acls), $aclNames); + return !empty($diff); + } + + /** + * @return array in the form [$subject, $emailBody] + * @throws SyntaxError + * @throws RuntimeError + * @throws LoaderError + * @throws Exception + */ + private function generateNewUserEmail(\XDUser $newUser): array + { + $pageTitle = \xd_utilities\getConfiguration('general', 'title'); + $siteAddress = \xd_utilities\getConfigurationUrlBase('general', 'site_address'); + $userName = $newUser->getUsername(); + $rid = $newUser->generateRID(); + + return [ + sprintf('%s: Account Created', $pageTitle), + $this->twig->render( + 'twig/emails/new_user.html.twig', + [ + 'page_title' => $pageTitle, + 'site_address' => $siteAddress, + 'username' => $userName, + 'rid' => $rid + ] + ) + ]; + } + + private function parseMicrotime($mtime) + { + + $time_frags = explode('.', $mtime); + return $time_frags[0] * 1000; + + } +} diff --git a/src/Controller/InternalDashboard/UserVisitController.php b/src/Controller/InternalDashboard/UserVisitController.php new file mode 100644 index 0000000000..77ede6ac09 --- /dev/null +++ b/src/Controller/InternalDashboard/UserVisitController.php @@ -0,0 +1,83 @@ + '.*'],)] +class UserVisitController extends BaseController +{ + public static $columns = [ + "Last Name", + "First Name", + "E-Mail", + "Roles", + "Visit Frequency", + "User Type", + "Date", + "Count" + ]; + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('', methods: ['POST'])] + public function getUserVisits(Request $request): Response + { + list($data,) = $this->getUserVisitData($request); + return $this->json([ + 'success' => true, + 'stats' => $data + ]); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/export', methods: ['POST'])] + public function exportUserVisits(Request $request): Response + { + list($data, list($timeframe,)) = $this->getUserVisitData($request); + + $data = array_map(function($row) { + return implode(',', $row); + }, $data); + array_unshift($data, implode(',', self::$columns)); + + $content = sprintf("%s\n", implode("\n", $data)); + $this->logger->debug(sprintf("Export User Visits: Content: %s", $content)); + return new Response($content, 200, [ + 'Content-Type' => 'text/csv', + 'Content-Disposition' => sprintf('attachment;filename="xdmod_visitation_stats_by_%s.csv"', $timeframe) + ]); + } + + /** + * @return array in the form [userVisits, [timeframe, userTypes]] + * @throws Exception + */ + private function getUserVisitData(Request $request): array + { + $timeframe = $this->getStringParam($request, 'timeframe', true); + $userTypes = explode(',', $this->getStringParam($request, 'user_types', true)); + if (strtolower($timeframe) !== 'year' && strtolower($timeframe) !== 'month') { + throw new BadRequestHttpException('Invalid value specified for the timeframe'); + } + $results = \XDStatistics::getUserVisitStats($timeframe, $userTypes); + return [$results, [$timeframe, $userTypes]]; + } +} diff --git a/src/Controller/MailController.php b/src/Controller/MailController.php new file mode 100644 index 0000000000..92d128e7b7 --- /dev/null +++ b/src/Controller/MailController.php @@ -0,0 +1,242 @@ +getUserFromRequest($request); + $operation = $this->getStringParam($request, 'operation'); + + if (empty($operation)) { + return $this->json(buildError('operation_not_specified')); + } + + switch ($operation) { + case 'contact': + return $this->contact($request, $user); + case 'sign_up': + return $this->signUp($request); + default: + return $this->json(buildError('invalid_operation_specified')); + } + } + + /** + * Takes the place of the old html/controllers/mailer/contact.php + * + * @param Request $request + * @param ?XDUser $user + * @return Response + */ + private function contact(Request $request, ?XDUser $user): Response + { + if (!isset($user)) { + $user = XDUser::getPublicUser(); + } + + $name = $this->getStringParam($request, 'name', true, null, RESTRICTION_FIRST_NAME); + // This variable is overwritten before it is used. I'm leaving it here for now but it should be removed after + // the rest stack migration is complete. + $message = $this->getStringParam($request, 'message', true, null, RESTRICTION_NON_EMPTY); + $username = $this->getStringParam($request, 'username', true, null, RESTRICTION_NON_EMPTY); + $token = $this->getStringParam($request, 'token', true, null, RESTRICTION_NON_EMPTY); + $timestamp = $this->getStringParam($request, 'timestamp', true, null, RESTRICTION_NON_EMPTY); + $email = $this->getEmailParam($request, 'email', true); + $reason = $this->getStringParam($request, 'reason', false, 'contact'); + + $userInfo = $user->isPublicUser() ? 'Public Visitor' : "Username: $username"; + + $this->verifyCaptcha($request); + + switch ($reason) { + case 'wishlist': + $subject = '[WISHLIST] Feature request sent from a portal visitor'; + $message_type = 'feature request'; + break; + + default: + $subject = 'Message sent from a portal visitor'; + $message_type = 'message'; + break; + } + $timestamp = date('m/d/Y, g:i:s A', $timestamp); + $message = "Below is a $message_type from '$name' ($email):\n\n"; + $message .= $message; + $message .= "\n------------------------\n\nSession Tracking Data:\n\n "; + $message .= "$userInfo\n\n Token: $token\n Timestamp: $timestamp"; + + try { + //Original sender's e-mail must be in the 'fromAddress' field for the XDMoD Request Tracker to function + MailWrapper::sendMail(array( + 'body' => $message, + 'subject' => $subject, + 'toAddress' => $this->parameters->get('xdmod.portal_settings.general.contact_page_recipient'), + 'fromAddress' => $_POST['email'], + 'fromName' => $_POST['name'] + ) + ); + } catch (Exception $e) { + return $this->json([ + 'success' => false, + 'message' => $message + ]); + } + + $message + = "Hello, $name\n\n" + . "This e-mail is to inform you that the XDMoD Portal Team has received your $message_type, and will\n" + . "be in touch with you as soon as possible.\n\n" + . MailWrapper::getMaintainerSignature(); + + try { + MailWrapper::sendMail(array( + 'body' => $message, + 'subject' => "Thank you for your $message_type.", + 'toAddress' => $_POST['email'] + ) + ); + } catch (Exception $e) { + return $this->json([ + 'success' => false, + 'message' => $message + ]); + } + return $this->json([ + 'success' => true + ]); + } + + /** + * Takes the place of the old html/controllers/mailer/sign_up.php + * @param Request $request + * @return Response + * @throws Exception if unable to contact the database. + */ + private function signUp(Request $request): Response + { + $firstName = $this->getStringParam($request, 'first_name', true, null, RESTRICTION_FIRST_NAME); + $lastName = $this->getStringParam($request, 'last_name', true, null, RESTRICTION_LAST_NAME); + $title = $this->getStringParam($request, 'title', true, null, RESTRICTION_NON_EMPTY); + $organization = $this->getStringParam($request, 'organization', true, null, RESTRICTION_NON_EMPTY); + $fieldOfScience = $this->getStringParam($request, 'field_of_science', true, null, RESTRICTION_NON_EMPTY); + $additionalInformation = $this->getStringParam($request, 'additional_information', true, null, RESTRICTION_NON_EMPTY); + $email = $this->getEmailParam($request, 'email', true); + + $this->verifyCaptcha($request); + + // Insert account request into database (so it appears in the internal + // dashboard under "XDMoD Account Requests"). + $pdo = DB::factory('database'); + + $pdo->execute( + " + INSERT INTO AccountRequests ( + first_name, + last_name, + organization, + title, + email_address, + field_of_science, + additional_information, + time_submitted, + status, + comments + ) VALUES ( + :first_name, + :last_name, + :organization, + :title, + :email_address, + :field_of_science, + :additional_information, + NOW(), + 'new', + '' + ) + ", + [ + 'first_name' => $firstName, + 'last_name' => $lastName, + 'organization' => $organization, + 'title' => $title, + 'email_address' => $email, + 'field_of_science' => $fieldOfScience, + 'additional_information' => $additionalInformation + ] + ); + + // Create email. + + $time_requested = date('D, F j, Y \a\t g:i A'); + $organization = ORGANIZATION_NAME; + + $message = <<parameters->get('xdmod.portal_settings.general.title') + ); + $toAddress = $this->parameters->get('xdmod.portal_settings.general.contact_page_recipient'); + $fromAddress = $this->getEmailParam($request, 'email'); + $fromName = sprintf( + '$%s, %s', + $this->getStringParam($request, 'last_name'), + $this->getStringParam($request, 'first_name') + ); + + try { + MailWrapper::sendMail([ + 'body' => $message, + 'subject' => $subject, + 'toAddress' => $toAddress, + 'fromAddress' => $fromAddress, + 'fromName' => $fromName + ]); + $response['success'] = true; + } catch (Exception $e) { + $response['success'] = false; + $response['message'] = $e->getMessage(); + } + + return $this->json($response); + } +} diff --git a/src/Controller/MetricExplorerController.php b/src/Controller/MetricExplorerController.php new file mode 100644 index 0000000000..8f3ac470ab --- /dev/null +++ b/src/Controller/MetricExplorerController.php @@ -0,0 +1,976 @@ + '.*'], methods: ['GET'])] + public function getQueries(Request $request): Response + { + $action = 'getQueries'; + $payload = [ + 'success' => false, + 'action' => $action + ]; + $statusCode = 401; + + try { + $user = $this->authorize($request); + if (isset($user)) { + $queries = new \UserStorage($user, self::QUERIES_STORE); + $data = $queries->get(); + + foreach ($data as &$query) { + $this->removeRoleFromQuery($user, $query); + $query['name'] = htmlspecialchars($query['name'], ENT_COMPAT, 'UTF-8', false); + } + + $payload['data'] = $data; + $payload['success'] = true; + $statusCode = 200; + } else { + $payload['message'] = self::DEFAULT_ERROR_MESSAGE; + } + } catch (BadRequestHttpException|HttpException $e) { + $payload['message'] = $e->getMessage(); + $statusCode = $e->getStatusCode(); + } catch(Exception $e) { + $payload['message'] = $e->getMessage(); + $statusCode = 500; + } + + return $this->json($payload, $statusCode); + } + + /** + * Retrieve a query's information by unique id for the requesting user. + * + * @param Request $request + * @param string $queryId + * @return Response + */ + #[Route('{prefix}metrics/explorer/queries/{queryId}', requirements: ["queryId"=>"\w+", 'prefix' => '.*'], methods: ['GET'])] + public function getQueryByid(Request $request, string $queryId): Response + { + $action = 'getQueryById'; + $payload = array( + 'success' => false, + 'action' => $action, + ); + $statusCode = 401; + + try { + $user = $this->authorize($request); + if (isset($user)) { + $queries = new \UserStorage($user, self::QUERIES_STORE); + + $query = $queries->getById($queryId); + + if (isset($query)) { + $payload['data'] = $query; + $payload['data']['name'] = htmlspecialchars($query['name'], ENT_COMPAT, 'UTF-8', false); + $payload['success'] = true; + $statusCode = 200; + } else { + $payload['message'] = 'Unable to find the query identified by the provided id: ' . $queryId; + $statusCode = 404; + } + } else { + $payload['message'] = self::DEFAULT_ERROR_MESSAGE; + } + } catch (BadRequestHttpException|HttpException $e) { + $payload['message'] = $e->getMessage(); + $statusCode = $e->getStatusCode(); + } catch(Exception $e) { + $payload['message'] = $e->getMessage(); + $statusCode = 500; + } + + return $this->json($payload, $statusCode); + } + + /** + * Create a new query to be stored in the requesting users User Profile. + * + * @param Request $request + * @return Response + */ + #[Route('{prefix}metrics/explorer/queries', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function createQuery(Request $request): Response + { + $action = 'creatQuery'; + $payload = array( + 'success' => false, + 'action' => $action, + ); + $statusCode = 401; + try { + $user = $this->authorize($request); + if (isset($user)) { + $queries = new \UserStorage($user, self::QUERIES_STORE); + $data = $request->get('data'); + if ($data === null) { + throw new BadRequestHttpException('data is a required parameter.'); + } + if (!is_string($data)) { + throw new BadRequestHttpException('Invalid value for data. Must be a(n) string.'); + } + $data = json_decode($data, true); + $success = $queries->insert($data) != null; + $payload['success'] = $success; + if ($success) { + $payload['success'] = true; + $payload['data'] = $data; + $statusCode = 200; + } else { + $payload['message'] = 'Error creating chart. User is over the chart limit.'; + $statusCode = 500; + } + } else { + $payload['message'] = self::DEFAULT_ERROR_MESSAGE; + } + } catch (BadRequestHttpException|HttpException $e) { + $payload['message'] = $e->getMessage(); + $statusCode = $e->getStatusCode(); + } catch (\Exception $e) { + $payload['message'] = $e->getMessage(); + $statusCode = 500; + } + + return $this->json($payload, $statusCode); + } + + /** + * Update the query identified by the provided 'id' parameter with the + * values of the following form params ( if provided ): + * - name + * - config + * - timestamp + * + * @param Request $request + * @param string $queryId + * @return Response + */ + #[Route('{prefix}metrics/explorer/queries/{queryId}', requirements: ["queryId"=> "\w+", 'prefix' => '.*'], methods: ['PUT', "POST"])] + public function updateQueryById(Request $request, string $queryId): Response + { + $action = 'updateQuery'; + $payload = array( + 'success' => false, + 'action' => $action, + 'message' => 'success' + ); + $statusCode = 401; + + try { + $user = $this->authorize($request); + if (isset($user)) { + $queries = new \UserStorage($user, self::QUERIES_STORE); + + $query = $queries->getById($queryId); + if (isset($query)) { + $data = $request->get('data'); + if (isset($data)) { + if (!is_string($data)) { + throw new BadRequestHttpException('Invalid value for data. Must be a(n) string.'); + } + $jsonData = json_decode($data, true); + $name = isset($jsonData['name']) ? $jsonData['name'] : null; + $config = isset($jsonData['config']) ? $jsonData['config'] : null; + $ts = isset($jsonData['ts']) ? $jsonData['ts'] : microtime(true); + } else { + $name = $this->getStringParam($request, 'name'); + $config = $this->getStringParam($request, 'config'); + $ts = $this->getDateTimeFromUnixParam($request, 'ts'); + } + + if (isset($name)) { + $query['name'] = $name; + } + if (isset($config)) { + $query['config'] = $config; + } + if (isset($ts)) { + $query['ts'] = $ts; + } + + $queries->upsert($queryId, $query); + + // required for the UI to do it's thing. + $total = count($queries->get()); + + // make sure everything is in place for returning to the + // front end. + $payload['total'] = $total; + $payload['success'] = true; + $statusCode = 200; + } else { + $payload['message'] = 'There was no query found for the given id'; + $statusCode = 404; + } + } else { + $payload['message'] = self::DEFAULT_ERROR_MESSAGE; + } + } catch (BadRequestHttpException|HttpException $e) { + $payload['message'] = $e->getMessage(); + $statusCode = $e->getStatusCode(); + } catch(\Exception $e) { + $payload['message'] = $e->getMessage(); + $statusCode = 500; + } + + return $this->json($payload, $statusCode); + } + + /** + * + * @param Request $request + * @param string $queryId + * @return Response + */ + #[Route('{prefix}metrics/explorer/queries/{queryId}', requirements: ["queryId"=> "\w+", 'prefix' => '.*'], methods: ['DELETE'])] + public function deleteQueryById(Request $request, string $queryId): Response + { + $action = 'deleteQueryById'; + $payload = array( + 'success' => false, + 'action' => $action, + 'message' => 'success' + ); + $statusCode = 401; + + try { + $user = $this->authorize($request); + if (isset($user)) { + $queries = new \UserStorage($user, self::QUERIES_STORE); + $query = $queries->getById($queryId); + + if (isset($query)) { + + $before = count($queries->get()); + $after = $queries->delById($queryId); + $success = $before > $after; + + // make sure everything is in place for returning to the + // front end. + $payload['success'] = $success; + $payload['message'] = $success ? $payload['message'] : 'There was an error removing the query identified by: ' . $queryId; + + $statusCode = $success ? 200 : 500; + } else { + $payload['message'] = 'There was no query found for the given id'; + $statusCode = 404; + } + } else { + $payload['message'] = self::DEFAULT_ERROR_MESSAGE; + } + } catch (BadRequestHttpException|HttpException $e) { + $payload['message'] = $e->getMessage(); + $statusCode = $e->getStatusCode(); + } catch (Exception $e) { + $payload['message'] = $e->getMessage(); + $statusCode = 500; + } + + return $this->json($payload, $statusCode); + } + + private function removeRoleFromQuery(XDUser $user, array &$query) + { + // If the query doesn't have a config, stop. + if (!array_key_exists('config', $query)) { + return; + } + + // If the query config doesn't have an active role, stop. + $queryConfig = json_decode($query['config']); + if (!property_exists($queryConfig, 'active_role')) { + return; + } + + // Remove the active role from the query config. + $activeRoleId = $queryConfig->active_role; + unset($queryConfig->active_role); + + // Check whether or not $activeRoleId is an acl name or acl display value. + // ( Old queries may utilize the `display` property). + $activeRole = Acls::getAclByName($activeRoleId); + if ($activeRole === null) { + $activeRole = Acls::getAclByDisplay($activeRoleId); + if ($activeRole !== null) { + $activeRoleId = $activeRole->getName(); + } + } + // Convert the active role into global filters. + MetricExplorer::convertActiveRoleToGlobalFilters($user, $activeRoleId, $queryConfig->global_filters); + + // Store the updated config in the query. + $query['config'] = json_encode($queryConfig); + } + + /** + * This function is just here to allow us to support the original metric explorer html controller urls. Which + * functioned by referencing the same url `/controllers/metric_explorer.php` and including an `operation` parameter + * to differentiate which file to load. Specifically, this function replicates the following controller operations + * ( including the file that this endpoint was ported from ): + * - `get_data`: `html/controllers/metric_explorer/get_data.php` + * - `get_dimension`: `html/controllers/metric_explorer/get_dimension.php` + * - `get_dw_descripter`: `html/controllers/metric_explorer/get_dw_descripter.php` + * - `get_filters`: `html/controllers/metric_explorer/get_filters.php` + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/controllers/metric_explorer.php', methods: ['POST', 'GET'])] + public function index(Request $request): Response + { + $operation = $this->getStringParam($request, 'operation', true); + + try { + switch ($operation) { + case 'get_data': + return $this->getData($request); + case 'get_dimension': + return $this->getDimensionValues($request); + case 'get_dw_descripter': + return $this->getDwDescriptors($request); + case 'get_filters': + return $this->getFilters($request); + case 'get_rawdata': + return $this->getRawData($request); + } + } catch (\Exception $e) { + return $this->json(buildError($e)); + } + + return $this->json([ + 'success' => false, + 'message' => 'Unknown Operation provided.' + ]); + } + + + /** + * + * @param Request $request + * @return Response + * @throws Exception if there is a problem with the processing of the get_data function. + */ + #[Route('{prefix}metrics/explorer/data', requirements: ['prefix' => '.*'], methods: ['POST', 'GET'])] + public function getData(Request $request): Response + { + $user = $this->detectUser($request, [XDUser::INTERNAL_USER, XDUser::PUBLIC_USER]); + + $params = array_merge($request->query->all(), $request->request->all()); + $m = new \DataWarehouse\Access\MetricExplorer($params); + try { + $result = $m->get_data($user); + return new Response($result['results'], 200, $result['headers']); + } catch (Exception $e) { + return $this->json( + [ + 'success' => false, + 'message' => $e->getMessage() + ], + 400 + ); + } + } + + + /** + * + * @param Request $request + * @return Response + * @throws SessionExpiredException + * @throws AccessDeniedException + * @throws UnknownGroupByException + */ + #[Route('{prefix}metrics/explorer/dimension/values', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function getDimensionValues(Request $request): Response + { + try { + $user = $this->tokenHelper->authenticate($request, false); + + // If token authentication failed then fallback to the standard session based authentication method. + if ($user === null) { + $user = $this->detectUser($request, array(\XDUser::PUBLIC_USER)); + } + } catch (Exception $e) { + return $this->json( + buildError(new Exception('Session Expired', 2)), + 401 + ); + } + + $dimensionId = $this->getStringParam($request, 'dimension_id', true); + $offset = $this->getStringParam($request ,'start'); + if (empty($offset)) { + $offset = 0; + } + $limit = $this->getIntParam($request, 'limit'); + $searchText = $this->getStringParam($request, 'search_text'); + + $selectedFilterIds = $this->getStringParam($request, 'selectedFilterIds', false, []); + if (!is_array($selectedFilterIds)) { + $selectedFilterIds = explode(',', $selectedFilterIds); + } + + $realms = $this->getStringParam($request, 'realm', false); + if ($realms !== null) { + $realms = preg_split('/,\s*/', trim($realms), null, PREG_SPLIT_NO_EMPTY); + } + + return $this->json(MetricExplorer::getDimensionValues( + $user, + $dimensionId, + $realms, + $offset, + $limit, + $searchText, + $selectedFilterIds + )); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception if unable to get the currently logged in user. + */ + #[Route('{prefix}metrics/explorer/get_dw_descripter',requirements: ['prefix' => '.*'], methods: ['POST'])] + public function getDwDescriptors(Request $request): Response + { + try { + $user = $this->tokenHelper->authenticate($request, false); + + // If token authentication failed then fallback to the standard session based authentication method. + if ($user === null) { + $user = $this->getLoggedInUser($request->getSession()); + } + } catch (Exception $e) { + return $this->json( + buildError(new Exception('Session Expired', 2)), + 401 + ); + } + + + $roles = $user->getAllRoles(true); + + $roleDescriptors = array(); + foreach ($roles as $activeRole) { + $shortRole = $activeRole; + $us_pos = strpos($shortRole, '_'); + if ($us_pos > 0) { + $shortRole = substr($shortRole, 0, $us_pos); + } + + if (array_key_exists($shortRole, $roleDescriptors)) { + continue; + } + + // If enabled, try to lookup answer in cache first. + $cache_enabled = $this->parameters->get('xdmod.portal_settings.internal.dw_desc_cache') === 'on'; + $cache_data_found = false; + if ($cache_enabled) { + $db = \CCR\DB::factory('database'); + $db->execute('create table if not exists dw_desc_cache (role char(5), response mediumtext, index (role) ) '); + $cachedResults = $db->query('select response from dw_desc_cache where role=:role', array('role' => $shortRole)); + if (count($cachedResults) > 0) { + $roleDescriptors[$shortRole] = unserialize($cachedResults[0]['response']); + $cache_data_found = true; + } + } + + // If the cache was not used or was not useful, get descriptors from code. + if (!$cache_data_found) { + $realms = []; + $realmObjects = Realms::getRealmObjectsForUser($user); + $query_descriptor_realms = Acls::getQueryDescripters($user); + + foreach ($query_descriptor_realms as $query_descriptor_realm => $query_descriptor_groups) { + $category = DataWarehouse::getCategoryForRealm($query_descriptor_realm); + if ($category === null) { + continue; + } + $seenStats = []; + + $realmObject = $realmObjects[$query_descriptor_realm]; + $realmDisplay = $realmObject->getDisplay(); + $realms[$query_descriptor_realm] = [ + 'text' => $query_descriptor_realm, + 'category' => $realmDisplay, + 'dimensions' => [], + 'metrics' => [], + ]; + foreach ($query_descriptor_groups as $query_descriptor_group) { + foreach ($query_descriptor_group as $query_descriptor) { + if ($query_descriptor->getDisableMenu()) { + continue; + } + + $groupByName = $query_descriptor->getGroupByName(); + $group_by_object = $query_descriptor->getGroupByInstance(); + $permittedStatistics = $group_by_object->getRealm()->getStatisticIds(); + + $realms[$query_descriptor_realm]['dimensions'][$groupByName] = [ + 'text' => $groupByName == 'none' ? 'None' : $group_by_object->getName(), + 'info' => $group_by_object->getHtmlDescription() + ]; + + $stats = array_diff($permittedStatistics, $seenStats); + if (empty($stats)) { + continue; + } + + $statsObjects = $query_descriptor->getStatisticsClasses($stats); + foreach ($statsObjects as $realm_group_by_statistic => $statistic_object) { + + if (!$statistic_object->showInMetricCatalog()) { + continue; + } + + $semStatId = \Realm\Realm::getStandardErrorStatisticFromStatistic( + $realm_group_by_statistic + ); + $realms[$query_descriptor_realm]['metrics'][$realm_group_by_statistic] = + [ + 'text' => $statistic_object->getName(), + 'info' => $statistic_object->getHtmlDescription(), + 'std_err' => in_array($semStatId, $permittedStatistics), + 'hidden_groupbys' => $statistic_object->getHiddenGroupBys() + ]; + $seenStats[] = $realm_group_by_statistic; + } + } + } + $texts = []; + foreach ($realms[$query_descriptor_realm]['metrics'] as $key => $row) { + $texts[$key] = $row['text']; + } + array_multisort($texts, SORT_ASC, $realms[$query_descriptor_realm]['metrics']); + } + $texts = []; + foreach ($realms as $key => $row) { + $texts[$key] = $row['text']; + } + array_multisort($texts, SORT_ASC, $realms); + + $roleDescriptors[$shortRole] = ['totalCount' => 1, 'data' => [['realms' => $realms]]]; + + // Cache the results if the cache is enabled. + if ($cache_enabled) { + $db->execute('insert into dw_desc_cache (role, response) values (:role, :response)', [ + 'role' => $shortRole, + 'response' => serialize($roleDescriptors[$shortRole]) + ]); + } + } + } + + $combinedRealmDescriptors = []; + foreach ($roleDescriptors as $roleDescriptor) { + foreach ($roleDescriptor['data'][0]['realms'] as $realm => $realmDescriptor) { + if (!isset($combinedRealmDescriptors[$realm])) { + $combinedRealmDescriptors[$realm] = [ + 'metrics' => [], + 'dimensions' => [], + 'text' => $realmDescriptor['text'], + 'category' => $realmDescriptor['category'], + ]; + } + + $combinedRealmDescriptors[$realm]['metrics'] += $realmDescriptor['metrics']; + $combinedRealmDescriptors[$realm]['dimensions'] += $realmDescriptor['dimensions']; + } + } + + return $this->json([ + 'totalCount' => 1, + 'data' => [ + [ + 'realms' => $combinedRealmDescriptors + ] + ] + ]); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception if unable to retrieve the currently logged in user. + */ + #[Route('{prefix}metrics/explorer/filters', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function getFilters(Request $request): Response + { + try { + $user = $this->getLoggedInUser($request->getSession()); + + $userProfile = $user->getProfile(); + $filters = $userProfile->fetchValue('filters'); + if ($filters != null) { + $filtersArray = json_decode($filters); + $returnData = [ + 'totalCount' => count($filtersArray), + 'message' => 'success', + 'data' => $filtersArray, + 'success' => true + ]; + } else { + $returnData = [ + 'totalCount' => 0, + 'message' => 'success', + 'data' => [], + 'success' => true + ]; + } + + } catch (SessionExpiredException $see) { + // TODO: Refactor generic catch block below to handle specific exceptions, + // which would allow this block to be removed. + throw $see; + } catch (Exception $ex) { + $returnData = [ + 'totalCount' => 0, + 'message' => $ex->getMessage(), + 'data' => [], + 'success' => false + ]; + } + + return $this->json($returnData); + } + + /** + * @param Request $request + * @return Response + * @throws Exception if there is a problem retrieving a user for the request. + */ + #[Route('{prefix}metrics/explorer/raw_data', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function getRawData(Request $request): Response + { + $user = $this->detectUser($request, array(XDUser::INTERNAL_USER, XDUser::PUBLIC_USER)); + + try { + $requestedFormat = $this->getStringParam($request, 'format'); + $format = DataWarehouse\ExportBuilder::validateFormat($requestedFormat, 'jsonstore', ['jsonstore']); + $dataSetId = $this->getStringParam($request, 'datasetId', true); + $datapoint = $this->getStringParam($request, 'datapoint', true); + $requestedStartDate = $this->getDateFromISO8601Param($request, 'start_date', true); + $requestedStartDateTs = date_timestamp_get($requestedStartDate); + + $requestedEndDate = $this->getDateFromISO8601Param($request, 'end_date', true); + $requestedEndDateTs = date_timestamp_get($requestedEndDate); + + + if ($requestedStartDateTs > $requestedEndDateTs) { + throw new BadRequestHttpException('End date must be greater than or equal to start date'); + } + + $startDate = $requestedStartDate->format('Y-m-d'); + $endDate = $requestedEndDate->format('Y-m-d'); + $isTimeseries = $this->getBooleanParam($request, 'timeseries', false, false); + + if ($isTimeseries) { + // For timeseries data the date range is set to be only the data-point that was + // selected. Therefore we adjust the start and end date appropriately + $aggregationUnit = $this->getStringParam($request, 'aggregation_unit', false, 'auto'); + $time_period = TimeAggregationUnit::deriveAggregationUnitName($aggregationUnit, $startDate, $endDate); + $time_point = $datapoint / 1000; + + list($startDate, $endDate) = TimeAggregationUnit::getRawTimePeriod($time_point, $time_period); + } + + $requestedGlobalFilters = $this->getStringParam($request, 'global_filters'); + + $globalFilters = (object)['data' => [], 'total' => 0]; + if (!empty($requestedGlobalFilters)) { + $globalFiltersDecoded = urldecode($requestedGlobalFilters); + $globalFiltersJson = json_decode($globalFiltersDecoded, true); + + if (!empty($globalFiltersJson) && isset($globalFiltersJson['data']) && is_array($globalFiltersJson['data'])) { + foreach ($globalFiltersJson['data'] as $datum) { + $globalFilters->data[] = (object)$datum; + $globalFilters->total++; + } + } + } + + $dataset_classname = '\DataWarehouse\Data\SimpleDataset'; + + try { + $all_data_series = $this->getDataSeries($request); + } catch (Exception $e) { + return $this->json( + [ + 'success' => false, + 'message' => $e->getMessage() + ], + 400 + ); + } + + // find requested dataset. + $data_description = null; + foreach ($all_data_series as $data_description_index => $data_series) { + // NOTE: this only works if the id's are not floats. + if ("{$data_series->id}" == "$dataSetId") { + $data_description = $data_series; + break; + } + } + + if ($data_description === null) { + return $this->json( + [ + 'success'=> false, + 'message' => 'Invalid data_series provided.' + ], + 400 + ); + } + + // Check that the user has at least one role authorized to view this data. + MetricExplorer::checkDataAccess( + $user, + $data_description->realm, + 'none', + $data_description->metric + ); + + if ($format === 'jsonstore') { + + $query_classname = '\\DataWarehouse\\Query\\' . $data_description->realm . '\\RawData'; + + $query = new $query_classname( + $data_description->realm, + 'day', + $startDate, + $endDate, + null, + $data_description->metric, + [] + ); + + $groupedRoleParameters = []; + foreach ($globalFilters->data as $global_filter) { + if ($global_filter->checked == 1) { + if ( + !isset( + $groupedRoleParameters[$global_filter->dimension_id] + ) + ) { + $groupedRoleParameters[$global_filter->dimension_id] + = []; + } + + $groupedRoleParameters[$global_filter->dimension_id][] + = $global_filter->value_id; + } + } + + $query->setMultipleRoleParameters($user->getAllRoles(), $user); + + $query->setRoleParameters($groupedRoleParameters); + + $query->setFilters($data_description->filters); + + $dataset = new $dataset_classname($query); + + $limit = null; + $limitParam = $this->getStringParam($request, 'limit'); + if (!empty($limitParam)) { + try { + $limit = $this->getIntParam($request, 'limit'); + if ($limit < 0) { + $limit = null; + } + } catch (Exception $e) { + // NOOP + } + } + + $offset = 0; + $offsetParam = $this->getStringParam($request, 'start'); + if (!empty($offsetParam)) { + try { + $offset = intval($offsetParam); + } catch (Exception $e) { + // NOOP + } + } + $offset = max($offset, 0); + $totalCount = $dataset->getTotalPossibleCount(); + + $ret = array(); + + // As a small optimization only compute the total count the first time (ie when the offset is 0) + if ($offset === null or $offset == 0) { + $privquery = new $query_classname( + $data_description->realm, + 'day', + $startDate, + $endDate, + null, + $data_description->metric, + array() + ); + $privquery->setRoleParameters($groupedRoleParameters); + $privquery->setFilters($data_description->filters); + + $privdataset = new $dataset_classname($privquery); + + $ret['totalAvailable'] = $privdataset->getTotalPossibleCount(); + } + // This is so that the behavior of this endpoint matches get_rawdata.php + if ($offsetParam === null && !empty($limit)) { + $offset = null; + } + $ret['data'] = $dataset->getResults($limit, $offset,false, false, null, null, $this->logger); + $ret['totalCount'] = $totalCount; + + return $this->json($ret); + } + } catch (SessionExpiredException $see) { + // TODO: Refactor generic catch block below to handle specific exceptions, + // which would allow this block to be removed. + return $this->json(buildError($see)); + } catch (Exception $ex) { + return $this->json(buildError($ex)); + } + + return $this->json([ + 'success' => false, + 'message' => 'An unexpected error has occurred. Please contact support.' + ]); + } + + + private function getDataSeries(Request $request): array + { + $requestedDataSeries = null; + try { + $dataSeriesParam = $this->getStringParam($request, 'data_series', false, '[]'); + $requestedDataSeries = json_decode(urldecode($dataSeriesParam), true); + } catch (Exception $e) { + // NOOP + } + if (is_array($requestedDataSeries) && isset($requestedDataSeries['data']) && is_array($requestedDataSeries['data'])) { + return $this->getDataSeriesFromArray($requestedDataSeries); + } else { + return $this->getDataSeriesFromJsonString($this->getStringParam($request, 'data_series')); + } + } + + private function getDataSeriesFromArray(array $dataSeries): array + { + $results = []; + foreach ($dataSeries['data'] as $datum) { + $y = (object)$datum; + + for ($i = 0, $b = count($y->filters['data']); $i < $b; $i++) { + $y->filters['data'][$i] = (object)$y->filters['data'][$i]; + } + + $y->filters = (object)$y->filters; + + // Set values of new attribs for backward compatibility. + if (empty($y->line_type)) { + $y->line_type = 'Solid'; + } + + if ( + empty($y->line_width) + || !is_numeric($y->line_width) + ) { + $y->line_width = 2; + } + + if (empty($y->color)) { + $y->color = 'auto'; + } + + if (empty($y->shadow)) { + $y->shadow = false; + } + + $results[] = $y; + } + return $results; + } + + /** + * + * @param string $dataSeries + * @return array + */ + private function getDataSeriesFromJsonString(string $dataSeries): array + { + $jsonDataSeries = json_decode(urldecode($dataSeries)); + if (null === $jsonDataSeries) { + throw new BadRequestHttpException('Invalid data_series specified'); + } + foreach ($jsonDataSeries as &$y) { + // Set values of new attribs for backward compatibility. + if (empty($y->line_type)) { + $y->line_type = 'Solid'; + } + + if (empty($y->line_width) || !is_numeric($y->line_width)) { + $y->line_width = 2; + } + + if (empty($y->color)) { + $y->color = 'auto'; + } + + if (empty($y->shadow)) { + $y->shadow = false; + } + } + + return $jsonDataSeries; + } +} diff --git a/src/Controller/OrganizationController.php b/src/Controller/OrganizationController.php new file mode 100644 index 0000000000..d0dc598ad2 --- /dev/null +++ b/src/Controller/OrganizationController.php @@ -0,0 +1,277 @@ +getStringParam($request, 'operation', true); + # Note: this is here so that we get the same error messages for the same tests as previously. + # Once we deprecate the old routes this should go away. + if (in_array($operation, ['upgrade_member', 'downgrade_member'])) { + try { + $user = $this->authorize($request, [ROLE_ID_CENTER_DIRECTOR], true); + } catch (Exception $e) { + return $this->json( + [ + "status" => "not_a_center_director", + "success" => false, + "totalCount" => 0, + "message" => "not_a_center_director", + "data" => [] + ] + ); + } + } + + try { + $memberId = $this->getStringParam($request, 'member_id',false, null, RESTRICTION_UID ); + } catch (Exception $e) { + return $this->json(buildError("Invalid value specified for 'member_id'.")); + } + + if (is_null($memberId)) { + return $this->json(buildError("'member_id' not specified.")); + } + + switch($operation) { + case 'downgrade_member': + return $this->downgradeMember($request, $memberId); + case 'enum_center_staff_members': + return $this->getMembers($request); + case 'get_member_status': + return $this->getMemberStatus($request, $memberId); + case 'upgrade_member': + return $this->upgradeMember($request, $memberId); + } + + return $this->json(buildError('Unknown operation provided.')); + + } + + /** + * Retrieve the other members associated with the requesting user's organization. + * + * + + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}organizations/members', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function getMembers(Request $request): Response + { + $user = $this->authorize($request, $this->getParameter('center_related_acls'), true); + $members = Users::getUsersAssociatedWithCenter($user->getUserID()); + + return $this->json([ + 'success' => true, + 'count' => count($members), + 'members' => $members + ]); + } + + /** + * + * @param Request $request + * @param string $memberId + * @return Response + * @throws Exception + */ + #[Route('{prefix}organizations/members/{memberId}/status', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function getMemberStatus(Request $request, string $memberId): Response + { + $user = $this->authorize($request, $this->getParameter('center_related_acls'), true); + + if (empty($memberId)) { + return $this->json(buildError("Invalid value specified for 'member_id'.")); + } + + $member = XDUser::getUserByID($memberId); + if ($member === null) { + return $this->json(\xd_response\buildError('user_does_not_exist')); + } + + $returnData = [ + 'success' => true, + 'message' => '', + 'eligible' => true + ]; + + $organization = $user->getOrganizationID(); + $memberUserId = $member->getUserID(); + + // An eligible user must be associated with the currently logged in users center. + if (!Users::userIsAssociatedWithCenter($memberUserId, $organization)) { + throw new BadRequestHttpException('center_mismatch_between_member_and_director'); + } + + // They must not already be a Center Director for the organization. + if (Centers::hasCenterRelation($memberUserId, $organization, ROLE_ID_CENTER_DIRECTOR)) { + $returnData['success'] = false; + $returnData['message'] = 'is a Center Director'; + return $this->json($returnData); + } + + // This makes them ineligible for promotion, but eligible for demotion. + if (Centers::hasCenterRelation($memberUserId, $organization, ROLE_ID_CENTER_STAFF)) { + $returnData['eligible'] = false; + } + + // They must be active + if (!$member->getAccountStatus()) { + $returnData['success'] = false; + $returnData['message'] = 'User is disabled'; + return $this->json($returnData); + } + + return $this->json($returnData); + } + + /** + * @param Request $request + * @param string $memberId + * @return Response + * @throws Exception + */ + #[Route('{prefix}organizations/members/{memberId}/upgrade', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function upgradeMember(Request $request, string $memberId): Response + { + $this->logger->error('Upgrading Member Id: ' . var_export($memberId, true)); + try { + $user = $this->authorize($request, [ROLE_ID_CENTER_DIRECTOR], true); + $this->logger->error('Successfully Authenticated requesting user has CD'); + } catch (Exception $e) { + return $this->json( + [ + "status" => "not_a_center_director", + "success" => false, + "totalCount" => 0, + "message" => "not_a_center_director", + "data" => [] + ] + ); + } + $this->logger->error('Checking member id next.'); + if (empty($memberId)) { + return $this->json(buildError("Invalid value specified for 'member_id'.")); + } + $member = XDUser::getUserByID($memberId); + if ($member === null) { + return $this->json(\xd_response\buildError('user_does_not_exist')); + } + $returnData = []; + + // Ensure that the user performing this operation is authorized + if (!$user->hasAcl(ROLE_ID_CENTER_DIRECTOR) || !$user->getAccountStatus()) { + return $this->json([ + 'success' => false, + 'message' => 'You are not authorized to perform this action' + ]); + } + $organization = $user->getActiveOrganization(); + $memberUserId = $member->getUserID(); + + // An eligible user must be associated with the currently logged in users center. + if (!Users::userIsAssociatedWithCenter($memberUserId, $organization)) { + $this->json(\xd_response\buildError('center_mismatch_between_member_and_director')); + } + + // They must not already be a Center Director for the organization. + if (Centers::hasCenterRelation($memberUserId, $organization, ROLE_ID_CENTER_DIRECTOR)) { + $returnData['success'] = false; + $returnData['message'] = 'is a Center Director'; + return $this->json($returnData); + } + + // They must not be a Center Staff for the organization. + // Although this makes them eligible for demotion. + if (Centers::hasCenterRelation($memberUserId, $organization, ROLE_ID_CENTER_STAFF)) { + $returnData['success'] = false; + $returnData['message'] = 'is already a Center Staff'; + return $this->json($returnData); + } + + Users::promoteUserToCenterStaff($member, $organization); + $returnData['success'] = true; + $returnData['message'] = "has been upgraded to Center Staff
(promoted by {$user->getFormalName()})"; + + return $this->json($returnData); + } + + /** + * @param Request $request + * @param ?string $memberId + * @return Response + * @throws Exception + */ + #[Route('{prefix}organizations/members/{memberId}/downgrade', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function downgradeMember(Request $request, ?string $memberId): Response + { + try { + $user = $this->authorize($request, [ROLE_ID_CENTER_DIRECTOR], true); + } catch (Exception $e) { + return $this->json( + [ + "status" => "not_a_center_director", + "success" => false, + "totalCount" => 0, + "message" => "not_a_center_director", + "data" => [] + ] + ); + } + + if (empty($memberId)) { + return $this->json(buildError("Invalid value specified for 'member_id'.")); + } + + try { + $memberId = $this->getStringParam($request, 'member_id', false, null, RESTRICTION_UID); + } catch (Exception $e) { + return $this->json(buildError("Invalid value specified for 'member_id'.")); + } + + $member = XDUser::getUserByID($memberId); + if ($member === null) { + return $this->json(\xd_response\buildError('user_does_not_exist')); + } + + $organization = $user->getOrganizationID(); + $memberUserId = $member->getUserID(); + + // An eligible user must be associated with the currently logged in users center. + if (!Users::userIsAssociatedWithCenter($memberUserId, $organization)) { + return $this->json(\xd_response\buildError('center_mismatch_between_member_and_director')); + } + + Users::demoteUserFromCenterStaff($member, $organization); + + return $this->json(['success' => true]); + } + +} diff --git a/src/Controller/PasswordResetController.php b/src/Controller/PasswordResetController.php new file mode 100644 index 0000000000..106aa72d8c --- /dev/null +++ b/src/Controller/PasswordResetController.php @@ -0,0 +1,69 @@ + '.*'], methods: ['GET'])] + #[Route('/controllers/password_reset.php', methods: ['GET'])] + public function index(Request $request): Response + { + $validationCheck = [ + 'status' => INVALID, + 'user_first_name' => 'INVALID', + 'user_id' => INVALID + ]; + + $mode = $this->getStringParam($request, 'mode', false, 'update'); + if (isset($mode) && $mode === 'new') { + $mode = 'create'; + } + + $rid = $this->getStringParam($request, 'rid', false, null, RESTRICTION_RID); + if (isset($rid)) { + $validationCheck = \XDUser::validateRID($rid); + } + + + if ($validationCheck['status'] === INVALID || !in_array($mode, self::$validModes)) { + return $this->render( + 'twig/password_reset_expired.html.twig', + [ + 'site_address' => $this->parameters->get('xdmod.portal_settings.general.site_address') + ] + ); + } + + return $this->render( + '/twig/password_reset.html.twig', + [ + 'rid' => $rid, + 'mode' => $mode, + 'first_name' => $validationCheck['user_first_name'], + 'password_max' => CHARLIM_PASSWORD, + 'extjs_path' => '/gui/lib', + 'extjs_version' => '/extjs' + ] + ); + } +} diff --git a/src/Controller/PersonController.php b/src/Controller/PersonController.php new file mode 100644 index 0000000000..da42d457cb --- /dev/null +++ b/src/Controller/PersonController.php @@ -0,0 +1,36 @@ + '.*'])] +class PersonController extends BaseController +{ + + /** + * + * @param Request $request + * @param int $id + * @return Response + * @throws \Exception + */ + #[Route('/{id}/organization', requirements: ["id" => "(-)?\d+"], methods: ['GET'])] + public function getOrganizationForPerson(Request $request, int $id): Response + { + $this->authorize($request, ['mgr']); + + return $this->json([ + 'success' => true, + 'results' => [ + 'id' => Organizations::getOrganizationIdForPerson($id) + ] + ]); + } +} diff --git a/src/Controller/ReportBuilderController.php b/src/Controller/ReportBuilderController.php new file mode 100644 index 0000000000..5d5401d7f7 --- /dev/null +++ b/src/Controller/ReportBuilderController.php @@ -0,0 +1,702 @@ +getStringParam($request, 'operation'); + if (empty($operation)) { + return $this->json(buildError('operation_not_defined')); + } + + try { + switch ($operation) { + case 'build_from_template': + $templateId = $this->getStringParam($request, 'template_id'); + return $this->getReportFromTemplate($request, $templateId); + case 'download_report': + return $this->downloadReport($request); + case 'enum_available_charts': + return $this->getAvailableCharts($request); + case 'enum_reports': + return $this->getReports($request); + case 'enum_templates': + return $this->getTemplates($request); + case 'fetch_report_data': + $reportId = $this->getStringParam($request, 'selected_report', true); + return $this->getReportData($request, $reportId); + case 'get_new_report_name': + return $this->getNewReportName($request); + case 'get_preview_data': + return $this->getPreviewData($request); + case 'remove_chart_from_pool': + return $this->removeChartFromPool($request); + case 'remove_report_by_id': + return $this->removeReportsById($request); + case 'save_report': + return $this->saveReport($request); + case 'send_report': + return $this->sendReport($request); + } + } catch(\Exception $e) { + return $this->json(buildError($e)); + } + + return $this->json(buildError('invalid_operation_specified')); + } + + /** + * + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/reports/builder/list', methods: ['GET'])] + public function getReports(Request $request): Response + { + try { + $user = $this->detectUser($request, [XDUser::PUBLIC_USER]); + } catch(Exception $e) { + return $this->json(buildError($e), 401); + } + + $reportManager = new \XDReportManager($user); + + return $this->json([ + 'status' => 'success', + 'queue' => $reportManager->fetchReportTable() + ]); + } + + /** + * + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/reports/builder/charts', methods: ['POST'])] + public function getAvailableCharts(Request $request): Response + { + try { + $user = $this->detectUser($request, [XDUser::PUBLIC_USER]); + } catch(Exception $e) { + return $this->json(buildError($e), 401); + } + + $reportManager = new \XDReportManager($user); + return $this->json([ + 'status' => 'success', + 'queue' => $reportManager->fetchChartPool() + ]); + } + + /** + * + * @param Request $request + * @param string $templateId + * @return Response + * @throws Exception + */ + #[Route('/reports/builder/templates/{templateId}', methods: ['POST'])] + public function getReportFromTemplate(Request $request, string $templateId): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + $template = \XDReportManager::retrieveReportTemplate($user, $templateId); + $parameters = $request->request->all(); + $template->buildReportFromTemplate($parameters); + return $this->json(['success' => true]); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/reports/builder/send', methods: ['POST'])] + public function sendReport(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + $reportManager = new \XDReportManager($user); + + $buildOnly = $this->getBooleanParam($request, 'build_only'); + $reportId = $this->getStringParam($request, 'report_id', false, null, ReportGenerator::REPORT_ID_REGEX); + $exportFormat = $this->getStringParam($request, 'export_format', false, \XDReportManager::DEFAULT_FORMAT); + + $buildResponse = $reportManager->buildReport($reportId, $exportFormat); + $workingDir = $buildResponse['template_path']; + $reportFileName = $buildResponse['report_file']; + $responseData = [ + 'action' => 'send_report', + 'build_only' => $buildOnly + ]; + + if ($buildOnly) { + $responseData['report_loc'] = basename($workingDir); + $responseData['message'] = 'Report built successfully
'; + $responseData['success'] = true; + $responseData['report_name'] = sprintf('%s.%s', $reportManager->getReportName($reportId, true), $exportFormat); + return $this->json($responseData); + } + + $mailStatus = $reportManager->mailReport($reportId, $reportFileName, '', $buildResponse); + $destinationAddress = $reportManager->getReportUserEmailAddress($reportId); + $message = $mailStatus ? sprintf('Report built and sent to
%s', $destinationAddress) : 'Problem mailing the report'; + + return $this->json([ + 'message' => $message, + 'success' => $mailStatus + ]); + } + + /** + * + * @param Request $request + * @param string $reportName populated if calling the old report_build.php/report_name route. + * @return Response + * @throws Exception + */ + #[Route('/reports/builder/download', methods: ['GET'])] + #[Route('/controllers/report_builder.php/{report_name}', methods: ["GET"])] + public function downloadReport(Request $request, string $reportName = ''): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + + $reportLoc = $this->getStringParam($request, 'report_loc'); + if (empty($reportLoc)) { + return $this->json([ + 'success' => false, + 'message' => 'report_loc is a required parameter.' + + ]); + } + $format = $this->getStringParam($request, 'format'); + if (empty($format)) { + return $this->json([ + 'success' => false, + 'message' => 'format is a required parameter.' + ]); + } + + $reportLoc = $this->getStringParam($request, 'report_loc', true, null, ReportGenerator::REPORT_TMPDIR_REGEX); + $format = $this->getStringParam($request, 'format', false, null, ReportGenerator::REPORT_FORMATS_REGEX); + + if (!\XDReportManager::isValidFormat($format)) { + throw new BadRequestHttpException('Invalid format specified'); + } + + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + $reportManager = new \XDReportManager($user); + + $reportId = preg_replace('/(.+)-(.+)-(.+)/', '$1-$2', $reportLoc); + $workingDirectory = sys_get_temp_dir() . '/' . $reportLoc; + + $reportFile = $workingDirectory . '/' . $reportId . '.' . $format; + if (!file_exists($reportFile)) { + throw new BadRequestHttpException('The report you are referring to does not exist.'); + } + + $reportName = $reportManager->getReportName($reportId, true) . '.' . $format; + $headers = [ + 'Content-Type' => \XDReportManager::resolveContentType($format), + 'Content-Disposition' => sprintf('inline;filename="%s"', $reportName) + ]; + $contents = file_get_contents($reportFile); + return new Response($contents, 200, $headers); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/reports/builder/preview', methods: ['POST'])] + public function getPreviewData(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + + $reportId = $this->getStringParam($request, 'report_id', true); + $token = $this->getStringParam($request, 'token', true); + $chartsPerPage = $this->getIntParam($request, 'charts_per_page', true); + + $reportManager = new \XDReportManager($user); + $charts = $reportManager->getPreviewData($reportId, $token, $chartsPerPage); + + return $this->json([ + 'report_id' => $reportId, + 'success' => true, + 'charts' => $charts + ]); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/reports/builder/name', methods: ['POST'])] + public function getNewReportName(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + $reportManager = new \XDReportManager($user); + return $this->json([ + 'success' => true, + 'report_name' => $reportManager->generateUniqueName() + ]); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/reports/builder/save', methods: ['POST'])] + public function saveReport(Request $request): Response + { + $phase = $this->getStringParam($request, 'phase', true, null, '/^create|update$/'); + $map = []; + + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + $reportManager = new \XDReportManager($user); + switch ($phase) { + case 'create': + $reportId = sprintf('%s-%s', $user->getUserID(), time()); + break; + case 'update': + $reportId = $this->getStringParam($request, 'report_id', false, null, ReportGenerator::REPORT_ID_REGEX); + $reportManager->buildBlobMap($reportId, $map); + $reportManager->removeReportCharts($reportId); + break; + } + + $reportName = mb_convert_encoding($this->getStringParam($request, 'report_name', true), ReportGenerator::REPORT_CHAR_ENCODING); + $reportTitle = mb_convert_encoding($this->getStringParam($request, 'report_title', true), ReportGenerator::REPORT_CHAR_ENCODING); + $reportHeader = mb_convert_encoding($this->getStringParam($request, 'report_header', true), ReportGenerator::REPORT_CHAR_ENCODING); + $reportFooter = mb_convert_encoding($this->getStringParam($request, 'report_footer', true), ReportGenerator::REPORT_CHAR_ENCODING); + $reportFormat = $this->getStringParam($request, 'report_format', false, null, ReportGenerator::REPORT_FORMATS_REGEX . 'i'); + $chartsPerPage = max(1, $this->getIntParam($request, 'charts_per_page')); + $reportSchedule = $this->getStringParam($request, 'report_schedule', false, null, ReportGenerator::REPORT_SCHEDULE_REGEX); + $reportDelivery = $this->getStringParam($request, 'report_delivery', false, '', ReportGenerator::REPORT_DELIVERY_REGEX . 'i'); + + $reportManager->configureSelectedReport( + $reportId, + $reportName, + $reportTitle, + $reportHeader, + $reportFooter, + $reportFormat, + $chartsPerPage, + $reportSchedule, + $reportDelivery + ); + + if ($reportManager->isUniqueName($reportName, $reportId) === false) { + throw new BadRequestHttpException('Another report you have created is already using this name.'); + } + + switch ($phase) { + case 'create': + $reportManager->insertThisReport(); + break; + case 'update': + $reportManager->saveThisReport(); + break; + } + + foreach ($request->request->all() as $k => $v) { + if (preg_match('/chart_data_(\d+)/', $k, $m) > 0) { + $order = $m[1]; + + list($chart_id, $chart_title, $chart_drill_details, $chart_date_description, $timeframe_type, $entry_type) = explode(';', $v); + + $chart_title = str_replace('%3B', ';', $chart_title); + $chart_drill_details = str_replace('%3B', ';', $chart_drill_details); + + $cache_ref_variable = 'chart_cacheref_' . $order; + + // Transfer blobs residing in the directory used for temporary + // files into the database as necessary for each chart which + // comprises the report. + $cache_ref = $request->get($cache_ref_variable); + if (isset($cache_ref)) { + $cache_ref = filter_var( + $cache_ref, + FILTER_VALIDATE_REGEXP, + ['options' => ['regexp' => ReportGenerator::CHART_CACHEREF_REGEX]] + ); + + list($start_date, $end_date, $ref, $rank) = explode(';', $cache_ref); + + $location = sys_get_temp_dir() . "/{$ref}_{$rank}_{$start_date}_{$end_date}.png"; + + // Generate chart blob if it doesn't exist. This file should have already been create. + if (!is_file($location)) { + $insertion_rank = [ + 'rank' => $rank, + 'did' => '', + ]; + $this->logger->info('Saving Report', ['volatile', $insertion_rank, $start_date, $end_date]); + $cached_blob = $start_date . ',' . $end_date . ';' + . $reportManager->generateChartBlob('volatile', $insertion_rank, $start_date, $end_date); + } else { + $cached_blob = $start_date . ',' . $end_date . ';' . file_get_contents($location); + } + + $chart_id_found = false; + + foreach ($map as &$e) { + if ($e['chart_id'] == $chart_id) { + $e['image_data'] = $cached_blob; + $chart_id_found = true; + } + } + + if ($chart_id_found === false) { + $map[] = [ + 'chart_id' => $chart_id, + 'image_data' => $cached_blob + ]; + } + } + + $reportManager->saveCharttoReport($reportId, $chart_id, $chart_title, $chart_drill_details, $chart_date_description, $order, $timeframe_type, $entry_type, $map); + } + + } + + return $this->json([ + 'action' => 'save_report', + 'phase' => $phase, + 'report_id' => $reportId, + 'success' => true, + 'status' => 'success' + ]); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/reports/builder/remove', methods: ['POST'])] + public function removeReportsById(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + $reportManager = new \XDReportManager($user); + + $reportIds = explode(';', $this->getStringParam($request, 'selected_report', true)); + foreach ($reportIds as $reportId) { + $reportManager->removeReportCharts($reportId); + $reportManager->removeReportbyID($reportId); + } + + return $this->json([ + 'action' => 'remove_report_by_id', + 'success' => true + ]); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/reports/builder/remove/chart', methods: ['POST'])] + public function removeChartFromPool(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + $reportManager = new \XDReportManager($user); + $responseData = [ + 'action' => 'remove', + 'success' => true, + 'dropped_entries' => [] + ]; + + foreach ($request->request->all() as $k => $v) { + if (preg_match('/^selected_chart_/', $k) == 1) { + + $reportManager->removeChartFromChartPoolByID($v); + if (preg_match('/controller_module=(.+?)&/', $v, $m)) { + + $module_id = $m[1]; + if (!isset($responseData['dropped_entries'][$module_id])) { + $responseData['dropped_entries'][$module_id] = []; + } + $responseData['dropped_entries'][$module_id][] = $v; + } + } + } + + return $this->json($responseData); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/reports/builder/templates', methods: ['GET'])] + public function getTemplates(Request $request): Response + { + try { + $user = $this->getLoggedInUser($request->getSession()); + } catch (Exception $e) { + return $this->json(buildError($e), 401); + } + + + $templates = \XDReportManager::enumerateReportTemplates($user->getRoles()); + // We do not want to show the "Dashboard Tab Reports" + foreach ($templates as $key => $value) { + if ($value['name'] === 'Dashboard Tab Report') { + unset($templates[$key]); + } + } + return $this->json([ + 'status' => 'success', + 'success' => true, + 'templates' => $templates, + 'count' => count($templates) + ]); + } + + /** + * + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/reports/builder/image', methods: ['GET'])] + #[Route('/report_image_renderer.php', name: 'report_image_renderer_legacy', methods: ['GET'])] + public function generateReportImage(Request $request): Response + { + $this->logger->debug('Generating report image'); + + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + + $userId = null; + try { + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + $this->logger->debug('Report image authenticated'); + + $type = $this->getStringParam($request, 'type', true, null, ReportGenerator::REPORT_CHART_TYPE_REGEX); + $ref = $this->getStringParam($request, 'ref', true, null, ReportGenerator::REPORT_CHART_REF_REGEX); + $did = $this->getStringParam($request, 'did', false, '', ReportGenerator::REPORT_CHART_DID_REGEX); + $start = $this->getStringParam($request, 'start', false, null, ReportGenerator::REPORT_DATE_REGEX); + $end = $this->getStringParam($request, 'end', false, null, ReportGenerator::REPORT_DATE_REGEX); + + + switch ($type) { + case 'chart_pool': + case 'volatile': + $this->logger->debug('Report image type is', [$type]); + $numMatches = preg_match('/^(\d+);(\d+)$/', $ref, $matches); + + if ($numMatches === 0) { + throw new Exception('Invalid thumbnail reference set'); + } + + $userId = (int)$matches[1]; + + if (isset($start) && isset($end)) { + $insertionRank = [ + 'rank' => $matches[2], + 'start_date' => $start, + 'end_date' => $end, + 'did' => $did + ]; + } else { + $insertionRank = [ + 'rank' => $matches[2], + 'did' => $did + ]; + } + break; + case 'report': + $numMatches = preg_match('/^((\d+)-(.+));(\d+)$/', $ref, $matches); + + if ($numMatches == 0) { + throw new Exception('Invalid thumbnail reference set'); + } + + $userId = $matches[2]; + $insertionRank = ['report_id' => $matches[1], 'ordering' => $matches[4]]; + break; + case 'cached': + $numMatches = preg_match('/^((\d+)-(.+));(\d+)$/', $ref, $matches); + + if ($numMatches == 0) { + throw new Exception('Invalid thumbnail reference set'); + } + + if (!isset($start) || !isset($end)) { + throw new Exception('Start and end dates not set'); + } + + $validStart = preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $start); + $validEnd = preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $end); + + if (($validStart * $validEnd) == 0) { + throw new Exception('Invalid start and/or end date supplied'); + } + + $userId = $matches[2]; + + $insertionRank = [ + 'report_id' => $matches[1], + 'ordering' => $matches[4], + 'start_date' => $start, + 'end_date' => $end, + ]; + break; + default: + throw new Exception('Invalid thumbnail type value supplied: ' . $request['type']); + } + + if ($userId != $user->getUserID()) { + throw new AccessDeniedHttpException(sprintf('Invalid User Request. Expected %s, Actual: %s', $user->getUserID(), $userId)); + } + + $reportManager = null; + try { + $reportManager = new XDReportManager($user); + } catch (Exception $exception) { + $this->logger->error('Error instantiating XDReportManager', [$exception->getMessage()]); + } + + if (!empty($reportManager)) { + $this->logger->debug('Fetching chart blob', [$type, $insertionRank]); + $blob = $reportManager->fetchChartBlob($type, $insertionRank, null); + $image_data_header = substr($blob, 0, 8); + + if ($image_data_header != "\x89PNG\x0d\x0a\x1a\x0a") { + throw new Exception($blob); + } + + // If the blob is empty, than substitute the image below to be returned to the user. + if (in_array(md5($blob), self::$emptyBlobs)) { + $blob = file_get_contents(dirname(__FILE__) . '/gui/images/report_thumbnail_no_data.png'); + } + + $headers = ['Content-Type' => 'image/png']; + return new Response($blob, 200, $headers); + } else { + $this->logger->error('An error occurred generating the report image.'); + } + + return $this->json(['message' => 'Unable to instantiate report manager'], 500); + } catch (Exception $e) { + /* There used to be some code here that generated a custom image but it didn't actually do anything with + * that image, just threw the exception so I have elected to not include it here. + */ + $uniqueId = uniqid(); + $this->logger->error('Image generation failed!'); + // The message format here is from classes/UniqueException.php + throw new HttpException(500, sprintf('[Unique ID %s] --> %s', $uniqueId, $e->getMessage())); + } + } + + /** + * + * @param Request $request + * @param string $reportId + * @return Response + * @throws Exception + */ + #[Route('/reports/builder/{reportId}', methods: ['GET'])] + public function getReportData(Request $request, string $reportId): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + + $reportManager = new \XDReportManager($user); + + $flushCache = $this->getBooleanParam($request, 'flush_cache'); + $basedOnAnother = $this->getBooleanParam($request, 'based_on_another'); + + if ($flushCache) { + $reportManager->flushReportImageCache(); + } + + $data = $reportManager->loadReportData($reportId); + + if ($basedOnAnother) { + // The report to be retrieved is to be the basis for a new report. + // In this case, overwrite the report_id and report name fields so when it comes time to save this + // report, a new report will be created instead of the original being overwritten / updated. + $data['report_id'] = ''; + $data['general']['name'] = $reportManager->generateUniqueName($data['general']['name']); + } else { + $data['report_id'] = $reportId; + } + + return $this->json([ + 'action' => 'fetch_report_data', + 'success' => true, + 'results' => $data + ]); + } + + /** + * + * + * @param Request $request + * @return Response + */ + #[Route('/img_placeholder.php', methods: ['GET'])] + public function imgPlaceholder(Request $request): Response + { + $filePath = tempnam(sys_get_temp_dir(), 'img-placeholder-'); + $src = imagecreatetruecolor(7, 12); + $background = imagecolorallocate($src, 255, 255, 255); + imagefill($src, 0, 0, $background); + imagepng($src, $filePath); + + return new BinaryFileResponse($filePath, 200, ['Content-Type: image/png']); + } +} diff --git a/src/Controller/ResourceController.php b/src/Controller/ResourceController.php new file mode 100644 index 0000000000..a6e0daa0c2 --- /dev/null +++ b/src/Controller/ResourceController.php @@ -0,0 +1,9 @@ +passwordResetService = $passwordResetService; + } + + #[Route('/controllers/user_auth.php', methods: ["POST"])] + public function index(Request $request): Response + { + $operation = $this->getStringParam($request, 'operation'); + if (empty($operation)) { + return $this->json(buildError('invalid_operation_specified')); + } + + switch ($operation) { + case 'pass_reset': + return $this->requestPasswordReset($request); + default: + return $this->json(buildError('invalid_operation_specified')); + } + } + + /** + * Request a password reset email be sent to a user who has an email corresponding to the one provided. + * + * @param Request $request + * @return Response + * @throws \Exception + */ + private function requestPasswordReset(Request $request): Response + { + $returnData = []; + $email = $this->getEmailParam($request, 'email'); + + if (empty($email)) { + $returnData['status'] = 'invalid_email_address'; + return $this->json($returnData); + }; + + $user_to_email = XDUser::userExistsWithEmailAddress($email, true); + + if ($user_to_email == INVALID) { + $returnData['status'] = 'no_user_mapping'; + return $this->json($returnData); + } + + if ($user_to_email == AMBIGUOUS) { + $returnData['status'] = 'multiple_accounts_mapped'; + return $this->json($returnData); + } + + $user_to_email = XDUser::getUserByID($user_to_email); + try { + $this->passwordResetService->sendPasswordResetEmail($user_to_email); + $returnData['success'] = true; + $returnData['status'] = 'success'; + } catch (\Exception|\Throwable $e) { + $returnData['success'] = false; + $returnData['message'] = $e->getMessage(); + $returnData['status'] = 'failure'; + } + + return $this->json($returnData); + } +} diff --git a/classes/Rest/Controllers/UserControllerProvider.php b/src/Controller/UserController.php similarity index 59% rename from classes/Rest/Controllers/UserControllerProvider.php rename to src/Controller/UserController.php index 27a97f3ce7..31f0461b93 100644 --- a/classes/Rest/Controllers/UserControllerProvider.php +++ b/src/Controller/UserController.php @@ -1,25 +1,26 @@ 'string', + 'last_name' => 'string', + 'email_address' => 'string', + 'password' => 'string', + ]; /** * A mapping of user properties that can come in with a request to @@ -40,85 +41,70 @@ class UserControllerProvider extends BaseControllerProvider * * @var array */ - private static $propertySettingOptions = array( - 'first_name' => array( + private static $propertySettingOptions = [ + 'first_name' => [ 'setter' => 'setFirstName', - ), - 'last_name' => array( + ], + 'last_name' => [ 'setter' => 'setLastName', - ), - 'email_address' => array( + ], + 'email_address' => [ 'setter' => 'setEmailAddress', - ), - 'password' => array( + ], + 'password' => [ 'setter' => 'setPassword', - ), - ); + ], + ]; - /** - * @see BaseControllerProvider::setupRoutes - */ - public function setupRoutes(Application $app, \Silex\ControllerCollection $controller) - { - $root = $this->prefix; - $controller->get("$root/current", '\Rest\Controllers\UserControllerProvider::getCurrentUser'); - $controller->patch("$root/current", '\Rest\Controllers\UserControllerProvider::updateCurrentUser'); - $controller->get("$root/current/api/token", '\Rest\Controllers\UserControllerProvider::getCurrentAPIToken'); - $controller->post("$root/current/api/token", '\Rest\Controllers\UserControllerProvider::createAPIToken'); - $controller->delete("$root/current/api/token", '\Rest\Controllers\UserControllerProvider::revokeAPIToken'); - } /** * Get details for the current user. * - * @param Request $request The request used to make this call. - * @param Application $app The router application. - * @return array Response data containing the following info: - * success: A boolean indicating if the call was successful. - * results: An object containing data about - * the current user. + * @param Request $request + * @return Response + * @throws \Exception */ - public function getCurrentUser(Request $request, Application $app) + #[Route("{prefix}users/current", name: "get_current_user", requirements: ['prefix' => '.*'], methods: ["GET"])] + public function getCurrentUser(Request $request) { - // Ensure that the user is logged in. $this->authorize($request); - // Extract and return the information for the user. - return $app->json(array( + return $this->json([ 'success' => true, - 'results' => $this->extractUserData($this->getUserFromRequest($request)), - )); + 'results' => $this->extractUserData(XDUser::getUserByUserName($this->getUser()->getUserIdentifier())) + ]); } /** * Update details about the current user. * - * @param Request $request The request used to make this call. - * @param Application $app The router application. - * @return array Response data containing the following info: - * success: A boolean indicating if the call was successful. - * message + * @param Request $request + * @return Response + * @throws \Exception if unable to look up an XDUser by the currently logged in user's id. */ - public function updateCurrentUser(Request $request, Application $app) + #[Route("{prefix}users/current", name: "update_current_user", requirements: ['prefix' => '.*'], methods: ["PATCH"])] + public function updateCurrentUser(Request $request) { // Ensure that the user is logged in. + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request); // Attempt to update the user's profile with the given information. $this->updateUser( - $this->getUserFromRequest($request), + XDUser::getUserByUserName($this->getUser()->getUserIdentifier()), $this->extractUserSettableProperties($request) ); // If the last step completed successfully, hide the welcome message // for first-time XSEDE users and return a success message. - $_SESSION['suppress_profile_autoload'] = true; + SessionSingleton::getSession()->set('suppress_profile_autoload', true); - return $app->json(array( + return $this->json([ 'success' => true, - 'message' => 'User profile updated successfully', - )); + 'message' => 'User profile updated successfully' + ]); } /** @@ -126,14 +112,15 @@ public function updateCurrentUser(Request $request, Application $app) * included in the data returned. To receive a successful response from this endpoint a user must fulfill the * following conditions: * - They just have authenticated to XDMoD via one of the supported methods. - * - THey must have an active API Token. + * - They must have an active API Token. + * * * @param Request $request - * @param Application $app - * @return mixed + * @return Response * @throws \Exception */ - public function getCurrentAPIToken(Request $request, Application $app) + #[Route('{prefix}users/current/api/token', requirements: ['prefix' => '.*'], methods: ['GET'])] + public function getCurrentAPIToken(Request $request): Response { $user = $this->authorize($request); @@ -143,11 +130,10 @@ public function getCurrentAPIToken(Request $request, Application $app) $tokenData = $this->getCurrentAPITokenMetaData($user); - return $app->json(array( - 'success' => true, - 'data' => $tokenData - ) - ); + return $this->json([ + 'success' => true, + 'data' => $tokenData + ]); } /** @@ -157,11 +143,11 @@ public function getCurrentAPIToken(Request $request, Application $app) * - They must not have an existing API Token. * * @param Request $request - * @param Application $app * @return Response * @throws \Exception if there is a problem retrieving a database connection. */ - public function createAPIToken(Request $request, Application $app) + #[Route('{prefix}users/current/api/token', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function createAPIToken(Request $request): Response { $user = $this->authorize($request); @@ -169,7 +155,7 @@ public function createAPIToken(Request $request, Application $app) throw new ConflictHttpException('Token already exists.'); } - return $app->json(array( + return $this->json(array( 'success' => true, 'data' => $this->createToken($user) )); @@ -182,12 +168,13 @@ public function createAPIToken(Request $request, Application $app) * - They must have authenticated to XDMoD via one of the supported methods. * - They must have an active API Token * + * * @param Request $request - * @param Application $app * @return Response * @throws \Exception */ - public function revokeAPIToken(Request $request, Application $app) + #[Route('{prefix}users/current/api/token', requirements: ['prefix' => '.*'], methods: ['DELETE'])] + public function revokeAPIToken(Request $request): Response { $user = $this->authorize($request); @@ -198,16 +185,138 @@ public function revokeAPIToken(Request $request, Application $app) // Attempt to revoke the requesting users token. if ($this->revokeToken($user)) { - return $app->json(array( + return $this->json(array( 'success' => true, 'message' => 'Token successfully revoked.' )); } // If the `revokeToken` failed for some reason then we let the user know. - throw new Exception('Unable to revoke API token.'); + throw new \Exception('Unable to revoke API token.'); } + /** + * This function is just here to allow us to support the original html controller urls. + * + * @param Request $request + * @return Response + * @throws \Exception + */ + #[Route('/controllers/sab_user.php', name: 'list_users_legacy', methods: ["GET", "POST"])] + public function sabUser(Request $request): Response + { + $operation = $this->getStringParam($request, 'operation'); + + return match ($operation) { + 'enum_tg_users' => $this->listUsers($request), + + default => $this->json([ + 'status' => 'invalid_operation_specified', + 'success' => false, + 'totalCount' => 0, + 'message' => 'invalid_operation_specified', + 'data' => [] + ]), + }; + } + + /** + * This function is a port of `html/controllers/sab_users/enum_tg_users.php`. + * + * It's here as opposed to a SABUserController because the other two `sab_user` operations are not used. + * + * @param Request $request + * @return Response + * @throws \Exception + */ + private function listUsers(Request $request): Response + { + // Users must be authenticated before accessing this endpoint. + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + + // Retrieve / handle the required paramters first + try { + $start = $this->getIntParam($request, 'start', true); + $limit = $this->getIntParam($request, 'limit', true); + $searchMode = $this->getStringParam($request, 'search_mode', true, null, RESTRICTION_SEARCH_MODE); + $piOnly = $this->getStringParam($request, 'pi_only', true, null, RESTRICTION_YES_NO); + } catch (\Exception $e) { + return $this->json([ + 'success' => false, + 'status' => 'invalid_params_specified', + 'message' => 'invalid_params_specified', + 'total_user_count' => 0 + ]); + } + + // Retrieve the optional parameters + $nameFilter = $this->getStringParam($request, 'query'); + $userManagement = $this->getBooleanParam($request, 'userManagement'); + + // Retrieve an XDUser for the currently authenticated user. + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + + // Potentially retrieve this users associated provider if the user is a Campus Champion + $universityId = null; + if ($user->hasAcl(ROLE_ID_CAMPUS_CHAMPION) && !isset($userManagement)) { + $universityId = Acls::getDescriptorParamValue($user, ROLE_ID_CAMPUS_CHAMPION, 'provider'); + } + + $searchMethod = null; + switch ($searchMode) { + case 'formal_name': + $searchMethod = FORMAL_NAME_SEARCH; + break; + case 'username': + $searchMethod = USERNAME_SEARCH; + break; + default: + return $this->json(buildError('Unknown search method')); + } + + $dataWarehouse = new XDWarehouse(); + list($userCount, $users) = $dataWarehouse->enumerateGridUsers( + $searchMethod, + $start, + $limit, + $nameFilter, + $piOnly, + $universityId + ); + + $entryId = 0; + $userEntries = []; + foreach ($users as $currentUser) { + $entryId++; + + if ($searchMethod === FORMAL_NAME_SEARCH) { + $personName = $currentUser['long_name']; + $personID = $currentUser['id']; + } elseif ($searchMethod === USERNAME_SEARCH) { + $personName = $currentUser['absusername']; + + // Append the absusername to the id so that each entry is guaranteed + // to have a unique identifier (needed for dependent ExtJS combobox + // (TGUserDropDown.js) to work properly regarding selections). + $personID = $currentUser['id'] . ';' . $currentUser['absusername']; + } + $userEntries[] = [ + 'id' => $entryId, + 'person_id' => $personID, + 'person_name' => $personName + ]; + } + + return $this->json([ + 'success' => true, + 'status' => 'success', + 'message' => 'success', + 'total_user_count' => $userCount, + 'users' => $userEntries + ]); + } + + /** * Extract information from a user object. * @@ -215,6 +324,7 @@ public function revokeAPIToken(Request $request, Application $app) * * @param XDUser $user The user object to extract data from. * @return array An associative array of data for the user. + * @throws \Exception */ private function extractUserData(XDUser $user) { @@ -236,19 +346,19 @@ function ($item) { $rawRealmConfig ); - return array( + return [ 'first_name' => $user->getFirstName(), 'last_name' => $user->getLastName(), 'email_address' => $emailAddress, 'is_sso_user' => $user->isSSOUser(), 'first_time_login' => $user->getCreationTimestamp() == $user->getLastLoginTimestamp(), - 'autoload_suppression' => isset($_SESSION['suppress_profile_autoload']), + 'autoload_suppression' => SessionSingleton::getSession()->get('suppress_profile_autoload', false), 'field_of_science' => $user->getFieldOfScience(), 'active_role' => $mostPrivilegedFormalName, 'most_privileged_role' => $mostPrivilegedFormalName, 'person_id' => $user->getPersonID(true), 'raw_data_allowed_realms' => $rawDataRealms - ); + ]; } /** @@ -261,12 +371,22 @@ function ($item) { private function extractUserSettableProperties(Request $request) { $requestProperties = array(); - foreach (self::$userSettableProperties as $propertyName) { - $propertyValue = $this->getStringParam($request, $propertyName); + foreach (self::$userSettableProperties as $propertyName => $propertyType) { + $propertyValue = $request->get($propertyName); if ($propertyValue === null) { continue; } + + // Check to make sure that the property value type is what we expect. + if (get_debug_type($propertyValue) !== $propertyType) { + throw new BadRequestHttpException( + sprintf( + "Invalid value for $propertyName. Must be a(n) %s.", + $propertyType + ) + ); + } $requestProperties[$propertyName] = $propertyValue; } return $requestProperties; @@ -279,14 +399,13 @@ private function extractUserSettableProperties(Request $request) * @param array $updatedProperties A mapping of properties to update * to their new values. * - * @throws Exception The new property values failed to save. + * @throws \Exception The new property values failed to save. */ private function updateUser(XDUser $user, array $updatedProperties) { // For each property that can be set, check if it is included in the // given set of properties. If so, invoke that property's setter on the // given user with the given property value. - $userType = $user->getUserType(); foreach ($updatedProperties as $propertyName => $propertyValue) { if (!array_key_exists($propertyName, self::$propertySettingOptions)) { continue; @@ -386,7 +505,7 @@ private function createToken(XDUser $user) $hash = password_hash($password, PASSWORD_DEFAULT, array('cost' => 12)); $createdOn = date_create()->format('Y-m-d H:m:s'); - $expirationInterval = \xd_utilities\getConfiguration('api_token', 'expiration_interval'); + $expirationInterval = $this->parameters->get('xdmod.portal_settings.api_token.expiration_interval'); if (empty($expirationInterval)) { throw new \Exception('Expiration Interval not provided.'); } @@ -396,19 +515,19 @@ private function createToken(XDUser $user) $result = $db->execute( $query, array( - ':user_id' => $user->getUserID(), - ':token' => $hash, + ':user_id' => $user->getUserID(), + ':token' => $hash, ':created_on' => $createdOn, ':expires_on' => $expirationDate ) ); - if ($result != 1) { + if ($result !== 1) { throw new \Exception('Unable to create a new API token.'); } return array( - 'token' => sprintf('%s.%s', $user->getUserID(), $password), + 'token' => sprintf('%s.%s', $user->getUserID(), $password), 'expiration_date' => $expirationDate, ); } diff --git a/src/Controller/UserInterfaceController.php b/src/Controller/UserInterfaceController.php new file mode 100644 index 0000000000..abb0d3b40a --- /dev/null +++ b/src/Controller/UserInterfaceController.php @@ -0,0 +1,464 @@ +getStringParam($request, 'operation'); + if (empty($operation)) { + return $this->json(buildError('operation_not_defined')); + } + + try { + switch ($operation) { + case 'get_charts': + return $this->getCharts($request); + case 'get_data': + return $this->getData($request); + case 'get_menus': + return $this->getMenus($request); + case 'get_param_descriptions': + return $this->getParamDescriptions($request); + case 'get_tabs': + return $this->getTabs($request); + } + } catch (\Exception $e) { + return $this->json(buildError($e)); + } + return $this->json(buildError('invalid_operation_specified')); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}interfaces/user/tabs', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function getTabs(Request $request): Response + { + $user = $this->getXDUser($request->getSession()); + + $tabs = Tabs::getTabs($user); + + $results = []; + foreach ($tabs as $tab) { + $results[] = [ + 'tab' => $tab['name'], + 'isDefault' => isset($tab['default']) ? $tab['default'] : false, + 'title' => $tab['title'], + 'pos' => $tab['position'], + 'permitted_modules' => isset($tab['permitted_modules']) ? $tab['permitted_modules'] : null, + 'javascriptClass' => $tab['javascriptClass'], + 'javascriptReference' => $tab['javascriptReference'], + 'tooltip' => isset($tab['tooltip']) ? $tab['tooltip'] : '', + 'userManualSectionName' => $tab['userManualSectionName'], + ]; + } + // Sort tabs + usort( + $results, + function ($a, $b) { + return ($a['pos'] < $b['pos']) ? -1 : 1; + } + ); + + return $this->json([ + 'success' => true, + 'totalCount' => 1, + 'message' => '', + 'data' => [ + ['tabs' => json_encode(array_values($results))] + ] + ]); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}interfaces/user/charts', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function getCharts(Request $request): Response + { + $this->logger->debug('Calling Get Charts'); + try { + $user = $this->tokenHelper->authenticate($request, false); + + // If token authentication failed then fallback to the standard session based authentication method. + if ($user === null) { + $user = $this->getXDUser($request->getSession()); + } + } catch (Exception $e) { + return $this->json( + buildError(new Exception('Session Expired', 2)), + 401 + ); + } + + $allowPublicUser = $request->get('public_user', false); + if ($user->isPublicUser() && !$allowPublicUser) { + return $this->json(buildError(new Exception('Session Expired', 2)), 401); + } + + // Send the request and user to the Usage-to-Metric Explorer adapter. + $this->logger->debug('Instantiating Usage Object'); + $usageAdapter = new Usage($request->request->all()); + + $this->logger->debug('Calling Usage->getCharts'); + + try { + $chartResponse = $usageAdapter->getCharts($user); + } catch (Exception $e) { + $message = $e->getMessage(); + $statusCode = 400; + if (str_starts_with($message, 'Your user account does not have permission to view the requested data.')) { + $statusCode = 403; + } elseif ($message === 'One or more realms must be specified.') { + $statusCode = 500; + } + return $this->json(buildError($e), $statusCode); + } + + $newHeaders = []; + foreach ($chartResponse['headers'] as $headerName => $headerValue) { + $newHeaders [] = sprintf('%s: %s', $headerName, $headerValue); + } + + $format = $this->getStringParam($request, 'format'); + $this->logger->debug(sprintf('Requested Format %s', var_export($format, true))); + if (isset($format)) { + switch ($format) { + case 'pdf': + $newHeaders['Content-Type'] = 'application/pdf'; + break; + case 'png': + $newHeaders['Content-Type'] = 'image/png'; + break; + case 'csv': + $newHeaders['Content-Type'] = 'application/xls'; + break; + case 'svg': + $newHeaders['Content-Type'] = 'image/svg+xml'; + break; + case 'xml': + $newHeaders['Content-Type'] = 'text/xml;charset=UTF-8'; + break; + } + } + $this->logger->debug(sprintf('Adding Headers: %s', var_export($newHeaders, true))); + + return new Response($chartResponse['results'], 200, $newHeaders); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}interfaces/user/data', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function getData(Request $request): Response + { + $this->logger->debug('GetData Called'); + return $this->getCharts($request); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}interfaces/user/menus', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function getMenus(Request $request): Response + { + $returnData = []; + + $user = $this->getXDUser($request->getSession()); + + $node = $this->getStringParam($request, 'node'); + $this->logger->debug('Getting Menus for ', [$node]); + if (isset($node) && $node === 'realms') { + $this->logger->debug('Getting Menus for realms'); + $queryGroupName = $this->getStringParam($request, 'query_group', false, 'tg_usage'); + + $realms = Realms::getRealmsForUser($user); + + foreach ($realms as $realm) { + $returnData[] = [ + 'text' => $realm, + 'id' => 'realm_' . $realm, + 'realm' => $realm, + 'query_group' => $queryGroupName, + 'node_type' => 'realm', + 'iconCls' => 'realm', + 'description' => $realm, + 'leaf' => false, + ]; + } + } elseif (isset($node) && \xd_utilities\string_begins_with($node, 'category_')) { + $this->logger->debug('Getting Menus for category_'); + $queryGroupName = $this->getStringParam($request, 'query_group', false, 'tg_usage'); + + // Get the categories ( realms ) that XDMoD knows about. + $categories = DataWarehouse::getCategories(); + + // Retrieve the realms that the user has access to + $realms = Realms::getRealmIdsForUser($user); + + // Filter the categories by those that the user has access to. + $categories = array_map(function ($category) use ($realms) { + return array_filter($category, function ($realm) use ($realms) { + return in_array($realm, $realms); + }); + }, $categories); + $categories = array_filter($categories); + + // Ensure the categories are sorted as the realms were. + $categoryRealmIndices = []; + foreach ($categories as $categoryName => $category) { + foreach ($category as $realm) { + $realmIndex = array_search($realm, $realms); + if ( + !isset($categoryRealmIndices[$categoryName]) + || $categoryRealmIndices[$categoryName] > $realmIndex + ) { + $categoryRealmIndices[$categoryName] = $realmIndex; + } + } + } + array_multisort($categoryRealmIndices, $categories); + + // If the user requested certain categories, ensure those categories + // are valid. + $category = $this->getStringParam($request, 'category'); + if (isset($category)) { + $requestedCategories = explode(',', $category); + $missingCategories = array_diff($requestedCategories, array_keys($categories)); + if (!empty($missingCategories)) { + throw new Exception("Invalid categories: " . implode(', ', $missingCategories)); + } + $categories = array_map(function ($categoryName) use ($categories) { + return $categories[$categoryName]; + }, $requestedCategories); + } + + foreach ($categories as $categoryName => $category) { + $hasItems = false; + $categoryReturnData = []; + foreach ($category as $realm_name) { + + // retrieve the query descripters this user is authorized to view for this realm. + $queryDescriptorGroups = Acls::getQueryDescripters( + $user, + $realm_name + ); + foreach ($queryDescriptorGroups as $groupByName => $queryDescriptorData) { + $queryDescriptor = $queryDescriptorData['all']; + + if ($queryDescriptor->getShowMenu() !== true) { + continue; + } + + $nodeId = ( + 'node=group_by&realm=' + . $categoryName + . '&group_by=' + . $queryDescriptor->getGroupByName() + ); + + // Make sure that the nodeText, derived from the query descripters menu + // label, has each instance of $realm_name replaced with $categoryName. + $nodeText = preg_replace( + '/' . preg_quote($realm_name, '/') . '/', + $categoryName, + $queryDescriptor->getMenuLabel() + ); + + // If this $nodeId has been seen before but for a different realm. Update + // the list of realms associated with this $nodeId + $nodeRealms = ( + isset($categoryReturnData[$nodeId]) + ? $categoryReturnData[$nodeId]['realm'] . ",{$realm_name}" + : $realm_name + ); + + $categoryReturnData[$nodeId] = [ + 'text' => $nodeText, + 'id' => $nodeId, + 'group_by' => $queryDescriptor->getGroupByName(), + 'query_group' => $queryGroupName, + 'category' => $categoryName, + 'realm' => $nodeRealms, + 'defaultChartSettings' => $queryDescriptor->getChartSettings(true), + 'chartSettings' => $queryDescriptor->getChartSettings(true), + 'node_type' => 'group_by', + 'iconCls' => 'menu', + 'description' => $queryDescriptor->getGroupByLabel(), + 'leaf' => false + ]; + + $hasItems = true; + } + } + + if ($hasItems) { + $returnData = array_merge( + $returnData, + array_values($categoryReturnData) + ); + + $returnData[] = [ + 'text' => '', + 'id' => '-111', + 'node_type' => 'separator', + 'iconCls' => 'blank', + 'leaf' => true, + 'disabled' => true + ]; + } + } + } elseif ( + isset($node) + && substr($node, 0, 13) == 'node=group_by' + ) { + $this->logger->debug('Getting Menus for group_by'); + $category = $this->getStringParam($request, 'category'); + if ($category) { + $categoryName = $category; + $groupByName = $this->getStringParam($request, 'group_by'); + if (isset($groupByName)) { + $queryGroupName = $this->getStringParam($request, 'query_group', false, 'tg_usage'); + + // Get the categories. If the requested one does not exist, + // throw an exception. + $categories = DataWarehouse::getCategories(); + if (!isset($categories[$categoryName])) { + throw new Exception('Category not found.'); + } + + foreach ($categories[$categoryName] as $realm_name) { + $queryDescriptor = Acls::getQueryDescripters($user, $realm_name, $groupByName); + if (empty($queryDescriptor)) { + continue; + } + + $group_by = $queryDescriptor->getGroupByInstance(); + + foreach ($queryDescriptor->getPermittedStatistics() as $realm_group_by_statistic) { + $statistic = $queryDescriptor->getStatistic($realm_group_by_statistic); + + if (!$statistic->showInMetricCatalog()) { + continue; + } + + $statName = $statistic->getId(); + $chartSettings = $queryDescriptor->getChartSettings(); + if (!$statistic->usesTimePeriodTablesForAggregate()) { + $chartSettingsArray = json_decode($chartSettings, true); + $chartSettingsArray['dataset_type'] = 'timeseries'; + $chartSettingsArray['display_type'] = 'line'; + $chartSettingsArray['swap_xy'] = false; + $chartSettings = json_encode($chartSettingsArray); + } + $returnData[] = [ + 'text' => $statistic->getName(false), + 'id' => 'node=statistic&realm=' + . $realm_name + . '&group_by=' + . $groupByName + . '&statistic=' + . $statName, + 'statistic' => $statName, + 'group_by' => $groupByName, + 'group_by_label' => $group_by->getName(), + 'query_group' => $queryGroupName, + 'category' => $categoryName, + 'realm' => $realm_name, + 'defaultChartSettings' => $chartSettings, + 'chartSettings' => $chartSettings, + 'node_type' => 'statistic', + 'iconCls' => 'chart', + 'description' => $statName, + 'leaf' => true, + 'supportsAggregate' => $statistic->usesTimePeriodTablesForAggregate() + ]; + } + } + + if (empty($returnData)) { + throw new Exception('Category not found.'); + } + + $texts = []; + foreach ($returnData as $key => $row) { + $texts[$key] = $row['text']; + } + array_multisort($texts, SORT_ASC, $returnData); + } + } + } + + return $this->json($returnData); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}interfaces/userparameters/descriptions', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function getParamDescriptions(Request $request): Response + { + $user = $this->getXDUser($request->getSession()); + + $queryBuilder = DataWarehouse\QueryBuilder::getInstance(); + $requestParams = $request->request->all(); + $parameterDescriptions = $queryBuilder->pullQueryParameterDescriptionsFromRequest($requestParams, $user); + + $keyValueParamDescriptions = []; + foreach ($parameterDescriptions as $param_desc) { + $kv = explode('=', $param_desc); + $keyValueParamDescriptions[] = ['key' => trim($kv[0], ' '), 'value' => trim($kv[1], ' ')]; + } + + return $this->json([ + 'totalCount' => count($keyValueParamDescriptions), + 'success' => true, + 'message' => 'success', + 'data' => $keyValueParamDescriptions + ]); + } + + +} diff --git a/classes/Rest/Controllers/WarehouseControllerProvider.php b/src/Controller/WarehouseController.php similarity index 66% rename from classes/Rest/Controllers/WarehouseControllerProvider.php rename to src/Controller/WarehouseController.php index 15251230c4..92edf35543 100644 --- a/classes/Rest/Controllers/WarehouseControllerProvider.php +++ b/src/Controller/WarehouseController.php @@ -1,70 +1,54 @@ array( "infoid" => \DataWarehouse\Query\RawQueryTypes::ACCOUNTING, @@ -215,133 +176,6 @@ class WarehouseControllerProvider extends BaseControllerProvider ) ); - /** - * This function is responsible for the setting up of any routes that this - * ControllerProvider is going to be managing. It *must* be overridden by - * a child class. - * - * @param Application $app - * @param ControllerCollection $controller - * @return null - */ - public function setupRoutes(Application $app, ControllerCollection $controller) - { - $root = $this->prefix; - - $current = get_class($this); - $conversions = '\Rest\Utilities\Conversions'; - // Search history routes - - $controller - ->get("$root/search/history", "$current::searchHistory"); - - $controller - ->post("$root/search/history", "$current::createHistory"); - - $controller - ->get("$root/search/history/{id}", "$current::getHistoryById") - ->assert('id', '\d+') - ->convert('id', "$conversions::toInt"); - - $controller - ->post("$root/search/history/{id}", "$current::updateHistory") - ->assert('id', '\d+') - ->convert('id', "$conversions::toInt"); - - $controller - ->put("$root/search/history/{id}", "$current::updateHistory") - ->assert('id', '\d+') - ->convert('id', "$conversions::toInt"); - - $controller - ->delete("$root/search/history/{id}", "$current::deleteHistory") - ->assert('id', '\d+') - ->convert('id', "$conversions::toInt"); - - $controller - ->delete("$root/search/history", "$current::deleteAllHistory"); - - // Job search routes - - $controller - ->get("$root/search/jobs", "$current::searchJobs"); - - $controller - ->get("$root/search/jobs/{action}", "$current::searchJobsByAction") - ->assert('action', '(\w|_|-])+') - ->convert('action', "$conversions::toString"); - $controller - ->post("$root/search/jobs/{action}", "$current::searchJobsByAction") - ->assert('action', '(\w|_|-])+') - ->convert('action', "$conversions::toString"); - - $controller - ->get("$root/search/cloud/{action}", "$current::searchJobsByAction") - ->assert('action', '(\w|_|-])+') - ->convert('action', "$conversions::toString"); - $controller - ->post("$root/search/cloud/{action}", "$current::searchJobsByAction") - ->assert('action', '(\w|_|-])+') - ->convert('action', "$conversions::toString"); - - // Metrics routes - $controller - ->get("$root/resources", "$current::getResources"); - - $controller - ->get("$root/realms", "$current::getRealms"); - - $controller - ->get("$root/dimensions", "$current::getDimensions"); - - $controller - ->get("$root/dimensions/{dimension}", "$current::getDimensionValues") - ->assert('dimension', '(\w|_|-])+') - ->convert('dimension', "$conversions::toString"); - - $controller - ->get("$root/dimensions/{dimensionId}/name", "$current::getDimensionName") - ->assert('dimensionId', '(\w|_|-])+') - ->convert('dimensionId', "$conversions::toString"); - - $controller - ->get("$root/dimensions/{dimensionId}/values/{valueId}/name", "$current::getDimensionValueName") - ->assert('dimension', '(\w|_|-])+') - ->convert('dimension', "$conversions::toString"); - - $controller - ->get("$root/quick_filters", "$current::getQuickFilters"); - - $controller - ->get("$root/aggregation_units", "$current::getAggregationUnits"); - - $controller - ->get("$root/datasets/types", "$current::getDatasetTypes"); - - $controller - ->get("$root/datasets/output_formats", "$current::getDatasetOutputFormats"); - - $controller - ->get("$root/datasets", "$current::getDatasets"); - - $controller->get("$root/aggregatedata", "$current::getAggregateData"); - - $controller - ->get("$root/plots/formats/output", "$current::getPlotOutputFormats"); - - $controller - ->get("$root/plots/types/display", "$current::getPlotDisplayTypes"); - - $controller - ->get("$root/plots/types/combine", "$current::getPlotCombineTypes"); - - $controller - ->get("$root/plots", "$current::getPlots"); - - $controller - ->get("$root/raw-data", "$current::getRawData"); - } - /** * Retrieves the Search History for the user making the request. * If the user was authenticated but no user object could be obtained @@ -363,15 +197,15 @@ public function setupRoutes(Application $app, ControllerCollection $controller) * } * * @param Request $request - * @param Application $app - * @return array in the format array( boolean success, string message) - * @throws AccessDeniedException + * @return Response + * @throws AccessDeniedHttpException * @throws BadRequestHttpException * @throws NotFoundHttpException */ - public function searchHistory(Request $request, Application $app) + #[Route('/warehouse/search/history', methods: ['GET'])] + #[Route('{prefix}warehouse/search/history', requirements: ['prefix' => '.*'], methods: ['GET'])] + public function searchHistory(Request $request): Response { - $action = 'searchHistory'; $user = $this->authorize($request); @@ -384,48 +218,50 @@ public function searchHistory(Request $request, Application $app) $title = $this->getStringParam($request, 'title'); if ($nodeId !== null && $tsId !== null && $infoId !== null && $jobId !== null && $recordId !== null && $realm !== null) { - $result = $this->processJobNodeTimeSeriesRequest($app, $user, $realm, $jobId, $tsId, $nodeId, $infoId, $action); + $result = $this->processJobNodeTimeSeriesRequest($user, $realm, $jobId, $tsId, $nodeId, $infoId, $action); } elseif ($tsId !== null && $infoId !== null && $jobId !== null && $recordId !== null && $realm !== null) { - $result = $this->processJobTimeSeriesRequest($app, $user, $realm, $jobId, $tsId, $infoId, $action); + $result = $this->processJobTimeSeriesRequest($user, $realm, $jobId, $tsId, $infoId, $action); } elseif ($infoId !== null && $jobId !== null && $recordId !== null && $realm !== null) { - $result = $this->processJobRequest($app, $user, $realm, $jobId, $infoId, $action); + $result = $this->processJobRequest($user, $realm, $jobId, $infoId, $action); } elseif ($jobId !== null && $recordId !== null && $realm !== null) { - $result = $this->processJobByJobId($app, $user, $realm, $jobId, $action); + $result = $this->processJobByJobId($user, $realm, $jobId, $action); } elseif ($recordId !== null && $realm !== null) { - $result = $this->getHistoryById($request, $app, $recordId); + $result = $this->getHistoryById($request, $recordId); } elseif ($realm !== null && $title !== null) { - $result = $this->getHistoryByTitle($request, $app, $realm, $title); + $result = $this->getHistoryByTitle($user, $realm, $title); } elseif ($realm !== null) { - $result = $this->processHistoryRequest($app, $user, $realm, $action); + $result = $this->processHistoryRequest($user, $realm, $action); } else { - $result = $this->processHistoryDefaultRealmRequest($app, $user, $action); + $result = $this->processHistoryDefaultRealmRequest($user, $action); } return $result; } /** - * Attempts to retrieve the Search History record identified by the - * provided 'id' + * Attempts to retrieve the Search History record identified by the + * provided 'id' + * + * Example Response: + * { + * 'success': , + * 'action' : 'getHistoryById', + * 'results': [ + * { + * ... search history data ... + * } + * ], + * } * - * Example Response: - * { - * 'success': , - * 'action' : 'getHistoryById', - * 'results': [ - * { - * ... search history data ... - * } - * ], - * } - * - * @param Request $request that will be used to complete the operation. - * @param Application $app that will be used to complete the operation. + * @param Request $request * @param int $id of the Search History record to be retrieved. - * @return \Symfony\Component\HttpFoundation\JsonResponse - * @throws AccessDeniedException + * @return Response + * + * @throws UnauthorizedHttpException|AccessDeniedHttpException|Exception */ - public function getHistoryById(Request $request, Application $app, $id) + #[Route('/warehouse/search/history/{id}', requirements: ["id" => "\d+"], methods: ['GET'])] + #[Route('{prefix}warehouse/search/history/{id}', requirements: ["id" => "\d+", 'prefix' => '.*'], methods: ['GET'])] + public function getHistoryById(Request $request, int $id): Response { $action = 'getHistoryById'; @@ -449,17 +285,18 @@ public function getHistoryById(Request $request, Application $app, $id) $record['success'] = true; $record['action'] = $action; - $results = $app->json($record); - - return $results; + return $this->json($record); } - public function getHistoryByTitle(Request $request, Application $app, $realm, $title) + /** + * @param XDUser $user + * @param string $realm + * @param string $title + * @return Response + */ + private function getHistoryByTitle(XDUser $user, string $realm, string $title): Response { $action = 'getHistoryByTitle'; - - $user = $this->getUserFromRequest($request); - $userHistory = $this->getUserStore($user, $realm); $searches = $userHistory->get(); foreach ($searches as $search) { @@ -468,7 +305,7 @@ public function getHistoryByTitle(Request $request, Application $app, $realm, $t if (!isset($search['dtype'])) { $search['dtype'] = 'recordid'; } - return $app->json( + return $this->json( array( 'action' => $action, 'success' => true, @@ -476,11 +313,10 @@ public function getHistoryByTitle(Request $request, Application $app, $realm, $t ), 200 ); - break; } } - throw new NotFoundHttpException(); + throw new NotFoundHttpException(''); } /** @@ -488,16 +324,16 @@ public function getHistoryByTitle(Request $request, Application $app, $realm, $t * throws and exception if the parameters are missing. * @param Request $request The request. * @return array decoded search parameters. - * @throws BadRequestHttpException If the required 'data' parameter is - * absent. + * @throws MissingMandatoryParametersException If the required parameters are absent. + * @throws BadRequestHttpException if `data.text` is not present. */ - private function getSearchParams(Request $request) + private function getSearchParams(Request $request): array { $data = $this->getStringParam($request, 'data', true); $decoded = json_decode($data, true); - if ($decoded === null || !isset($decoded['text']) ) { + if ($decoded === null || !isset($decoded['text'])) { throw new BadRequestHttpException( 'Malformed request. Expected \'data.text\' to be present.' ); @@ -510,39 +346,36 @@ private function getSearchParams(Request $request) /** * Attempt to create a new Search History record with the provided 'data' - * form parameter. + * form parameter. * - * @param Request $request that will be used to complete the requested operation - * @param Application $app that will be used to complete the requested operation - * @return \Symfony\Component\HttpFoundation\JsonResponse - * @throws AccessDeniedException + * @param Request $request + * @return Response + * @throws AccessDeniedHttpException * @throws BadRequestHttpException + * @throws \Exception */ - public function createHistory(Request $request, Application $app) + #[Route('/warehouse/search/history', methods: ['POST'])] + #[Route('{prefix}warehouse/search/history', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function createHistory(Request $request): Response { $action = 'createHistory'; - - $user = $this->authorize($request); - $realm = $this->getStringParam($request, 'realm', true); $history = $this->getUserStore($user, $realm); - $decoded = $this->getSearchParams($request); $recordId = $this->getIntParam($request, 'recordid'); - $created = is_numeric($recordId) ? $history->upsert($recordId, $decoded) : $history->insert($decoded); - if ($created == null) { + if ($created === null) { throw new BadRequestHttpException( "Create request will exceed record storage restrictions " . "(record count limited to " . - WarehouseControllerProvider::_MAX_RECORDS . ")" + self::MAX_RECORDS . ")" ); } @@ -551,7 +384,7 @@ public function createHistory(Request $request, Application $app) } - return $app->json( + return $this->json( array( 'success' => true, 'action' => $action, @@ -563,16 +396,18 @@ public function createHistory(Request $request, Application $app) /** * Attempt to update the Search History Record identified by the provided - * 'id' with the contents of the form parameter 'data'. + * 'id' with the contents of the form parameter 'data'. * * @param Request $request that will be used to complete the requested operation - * @param Application $app that will be used to complete the requested operation * @param int $id of the Search History Record to be updated. - * @return \Symfony\Component\HttpFoundation\JsonResponse + * @return Response * @throws BadRequestHttpException - * @throws AccessDeniedException + * @throws AccessDeniedHttpException + * @throws Exception */ - public function updateHistory(Request $request, Application $app, $id) + #[Route('/warehouse/search/history/{id}', requirements: ["id" => '\d+'], methods: ['POST', 'PUT'])] + #[Route('{prefix}warehouse/search/history/{id}', requirements: ["id" => '\d+', 'prefix' => '.*'], methods: ['POST', 'PUT'])] + public function updateHistory(Request $request, int $id): Response { $user = $this->authorize($request); @@ -590,7 +425,7 @@ public function updateHistory(Request $request, Application $app, $id) $result['dtype'] = 'recordid'; } - $results = $app->json( + $results = $this->json( array( 'success' => true, 'action' => $action, @@ -604,16 +439,16 @@ public function updateHistory(Request $request, Application $app, $id) /** * Attempt to delete the Search History Record identified by the - * provided 'id'. + * provided 'id'. * * @param Request $request that will be used to complete the requested operation - * @param Application $app that will be used to complete the requested operation * @param int $id of the Search History Record to be removed. - * @return \Symfony\Component\HttpFoundation\JsonResponse - * @throws BadRequestHttpException - * @throws AccessDeniedException + * @return Response + * @throws BadRequestHttpException|AccessDeniedHttpException|Exception */ - public function deleteHistory(Request $request, Application $app, $id) + #[Route('/warehouse/search/history/{id}', requirements: ["id" => "\d+"], methods: ['DELETE'])] + #[Route('{prefix}warehouse/search/history/{id}', requirements: ["id" => "\d+", 'prefix' => '.*'], methods: ['DELETE'])] + public function deleteHistory(Request $request, int $id): Response { $user = $this->authorize($request); $action = 'deleteHistory'; @@ -623,7 +458,7 @@ public function deleteHistory(Request $request, Application $app, $id) $history = $this->getUserStore($user, $realm); $deleted = $history->delById($id); - return $app->json( + return $this->json( array( 'success' => true, 'action' => $action, @@ -636,13 +471,15 @@ public function deleteHistory(Request $request, Application $app, $id) * Attempt to remove all of the Search History Records for the currently logged in * user making the request. * - * @param Request $request that will be used to complete the requested operation - * @param Application $app that will be used to complete the requested operation - * @return \Symfony\Component\HttpFoundation\JsonResponse + * @param Request $request + * @return Response * @throws BadRequestHttpException - * @throws AccessDeniedException + * @throws AccessDeniedHttpException + * @throws Exception */ - public function deleteAllHistory(Request $request, Application $app) + #[Route('/warehouse/search/history', methods: ['DELETE'])] + #[Route('{prefix}warehouse/search/history', requirements: ['prefix' => '.*'], methods: ['DELETE'])] + public function deleteAllHistory(Request $request): Response { $user = $this->authorize($request); @@ -653,7 +490,7 @@ public function deleteAllHistory(Request $request, Application $app) $history = $this->getUserStore($user, $realm); $history->del(); - return $app->json( + return $this->json( array( 'success' => true, 'action' => $action @@ -665,12 +502,13 @@ public function deleteAllHistory(Request $request, Application $app) * Attempt to perform a search of the jobs realm with the criteria provided in the * * @param Request $request - * @param Application $app - * @return \Symfony\Component\HttpFoundation\JsonResponse + * @return Response * @throws BadRequestHttpException - * @throws AccessDeniedException + * @throws AccessDeniedException if the user executing this request does not have access to the provided realm. + * @throws Exception if a user record is not found in the database that corresponds to the current user's username. */ - public function searchJobs(Request $request, Application $app) + #[Route('{prefix}warehouse/search/jobs', requirements: ['prefix' => '.*'], methods: ['GET'])] + public function searchJobs(Request $request): Response { $user = $this->authorize($request); @@ -683,52 +521,66 @@ public function searchJobs(Request $request, Application $app) throw new BadRequestHttpException('params parameter must be valid JSON'); } - if ( (isset($params['resource_id']) && isset($params['local_job_id'])) || isset($params['jobref']) ) { - return $this->getJobByPrimaryKey($app, $user, $realm, $params); + if ((isset($params['resource_id']) && isset($params['local_job_id'])) || isset($params['jobref'])) { + return $this->getJobByPrimaryKey($user, $realm, $params); } else { $startDate = $this->getStringParam($request, 'start_date', true); $endDate = $this->getStringParam($request, 'end_date', true); - return $this->processJobSearch($request, $app, $user, $realm, $startDate, $endDate, 'searchJobs'); + return $this->processJobSearch($request, $user, $realm, $startDate, $endDate, 'searchJobs'); } } /** * @param Request $request - * @param Application $app * @param string $action - * @return \Symfony\Component\HttpFoundation\JsonResponse + * @return Response * @throws BadRequestHttpException - * @throws AccessDeniedException + * @throws AccessDeniedHttpException + * @throws Exception if a user record is not found in the database that corresponds to the current user's username. */ - public function searchJobsByAction(Request $request, Application $app, $action) + #[Route( + "/warehouse/search/{realms}/{action}", + requirements: ["action" => "([\w|_|-])+", "realms" => "cloud|jobs"], + methods: ["GET", "POST"] + )] + #[Route( + "{prefix}warehouse/search/{realms}/{action}", + requirements: ["action" => "([\w|_|-])+", "realms" => "cloud|jobs", 'prefix' => '.*'], + methods: ["GET", "POST"] + )] + public function searchJobsByAction(Request $request, string $action): Response { $user = $this->authorize($request); - $name = 'searchJobsByAction'; + $actionName = 'searchJobsByAction'; + + /*TODO: verify that `ucfirst` is needed */ + $realm = ucfirst($this->getStringParam($request, 'realms')); - $realm = $this->getStringParam($request, 'realm'); $jobId = $this->getIntParam($request, 'jobid'); - $results = $this->processJobSearchByAction($request, $app, $user, $action, $realm, $jobId, $name); + $results = $this->processJobSearchByAction($request, $user, $action, $realm, $jobId, $actionName); return $results; - } /** - * Get the list of resources known to XDMoD and the metadata about them + * Get the list of resources known to XDMoD and the metadata about them. + * Specifically for the Data Analytics Framework * - * @param Request $request The request used to make this call. - * @param Application $app The router application. - * @return Response A response containing the following info: - * success: A boolean indicating if the call was successful. - * results: An object containing data about - * the dimensions retrieved. + * @param Request $request + * @return Response A response containing the following info: + * success: A boolean indicating if the call was successful. + * results: An object containing data about + * the dimensions retrieved. + * @throws Exception */ - public function getResources(Request $request, Application $app) + #[Route('/warehouse/resources', methods: ['GET'])] + #[Route('{prefix}warehouse/resources', requirements: ['prefix' => '.*'], methods: ['GET'])] + public function getResources(Request $request): Response { - Tokens::authenticate($request); + $this->tokenHelper->authenticate($request); $config = \Configuration\XdmodConfiguration::assocArrayFactory('resource_metadata.json', CONFIG_DIR); @@ -756,7 +608,7 @@ public function getResources(Request $request, Application $app) while ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { $resourceData[$result['resource_name']] = $result; } - return $app->json(array( + return $this->json(array( 'success' => true, 'results' => $resourceData )); @@ -767,36 +619,42 @@ public function getResources(Request $request, Application $app) * * Ported from: classes/REST/DataWarehouse/Explorer.php * - * @param Request $request The request used to make this call. - * @param Application $app The router application. - * @return Response A response containing the following info: - * success: A boolean indicating if the call was successful. - * results: An object containing data about - * the realms retrieved. + * @param Request $request The request used to make this call. + * + * @return Response A response containing the following info: + * success: A boolean indicating if the call was successful. + * results: An object containing data about + * + * @throws Exception */ - public function getRealms(Request $request, Application $app) + #[Route('/warehouse/realms', methods: ['GET'])] + public function getRealms(Request $request): Response { + /*TODO: verify that unauthorized users should be able to access this endpoint */ $user = $this->authorize($request); // Get the realms for the user's active role. $realms = Realms::getRealmsForUser($user); // Return the realms found. - return $app->json(array( + return $this->json([ 'success' => true, 'results' => $realms, - )); + ]); } /** * Return aggregate data from the datawarehouse * - * @param Request $request The request used to make this call. - * @param Application $app The router application. + * @param Request $request The request used to make this call. + * + * @return Response * - * @return json object + * @throws AccessDeniedException|UnauthorizedHttpException|Exception */ - public function getAggregateData(Request $request, Application $app) + #[Route('/warehouse/aggregatedata', methods: ['GET'])] + #[Route('{prefix}warehouse/aggregatedata', requirements: ['prefix' => '.*'], methods: ['GET'])] + public function getAggregateData(Request $request): Response { $user = $this->authorize($request); @@ -820,8 +678,8 @@ public function getAggregateData(Request $request, Application $app) $permittedStats = Acls::getPermittedStatistics($user, $config->realm, $config->group_by); $forbiddenStats = array_diff($config->statistics, $permittedStats); - if (!empty($forbiddenStats) ) { - throw new AccessDeniedException('access denied to ' . json_encode($forbiddenStats)); + if (!empty($forbiddenStats)) { + throw new AccessDeniedHttpException('access denied to ' . json_encode($forbiddenStats)); } $query = new \DataWarehouse\Query\AggregateQuery( @@ -852,7 +710,7 @@ public function getAggregateData(Request $request, Application $app) $dataset = new \DataWarehouse\Data\SimpleDataset($query); $results = $dataset->getResults($limit, $start); - foreach($results as &$val){ + foreach ($results as &$val) { $val['name'] = $val[$config->group_by . '_name']; $val['id'] = $val[$config->group_by . '_id']; $val['short_name'] = $val[$config->group_by . '_short_name']; @@ -862,7 +720,7 @@ public function getAggregateData(Request $request, Application $app) unset($val[$config->group_by . '_short_name']); unset($val[$config->group_by . '_order_id']); } - return $app->json( + return $this->json( array( 'results' => $results, 'total' => $dataset->getTotalPossibleCount(), @@ -876,14 +734,16 @@ public function getAggregateData(Request $request, Application $app) * * Ported from: classes/REST/DataWarehouse/Explorer.php * - * @param Request $request The request used to make this call. - * @param Application $app The router application. - * @return Response A response containing the following info: - * success: A boolean indicating if the call was successful. - * results: An object containing data about - * the dimensions retrieved. + * @param Request $request The request used to make this call. + * @return Response A response containing the following info: + * success: A boolean indicating if the call was successful. + * results: An object containing data about + * the dimensions retrieved. + * @throws Exception if a XDMoD user cannot be found for the currently logged in users username. */ - public function getDimensions(Request $request, Application $app) + #[Route('{prefix}warehouse/dimensions', requirements: ['prefix' => '.*'], methods: ['GET'])] + #[Route('/warehouse/dimensions', methods: ['GET'])] + public function getDimensions(Request $request): Response { $user = $this->authorize($request); @@ -897,8 +757,8 @@ public function getDimensions(Request $request, Application $app) ); $dimensionsToReturn = array(); - foreach($groupBys as $groupByName => $queryDescriptors) { - foreach($queryDescriptors as $queryDescriptor) { + foreach ($groupBys as $groupByName => $queryDescriptors) { + foreach ($queryDescriptors as $queryDescriptor) { if ($groupByName !== 'none') { $dimensionsToReturn[] = array( 'id' => $queryDescriptor->getGroupByName(), @@ -912,9 +772,9 @@ public function getDimensions(Request $request, Application $app) } // Return the dimensions found. - return $app->json(array( + return $this->json(array( 'success' => true, - 'results' => $dimensionsToReturn, + 'results' => $dimensionsToReturn )); } @@ -923,18 +783,21 @@ public function getDimensions(Request $request, Application $app) * * Ported from: classes/REST/DataWarehouse/Explorer.php * - * @param Request $request The request used to make this call. - * @param Application $app The router application. - * @return Response A response containing the following info: - * success: A boolean indicating if the call was successful. - * results: An object containing data about - * the dimension values retrieved. + * @param Request $request The request used to make this call. + * @param string $dimension + * @return Response A response containing the following info: + * success: A boolean indicating if the call was successful. + * results: An object containing data about + * the dimension values retrieved. + * @throws Exception */ - public function getDimensionValues(Request $request, Application $app, $dimension) + #[Route('/warehouse/dimensions/{dimension}', requirements: ["dimension" => "\w+"], methods: ['GET'])] + #[Route('{prefix}warehouse/dimensions/{dimension}', requirements: ["dimension" => "\w+", 'prefix' => '.*'], methods: ['GET'])] + public function getDimensionValues(Request $request, string $dimension): Response { $user = $this->authorize($request); - // Get parameters. + // Get Parameter values for feeding to MetricExplorer::getDimensionValues $offset = $this->getIntParam($request, 'offset', false, 0); $limit = $this->getIntParam($request, 'limit'); $searchText = $this->getStringParam($request, 'search_text'); @@ -942,7 +805,7 @@ public function getDimensionValues(Request $request, Application $app, $dimensio $realmParameter = $this->getStringParam($request, 'realm'); $realms = null; if ($realmParameter !== null) { - $realms = preg_split('/,\s*/', trim($realmParameter), null, PREG_SPLIT_NO_EMPTY); + $realms = preg_split('/,\s*/', trim($realmParameter), -1, PREG_SPLIT_NO_EMPTY); } // Get the dimension values. @@ -964,32 +827,40 @@ public function getDimensionValues(Request $request, Application $app, $dimensio } // Return the found dimension values. - return $app->json(array( + return $this->json(array( 'success' => true, - 'results' => $dimensionValuesData, + 'results' => $dimensionValuesData )); } /** * Get a set of quick filters tailored to the current user. * - * @param Request $request The request used to make this call. - * @param Application $app The router application. - * @return Response A response containing the following info: - * success: A boolean indicating if the call was successful. - * results: An object containing data about - * the metrics retrieved. + * @param Request $request The request used to make this call. + * @return Response A response containing the following info: + * success: A boolean indicating if the call was successful. + * results: An object containing data about + * the metrics retrieved. + * + * @throws UnavailableTimeAggregationUnitException + * @throws UnknownGroupByException + * @throws Exception if unable to find an XDMoD User by the currently logged in Users username. */ - public function getQuickFilters(Request $request, Application $app) + #[Route('/warehouse/quick_filters', methods: ['GET'])] + #[Route('{prefix}warehouse/quick_filters', requirements: ['prefix' => '.*'], methods: ['GET'])] + public function getQuickFilters(Request $request): Response { - // Get the user. - $user = $this->getUserFromRequest($request); + $user = $this->getUser(); + if (null === $user) { + $user = XDUser::getPublicUser(); + } else { + $user = XDUser::getUserByUserName($user->getUserIdentifier()); + } // Check whether multiple service providers are supported or not. try { - $multipleProvidersSupported = \xd_utilities\getConfiguration('features', 'multiple_service_providers') === 'on'; - } - catch(Exception $e){ + $multipleProvidersSupported = $this->parameters->get('xdmod.portal_settings.features.multiple_service_providers') === 'on'; + } catch (Exception $e) { $multipleProvidersSupported = false; } @@ -1060,25 +931,25 @@ public function getQuickFilters(Request $request, Application $app) } // Return the quick filters. - return $app->json(array( + return $this->json(array( 'success' => true, 'results' => array( 'dimensionNames' => $dimensionIdsToNames, - 'filters' => $filters, - ), + 'filters' => $filters + ) )); } - /** + /** * Attempt to retrieve the the name for the provided dimensionId. * - * @param Request $request - * @param Application $app - * @param string $dimensionId - * - * @return \Symfony\Component\HttpFoundation\JsonResponse + * @param Request $request + * @param string $dimensionId + * @return Response + * @throws Exception if there is no logged in user. */ - public function getDimensionName(Request $request, Application $app, $dimensionId) + #[Route('/warehouse/dimensions/{dimensionId}/name', requirements: ["dimensionId" => "(\w|_|-])+"], methods: ['GET'])] + public function getDimensionName(Request $request, string $dimensionId): Response { $user = $this->getUserFromRequest($request); $dimensionName = MetricExplorer::getDimensionName($user, $dimensionId); @@ -1086,17 +957,17 @@ public function getDimensionName(Request $request, Application $app, $dimensionI $status = $success ? 200 : 404; $payload = $success - ? array( - 'success' => $success, - 'results' => array( - 'name' => $dimensionName - )) - : array( - 'success' => false, - 'message' => "Unable to find a name for dimension: $dimensionId" - ); - - return $app->json( + ? array( + 'success' => $success, + 'results' => array( + 'name' => $dimensionName + )) + : array( + 'success' => false, + 'message' => "Unable to find a name for dimension: $dimensionId" + ); + + return $this->json( $payload, $status ); @@ -1106,14 +977,18 @@ public function getDimensionName(Request $request, Application $app, $dimensionI * Attempt to retrieve the the name for the provided dimensionId and * valueId. * - * @param Request $request - * @param Application $app - * @param string $dimensionId - * @param string $valueId - * - * @return \Symfony\Component\HttpFoundation\JsonResponse + * @param Request $request + * @param string $dimensionId + * @param string $valueId + * @return Response + * @throws Exception */ - public function getDimensionValueName(Request $request, Application $app, $dimensionId, $valueId) + #[Route( + "/warehouse/dimensions/{dimensionId}/values/{valueId}/name", + requirements: ["dimensionId" => "(\w|_|-])+", "valueId" => "(\w|_|-])+"], + methods: ["GET"] + )] + public function getDimensionValueName(Request $request, string $dimensionId, string $valueId): Response { $user = $this->getUserFromRequest($request); $valueName = MetricExplorer::getDimensionValueName($user, $dimensionId, $valueId); @@ -1121,18 +996,18 @@ public function getDimensionValueName(Request $request, Application $app, $dimen $status = $success ? 200 : 404; $payload = $success - ? array( - 'success' => $success, - 'results' => array( - 'name' => $valueName - ) - ) - : array( - 'success' => $success, - 'message' => "Unable to find a name for dimesion: $dimensionId | value: $valueId" - ); - - return $app->json( + ? array( + 'success' => $success, + 'results' => array( + 'name' => $valueName + ) + ) + : array( + 'success' => $success, + 'message' => "Unable to find a name for dimesion: $dimensionId | value: $valueId" + ); + + return $this->json( $payload, $status ); @@ -1143,20 +1018,21 @@ public function getDimensionValueName(Request $request, Application $app, $dimen * * Ported from: classes/REST/DataWarehouse/Explorer.php * - * @param Request $request The request used to make this call. - * @param Application $app The router application. - * @return Response A response containing the following info: - * success: A boolean indicating if the call was successful. - * results: An object containing data about - * the available aggregation units. + * @param Request $request The request used to make this call. + * @return Response A response containing the following info: + * success: A boolean indicating if the call was successful. + * results: An object containing data about + * the available aggregation units. + * @throws Exception */ - public function getAggregationUnits(Request $request, Application $app) + #[Route('/warehouse/aggregation_units', methods: ['GET'])] + public function getAggregationUnits(Request $request): Response { $this->authorize($request); // Return the available aggregation units. $aggregation_units = \DataWarehouse\QueryBuilder::getAggregationUnits(); - return $app->json(array( + return $this->json(array( 'success' => true, 'results' => array_keys($aggregation_units), )); @@ -1167,20 +1043,21 @@ public function getAggregationUnits(Request $request, Application $app) * * Ported from: classes/REST/DataWarehouse/Explorer.php * - * @param Request $request The request used to make this call. - * @param Application $app The router application. - * @return Response A response containing the following info: - * success: A boolean indicating if the call was successful. - * results: An object containing data about - * the available dataset types. + * @param Request $request The request used to make this call. + * @return Response A response containing the following info: + * success: A boolean indicating if the call was successful. + * results: An object containing data about + * the available dataset types. + * @throws Exception */ - public function getDatasetTypes(Request $request, Application $app) + #[Route('/warehouse/dataset/types', methods: ['GET'])] + public function getDatasetTypes(Request $request): Response { $this->authorize($request); // Return the available dataset types. $datasetTypes = \DataWarehouse\QueryBuilder::getDatasetTypes(); - return $app->json(array( + return $this->json(array( 'success' => true, 'results' => $datasetTypes, )); @@ -1189,21 +1066,21 @@ public function getDatasetTypes(Request $request, Application $app) /** * Get the dataset output formats available for use. * - * Ported from: classes/REST/DataWarehouse/Explorer.php + * Ported from: classes/REST/DataWarehouse/Explorer.php * - * @param Request $request The request used to make this call. - * @param Application $app The router application. - * @return Response A response containing the following info: - * success: A boolean indicating if the call was successful. - * results: An object containing data about - * the available dataset output formats. + * @param Request $request The request used to make this call. + * @return Response A response containing the following info: + * success: A boolean indicating if the call was successful. + * results: An object containing data about + * the available dataset output formats. */ - public function getDatasetOutputFormats(Request $request, Application $app) + #[Route('/warehouse/dataset/output_formats', methods: ['GET'])] + public function getDatasetOutputFormats(Request $request): Response { $this->authorize($request); // Return the available dataset output formats. - return $app->json(array( + return $this->json(array( 'success' => true, 'results' => \DataWarehouse\ExportBuilder::$dataset_action_formats, )); @@ -1212,11 +1089,12 @@ public function getDatasetOutputFormats(Request $request, Application $app) /** * Generate a dataset using the given parameters. * - * @param Request $request The request used to make this call. - * @param Application $app The router application. + * @param Request $request The request used to make this call. * @return Response + * @throws Exception */ - public function getDatasets(Request $request, Application $app) + #[Route('/datasets', methods: ['GET'])] + public function getDatasets(Request $request): Response { $user = $this->getUserFromRequest($request); @@ -1240,19 +1118,19 @@ public function getDatasets(Request $request, Application $app) * * Ported from: classes/REST/DataWarehouse/Explorer.php * - * @param Request $request The request used to make this call. - * @param Application $app The router application. + * @param Request $request The request used to make this call. * @return Response A response containing the following info: * success: A boolean indicating if the call was successful. * results: An object containing data about * the available plot output formats. */ - public function getPlotOutputFormats(Request $request, Application $app) + #[Route('/warehouse/plots/formats/output', methods: ['GET'])] + public function getPlotOutputFormats(Request $request) { $this->authorize($request); // Return the available plot output formats. - return $app->json(array( + return $this->json(array( 'success' => true, 'results' => \DataWarehouse\VisualizationBuilder::$plot_action_formats, )); @@ -1263,19 +1141,20 @@ public function getPlotOutputFormats(Request $request, Application $app) * * Ported from: classes/REST/DataWarehouse/Explorer.php * - * @param Request $request The request used to make this call. - * @param Application $app The router application. + * @param Request $request The request used to make this call. * @return Response A response containing the following info: * success: A boolean indicating if the call was successful. * results: An object containing data about * the available plot display types. + * @throws Exception */ - public function getPlotDisplayTypes(Request $request, Application $app) + #[Route('/warehouse/plots/formats/output', methods: ['GET'])] + public function getPlotDisplayTypes(Request $request): Response { $this->authorize($request); // Return the available plot display types. - return $app->json(array( + return $this->json(array( 'success' => true, 'results' => \DataWarehouse\VisualizationBuilder::$display_types, )); @@ -1286,19 +1165,20 @@ public function getPlotDisplayTypes(Request $request, Application $app) * * Ported from: classes/REST/DataWarehouse/Explorer.php * - * @param Request $request The request used to make this call. - * @param Application $app The router application. + * @param Request $request The request used to make this call. * @return Response A response containing the following info: * success: A boolean indicating if the call was successful. * results: An object containing data about * the available plot combine types. + * @throws Exception */ - public function getPlotCombineTypes(Request $request, Application $app) + #[Route('/warehouse/plots/types/combine', methods: ['GET'])] + public function getPlotCombineTypes(Request $request): Response { $this->authorize($request); // Return the available plot combine types. - return $app->json(array( + return $this->json(array( 'success' => true, 'results' => \DataWarehouse\VisualizationBuilder::$combine_types, )); @@ -1307,8 +1187,7 @@ public function getPlotCombineTypes(Request $request, Application $app) /** * Generate a plot using the given parameters. * - * @param Request $request The request used to make this call. - * @param Application $app The router application. + * @param Request $request The request used to make this call. * @return Response A response containing the following info * if JSON was requested: * success: A boolean indicating if the call was successful. @@ -1317,21 +1196,41 @@ public function getPlotCombineTypes(Request $request, Application $app) * * If another format was requested, the * response will contain file data. + * @throws Exception */ - public function getPlots(Request $request, Application $app) + #[Route('/warehouse/plots', methods: ['GET'])] + public function getPlots(Request $request): Response { $this->authorize($request); - return $this->getDatasets($request, $app); + return $this->getDatasets($request); } - public function processJobSearch(Request $request, Application $app, XDUser $user, $realm, $startDate, $endDate, $action) + /** + * @param Request $request + * @param XDUser $user + * @param string $realm + * @param string $startDate + * @param string $endDate + * @param string $action + * @return Response + * @throws Exception + * @noinspection PhpTooManyParametersInspection + */ + private function processJobSearch( + Request $request, + XDUser $user, + string $realm, + string $startDate, + string $endDate, + string $action + ): Response { $queryDescripters = Acls::getQueryDescripters($user, $realm); if (empty($queryDescripters)) { - throw new BadRequestHttpException('Invalid realm'); + throw new BadRequestHttpException('Invalid realm', null); } $offset = $this->getIntParam($request, 'start', true); @@ -1370,12 +1269,12 @@ public function processJobSearch(Request $request, Application $app, XDUser $use $row['text'] = "$resource-$localJobId"; $row['dtype'] = 'jobid'; - array_push($data, $row); + $data[] = $row; } $total = $dataSet->getTotalPossibleCount(); - $results = $app->json( + $results = $this->json( array( 'success' => true, 'action' => $action, @@ -1396,7 +1295,7 @@ public function processJobSearch(Request $request, Application $app, XDUser $use $privDataSet = new \DataWarehouse\Data\SimpleDataset($privQuery, 1, 0); $privResults = $privDataSet->getResults(); if (count($privResults) != 0) { - $results = $app->json( + $results = $this->json( array( 'success' => false, 'action' => $action, @@ -1413,48 +1312,68 @@ public function processJobSearch(Request $request, Application $app, XDUser $use /** * @param Request $request - * @param Application $app * @param XDUser $user - * @param $action - * @param $realm - * @param $jobId - * @param $actionName - * @return \Symfony\Component\HttpFoundation\JsonResponse - * @throws AccessDeniedException + * @param string $action + * @param string $realm + * @param ?int $jobId + * @param string $actionName + * @return Response + * @throws AccessDeniedException if the provided user does not have access to the specified realm. + * @throws Exception if executable information unavailable for the provided jobId. */ - public function processJobSearchByAction(Request $request, Application $app, XDUser $user, $action, $realm, $jobId, $actionName) + private function processJobSearchByAction( + Request $request, + XDUser $user, + string $action, + string $realm, + ?int $jobId, + string $actionName + ): Response { + switch ($action) { case 'accounting': case 'jobscript': case 'analysis': case 'metrics': case 'analytics': - $results = $this->getJobData($app, $user, $realm, $jobId, $action, $actionName); + /*TODO: verify that this doesn't need to be here*/ + /*$realm = $this->getStringParam($request, 'realm', true);*/ + $results = $this->getJobData($user, $realm, $jobId, $action); break; case 'peers': $start = $this->getIntParam($request, 'start', true); $limit = $this->getIntParam($request, 'limit', true); - $results = $this->getJobPeers($app, $user, $realm, $jobId, $start, $limit); + /*TODO: verify that this needs to be here.*/ + if ($jobId === null) { + throw new BadRequestHttpException('Invalid value for realm. Must be a(n) string.'); + } + /*TODO: verify that this needs to be here.*/ + $realm = $this->getStringParam($request, 'realm', true); + + $results = $this->getJobPeers($user, $realm, $jobId, $start, $limit); break; case 'executable': - $results = $this->getJobExecutable($app, $user, $realm, $jobId, $action, $actionName); + $realm = $this->getStringParam($request, 'realm', true); + $results = $this->getJobExecutable($user, $realm, $jobId, $action, $actionName); break; case 'detailedmetrics': - $results = $this->getJobSummary($app, $user, $realm, $jobId, $action, $actionName); + $realm = $this->getStringParam($request, 'realm', true); + $results = $this->getJobSummary($user, $realm, $jobId, $action, $actionName); break; case 'timeseries': $tsId = $this->getStringParam($request, 'tsid', true); - $nodeId = $this->getIntParam($request, 'nodeid', false); - $cpuId = $this->getIntParam($request, 'cpuid', false); - - $results = $this->getJobTimeSeriesData($app, $request, $user, $realm, $jobId, $tsId, $nodeId, $cpuId); + $nodeId = $this->getIntParam($request, 'nodeid'); + $cpuId = $this->getIntParam($request, 'cpuid'); + $realm = $this->getStringParam($request, 'realm', true); + $results = $this->getJobTimeSeriesData($request, $user, $realm, $jobId, $tsId, $nodeId, $cpuId); break; case 'vmstate': - $results = $this->getJobTimeSeriesData($app, $request, $user, $realm, $jobId, null, null, null); + $realm = $this->getStringParam($request, 'realm', true); + $results = $this->getJobTimeSeriesData($request, $user, $realm, $jobId, null, null, null); break; default: - $results = $app->json( + $results = $this->json( array( 'success' => false, 'action' => $actionName, @@ -1471,16 +1390,16 @@ public function processJobSearchByAction(Request $request, Application $app, XDU /** * Return data about a job's peers. * - * @param Application $app The router application. * @param XDUser $user the logged in user. - * @param $realm data realm. - * @param $jobId the unique identifier for the job. - * @param $start the start offset (for store paging). - * @param $limit the number of records to return (for store paging). - * @return json in Extjs.store parsable format. - * @throws NotFoundHttpException + * @param string $realm data realm. + * @param int $jobId the unique identifier for the job. + * @param int $start the start offset (for store paging). + * @param int $limit the number of records to return (for store paging). + * @return Response + * @throws AccessDeniedException if the provided user does not have access to the specified realm. + * @throws NotFoundHttpException if the provided jobId has no data in the provided realm. */ - protected function getJobPeers(Application $app, XDUser $user, $realm, $jobId, $start, $limit) + protected function getJobPeers(XDUser $user, string $realm, $jobId, int $start, int $limit): Response { $jobdata = $this->getJobDataSet($user, $realm, $jobId, 'internal'); if (!$jobdata->hasResults()) { @@ -1523,7 +1442,7 @@ protected function getJobPeers(Application $app, XDUser $user, $realm, $jobId, $ $dataset = $this->getJobDataSet($user, $realm, $jobId, 'peers'); foreach ($dataset->getResults() as $index => $jobpeer) { - if ( ($index >= $start) && ($index < ($start + $limit))) { + if (($index >= $start) && ($index < ($start + $limit))) { $result['series'][1]['data'][] = array( 'x' => $i++, 'low' => $jobpeer['start_time_ts'] * 1000.0, @@ -1539,7 +1458,7 @@ protected function getJobPeers(Application $app, XDUser $user, $realm, $jobId, $ } } - return $app->json(array( + return $this->json(array( 'success' => true, 'data' => array($result), 'total' => count($dataset->getResults()) @@ -1547,20 +1466,18 @@ protected function getJobPeers(Application $app, XDUser $user, $realm, $jobId, $ } /** - * @param Application $app * @param XDUser $user - * @param $realm - * @param $jobId - * @param $action - * @param $actionName - * @return \Symfony\Component\HttpFoundation\JsonResponse - * @throws \DataWarehouse\Query\Exceptions\AccessDeniedException - */ - private function getJobData(Application $app, XDUser $user, $realm, $jobId, $action, $actionName) + * @param string $realm + * @param int $jobId + * @param string $action + * @return Response + * @throws AccessDeniedException + */ + private function getJobData(XDUser $user, string $realm, int $jobId, string $action): Response { $dataSet = $this->getJobDataSet($user, $realm, $jobId, $action); - return $app->json( + return $this->json( array( 'data' => $dataSet->export(), 'success' => true @@ -1574,13 +1491,13 @@ private function getJobData(Application $app, XDUser $user, $realm, $jobId, $act * @param string $realm * @param int $jobId * @param string $action - * @return \DataWarehouse\Data\RawDataset - * @throws \DataWarehouse\Query\Exceptions\AccessDeniedException + * @return RawDataset + * @throws AccessDeniedException if the provided user does not have access to the specified realm. */ - private function getJobDataSet(XDUser $user, $realm, $jobId, $action) + private function getJobDataSet(XDUser $user, string $realm, $jobId, string $action): RawDataset { if (!\DataWarehouse\Access\RawData::realmExists($user, $realm)) { - throw new \DataWarehouse\Query\Exceptions\AccessDeniedException; + throw new AccessDeniedHttpException(); } $QueryClass = "\\DataWarehouse\\Query\\$realm\\JobDataset"; @@ -1590,13 +1507,13 @@ private function getJobDataSet(XDUser $user, $realm, $jobId, $action) $allRoles = $user->getAllRoles(); $query->setMultipleRoleParameters($allRoles, $user); - $dataSet = new \DataWarehouse\Data\RawDataset($query, $user); + $dataSet = new RawDataset($query, $user); if (!$dataSet->hasResults()) { $privilegedQuery = new $QueryClass($params, $action); $results = $privilegedQuery->execute(1); if ($results['count'] != 0) { - throw new \DataWarehouse\Query\Exceptions\AccessDeniedException; + throw new AccessDeniedHttpException(); } } return $dataSet; @@ -1605,16 +1522,13 @@ private function getJobDataSet(XDUser $user, $realm, $jobId, $action) /** * Retrieves the executable information for a given job. * - * @param Application $app the Application instance used. - * @param \XDUser $user the user that made this particular request. + * @param XDUser $user the user that made this particular request. * @param string $realm the data realm in which this request was made. - * @param string $jobId the unique identifier for the job. - * @param string $action the parent action that called this function. - * @param string $actionName the child action that called this function. - * @return \Symfony\Component\HttpFoundation\JsonResponse + * @param ?int $jobId the unique identifier for the job. + * @return Response * @throws Exception */ - private function getJobExecutable(Application $app, \XDUser $user, $realm, $jobId, $action, $actionName) + private function getJobExecutable(XDUser $user, string $realm, ?int $jobId): Response { $QueryClass = "\\DataWarehouse\\Query\\$realm\\JobMetadata"; $query = new $QueryClass(); @@ -1622,30 +1536,39 @@ private function getJobExecutable(Application $app, \XDUser $user, $realm, $jobI $execInfo = $query->getJobExecutableInfo($user, $jobId); if (count($execInfo) === 0) { - throw new Exception( + throw new \Exception( "Executable information unavailable for $realm $jobId", 500 ); } - return $app->json( - $this->arraytostore(json_decode(json_encode($execInfo), true)), - 200 + return $this->json( + $this->arrayToStore( + json_decode(json_encode($execInfo), true) + ) ); } - private function arraytostore(array $values) + /** + * @param array $values + * @return array[] + */ + private function arrayToStore(array $values): array { return array(array("key" => ".", "value" => "", "expanded" => true, "children" => $this->atosrecurse($values, false) )); } - private function atosrecurse(array $values) + /** + * @param array $values + * @return array + */ + private function atosRecurse(array $values): array { $result = array(); - foreach($values as $key => $value) { - if( is_array($value) ) { - if(count($value) > 0 ) { - $result[] = array("key" => "$key", "value" => "", "expanded" => true, "children" => $this->atosrecurse($value) ); + foreach ($values as $key => $value) { + if (is_array($value)) { + if (count($value) > 0) { + $result[] = array("key" => "$key", "value" => "", "expanded" => true, "children" => $this->atosRecurse($value)); } } else { $result[] = array("key" => "$key", "value" => $value, "leaf" => true); @@ -1654,30 +1577,26 @@ private function atosrecurse(array $values) return $result; } - /** - * @param Application $app * @param XDUser $user * @param string $realm - * @param int $jobId + * @param ?int $jobId * @param string $tsId * @param int $nodeId * @param int $infoId - * @param string $action - * @return \Symfony\Component\HttpFoundation\JsonResponse + * @return Response * @throws BadRequestHttpException - * @throws Exception + * @noinspection PhpTooManyParametersInspection */ private function processJobNodeTimeSeriesRequest( - Application $app, XDUser $user, - $realm, - $jobId, - $tsId, - $nodeId, - $infoId, - $action - ) { + string $realm, + ?int $jobId, + string $tsId, + int $nodeId, + int $infoId + ): Response + { if ($infoId != \DataWarehouse\Query\RawQueryTypes::TIMESERIES_METRICS) { throw new BadRequestHttpException("Node $infoId is a leaf"); @@ -1688,36 +1607,32 @@ private function processJobNodeTimeSeriesRequest( $result = array(); foreach ($info->getJobTimeseriesMetricNodeMeta($user, $jobId, $tsId, $nodeId) as $cpu) { - $cpu['url'] = "/rest/v0.1/warehouse/search/jobs/timeseries"; + $cpu['url'] = "/warehouse/search/jobs/timeseries"; $cpu['type'] = "timeseries"; $cpu['dtype'] = "cpuid"; $result[] = $cpu; } - return $app->json(array("success" => true, "results" => $result)); + return $this->json(array("success" => true, "results" => $result)); } /** - * @param Application $app * @param XDUser $user - * @param $realm - * @param int $jobId - * @param $tsId + * @param string $realm + * @param ?int $jobId + * @param string $tsId * @param int $infoId - * @param string $action - * @return \Symfony\Component\HttpFoundation\JsonResponse - * @throws BadRequestHttpException + * @return Response */ private function processJobTimeSeriesRequest( - Application $app, XDUser $user, - $realm, - $jobId, - $tsId, - $infoId, - $action - ) { + string $realm, + ?int $jobId, + string $tsId, + int $infoId + ): Response + { if ($infoId != \DataWarehouse\Query\RawQueryTypes::TIMESERIES_METRICS) { throw new BadRequestHttpException("Node $infoId is a leaf"); @@ -1728,90 +1643,85 @@ private function processJobTimeSeriesRequest( $result = array(); foreach ($info->getJobTimeseriesMetricMeta($user, $jobId, $tsId) as $node) { - $node['url'] = "/rest/v0.1/warehouse/search/jobs/timeseries"; + $node['url'] = "/warehouse/search/jobs/timeseries"; $node['type'] = "timeseries"; - $node['dtype'] = "nodeid"; + + /*TODO: verify that this is node not nodeid*/ + $node['dtype'] = "node"; $result[] = $node; } - return $app->json(array("success" => true, "results" => $result)); + return $this->json(array("success" => true, "results" => $result)); } /** - * @param Application $app * @param XDUser $user * @param string $realm * @param int $jobId - * @param int $infoId - * @param string $action - * @return \Symfony\Component\HttpFoundation\JsonResponse - * @throws BadRequestHttpException + * @param string $infoId + * @return Response */ private function processJobRequest( - Application $app, XDUser $user, - $realm, - $jobId, - $infoId, - $action - ) { + string $realm, + ?int $jobId, + int $infoId + ): Response + { switch ($infoId) { case "" . \DataWarehouse\Query\RawQueryTypes::VM_INSTANCE: - $infoclass = "\\DataWarehouse\\Query\\$realm\\JobMetadata"; - $info = new $infoclass(); + $infoClass = "\\DataWarehouse\\Query\\$realm\\JobMetadata"; + $info = new $infoClass(); - $result = array(); + $result = []; foreach ($info->getJobTimeseriesMetaData($user, $jobId) as $tsid) { - $tsid['url'] = "/rest/v0.1/warehouse/search/jobs/vmstate"; + $tsid['url'] = "/warehouse/search/jobs/vmstate"; $tsid['type'] = "timeseries"; $tsid['dtype'] = "tsid"; $result[] = $tsid; } - return $app->json(array('success' => true, "results" => $result)); - break; - case "" . \DataWarehouse\Query\RawQueryTypes::TIMESERIES_METRICS: - $infoclass = "\\DataWarehouse\\Query\\$realm\\JobMetadata"; - $info = new $infoclass(); + return $this->json(array("success" => true, "results" => $result)); + case '' . \DataWarehouse\Query\RawQueryTypes::TIMESERIES_METRICS: + $infoClass = "\\DataWarehouse\\Query\\$realm\\JobMetadata"; + $info = new $infoClass(); - $result = array(); + $result = []; foreach ($info->getJobTimeseriesMetaData($user, $jobId) as $tsid) { - $tsid['url'] = "/rest/v0.1/warehouse/search/jobs/timeseries"; + $tsid['url'] = "/warehouse/search/jobs/timeseries"; $tsid['type'] = "timeseries"; $tsid['dtype'] = "tsid"; $result[] = $tsid; } - return $app->json(array('success' => true, "results" => $result)); - break; + return $this->json(array('success' => true, "results" => $result)); default: throw new BadRequestHttpException("Node is a leaf"); } } /** - * @param Application $app * @param XDUser $user * @param string $realm * @param int $jobId * @param string $action - * @return \Symfony\Component\HttpFoundation\JsonResponse + * @return Response */ private function processJobByJobId( - Application $app, XDUser $user, - $realm, - $jobId, - $action - ) { + string $realm, + int $jobId, + string $action + ): Response + { $JobMetaDataClass = "\\DataWarehouse\\Query\\$realm\\JobMetadata"; $info = new $JobMetaDataClass(); $jobMetaData = $info->getJobMetadata($user, $jobId); - $data = array_intersect_key($this->_supported_types, $jobMetaData); + $data = array_intersect_key($this->supportedTypes, $jobMetaData); - return $app->json( + return $this->json( array( 'success' => true, 'action' => $action, @@ -1821,13 +1731,12 @@ private function processJobByJobId( } /** - * @param Application $app * @param XDUser $user * @param string $realm * @param string $action - * @return \Symfony\Component\HttpFoundation\JsonResponse + * @return Response */ - private function processHistoryRequest(Application $app, XDUser $user, $realm, $action) + private function processHistoryRequest(XDUser $user, string $realm, string $action): Response { $history = $this->getUserStore($user, $realm); $output = $history->get(); @@ -1841,7 +1750,7 @@ private function processHistoryRequest(Application $app, XDUser $user, $realm, $ ); } - return $app->json( + return $this->json( array( 'success' => true, 'action' => $action, @@ -1852,27 +1761,27 @@ private function processHistoryRequest(Application $app, XDUser $user, $realm, $ } /** - * @param Application $app - * @param $action - * @return \Symfony\Component\HttpFoundation\JsonResponse + * @param XDUser $user + * @param string $action + * @return Response */ - private function processHistoryDefaultRealmRequest(Application $app, XDUser $user, $action) + private function processHistoryDefaultRealmRequest(XDUser $user, string $action): Response { $results = array(); - foreach(\DataWarehouse\Access\RawData::getRawDataRealms($user) as $realmconfig) { - $history = $this->getUserStore($user, $realmconfig['name']); + foreach (\DataWarehouse\Access\RawData::getRawDataRealms($user) as $realmConfig) { + $history = $this->getUserStore($user, $realmConfig['name']); $records = $history->get(); if (!empty($records)) { $results[] = array( 'dtype' => 'realm', - 'realm' => $realmconfig['name'], - 'text' => $realmconfig['display'] + 'realm' => $realmConfig['name'], + 'text' => $realmConfig['display'] ); } } - return $app->json( + return $this->json( array( 'success' => true, 'action' => $action, @@ -1881,7 +1790,11 @@ private function processHistoryDefaultRealmRequest(Application $app, XDUser $use ); } - private function encodeFloatArray(array $in) + /** + * @param array $in + * @return array + */ + private function encodeFloatArray(array $in): array { $out = array(); foreach ($in as $key => $value) { @@ -1894,7 +1807,13 @@ private function encodeFloatArray(array $in) return $out; } - private function getJobSummary(Application $app, \XDUser $user, $realm, $jobId, $action, $actionName) + /** + * @param XDUser $user + * @param string $realm + * @param int $jobId + * @return Response + */ + private function getJobSummary(XDUser $user, string $realm, int $jobId): Response { $queryclass = "\\DataWarehouse\\Query\\$realm\\JobMetadata"; $query = new $queryclass(); @@ -1941,41 +1860,40 @@ private function getJobSummary(Application $app, \XDUser $user, $realm, $jobId, } } - return $app->json( - $result - ); + return $this->json($result); } /** * Encode a chart data series in CSV data and send as an attachment - * @param $data the data series information - * @return Response the data in a CSV file attachment + * + * @param array $data the data series information + * @return Response */ - private function chartDataResponse($data) + private function chartDataResponse(array $data): Response { $filename = tempnam(sys_get_temp_dir(), 'xdmod'); $fp = fopen($filename, 'w'); $columns = array('Time'); - $ndatapoints = 0; + $numberOfDataPoints = 0; foreach ($data['series'] as $series) { if (isset($series['dtype'])) { $columns[] = $series['name']; - if ($ndatapoints === 0) { - $ndatapoints = count($series['data']); + if ($numberOfDataPoints === 0) { + $numberOfDataPoints = count($series['data']); } } } fputcsv($fp, $columns); - for ($i = 0; $i < $ndatapoints; $i++) { + for ($i = 0; $i < $numberOfDataPoints; $i++) { $outline = array(); foreach ($data['series'] as $series) { if (isset($series['dtype'])) { if (count($outline) === 0) { - $outline[] = isset($series['data'][$i]['x']) ? $series['data'][$i]['x'] : $series['data'][$i][0]; + $outline[] = $series['data'][$i]['x'] ?? $series['data'][$i][0]; } - $outline[] = isset($series['data'][$i]['y']) ? $series['data'][$i]['y'] : $series['data'][$i][1]; + $outline[] = $series['data'][$i]['y'] ?? $series['data'][$i][1]; } } fputcsv($fp, $outline); @@ -1998,11 +1916,12 @@ private function chartDataResponse($data) * This function is used for exporting *Job Viewer Timeseries* plots only. * It repeats chart config performed for browser in job viewer's ChartPanel.js. * - * @param $data the data - * @param $type the type of image to generate - * @return Response the image as an attachment + * @param array $data the data + * @param string $type the type of image to generate + * @param array $settings + * @return Response */ - private function chartImageResponse($data, $type, $settings) + private function chartImageResponse(array $data, string $type, array $settings): Response { $axisTitleFontSize = ($settings['font_size'] + 12) . 'px'; $axisLabelFontSize = ($settings['font_size'] + 11) . 'px'; @@ -2011,11 +1930,11 @@ private function chartImageResponse($data, $type, $settings) $lineWidth = 1 + $settings['scale']; $chartConfig = array( - 'data' => $data, - 'axisTickSize' => $axisLabelFontSize, - 'axisTitleSize' => $axisTitleFontSize, - 'lineWidth' => $lineWidth, - 'chartTitleSize' => $mainTitleFontSize + 'data' => $data, + 'axisTickSize' => $axisLabelFontSize, + 'axisTitleSize' => $axisTitleFontSize, + 'lineWidth' => $lineWidth, + 'chartTitleSize' => $mainTitleFontSize ); $globalConfig = array( @@ -2029,7 +1948,26 @@ private function chartImageResponse($data, $type, $settings) return $this->sendAttachment($chartImage, $chartFilename, $mimeOverride); } - private function getJobTimeSeriesData(Application $app, Request $request, \XDUser $user, $realm, $jobId, $tsId, $nodeId, $cpuId) + /** + * @param Request $request + * @param XDUser $user + * @param string $realm + * @param ?int $jobId + * @param ?string $tsId + * @param ?int $nodeId + * @param ?int $cpuId + * @return Response + * @throws NotFoundHttpException + */ + private function getJobTimeSeriesData( + Request $request, + XDUser $user, + string $realm, + ?int $jobId, + ?string $tsId, + ?int $nodeId, + ?int $cpuId + ): Response { $infoclass = "\\DataWarehouse\\Query\\$realm\\JobMetadata"; $info = new $infoclass(); @@ -2054,11 +1992,11 @@ private function getJobTimeSeriesData(Application $app, Request $request, \XDUse 'height' => $this->getIntParam($request, 'height', false, 484), 'scale' => floatval($this->getStringParam($request, 'scale', false, '1')), 'font_size' => $this->getIntParam($request, 'font_size', false, 3), - 'show_title' => $this->getStringParam($request, 'show_title', false, 'y') === 'y' ? true : false, + 'show_title' => $this->getStringParam($request, 'show_title', false, 'y') === 'y', 'fileMetadata' => array( 'author' => $user->getFormalName(), - 'subject' => 'Timeseries data for ' . $results['schema']['source'], - 'title' => $results['schema']['description'] + 'subject' => 'Timeseries data for ' . $results['schema']['source'] ?? '', + 'title' => $results['schema']['description'] ?? '' ) ); $response = $this->chartImageResponse($results, $format, $exportConfig); @@ -2068,7 +2006,7 @@ private function getJobTimeSeriesData(Application $app, Request $request, \XDUse break; case 'json': default: - $response = $app->json(array("success" => true, "data" => array($results))); + $response = $this->json(array("success" => true, "data" => array($results))); break; } @@ -2081,18 +2019,16 @@ private function getJobTimeSeriesData(Application $app, Request $request, \XDUse * confusion between this internal identifier and the job id provided * by the resource-manager). * - * @param Application $app - * @param \XDUser $user + * @param XDUser $user * @param string $realm * @param array $searchparams - * @return \Symfony\Component\HttpFoundation\JsonResponse - * @throws \DataWarehouse\Query\Exceptions\AccessDeniedException - * @throws BadRequestHttpException + * @return Response + * @throws AccessDeniedException if the provided user does not have access to the provided realm. */ - private function getJobByPrimaryKey(Application $app, \XDUser $user, $realm, $searchparams) + private function getJobByPrimaryKey(XDUser $user, string $realm, array $searchparams): Response { if (!\DataWarehouse\Access\RawData::realmExists($user, $realm)) { - throw new \DataWarehouse\Query\Exceptions\AccessDeniedException; + throw new AccessDeniedHttpException(); } if (isset($searchparams['jobref']) && is_numeric($searchparams['jobref'])) { @@ -2114,13 +2050,13 @@ private function getJobByPrimaryKey(Application $app, \XDUser $user, $realm, $se $allRoles = $user->getAllRoles(); $query->setMultipleRoleParameters($allRoles, $user); - $dataSet = new \DataWarehouse\Data\RawDataset($query, $user); + $dataSet = new RawDataset($query, $user); $results = array(); foreach ($dataSet->getResults() as $result) { $result['text'] = $result['resource'] . "-" . $result['local_job_id']; $result['dtype'] = 'jobid'; - array_push($results, $result); + $results[] = $result; } if (!$dataSet->hasResults()) { @@ -2128,41 +2064,46 @@ private function getJobByPrimaryKey(Application $app, \XDUser $user, $realm, $se $privilegedResults = $privilegedQuery->execute(1); if ($privilegedResults['count'] != 0) { - throw new \DataWarehouse\Query\Exceptions\AccessDeniedException(); + throw new AccessDeniedHttpException(); } } - return $app->json( + return $this->json( array( 'success' => true, - "results" => $results, - "totalCount" => count($results) + 'results' => $results, + 'totalCount' => count($results) ) ); } - private function getUserStore(\XDUser $user, $realm) + /** + * @param XDUser $user + * @param string $realm + * @return UserStorage + */ + private function getUserStore(XDUser $user, string $realm): UserStorage { - $container = implode('-', array_filter(array(self::_HISTORY_STORE, strtoupper($realm)))); - return new \UserStorage($user, $container); + $container = implode('-', array_filter(array(self::HISTORY_STORE_KEY, strtoupper($realm)))); + return new UserStorage($user, $container); } /** * Endpoint to get rows of raw data from the data warehouse. Requires API - * token authorization. + * token authorization. * - * The request should contain the following parameters: - * - start_date: start of date range for which to get data. - * - end_date: end of date range for which to get data. - * - realm: data realm for which to get data. + * The request should contain the following parameters: + * - start_date: start of date range for which to get data. + * - end_date: end of date range for which to get data. + * - realm: data realm for which to get data. * - * It can also contain the following optional parameters: - * - fields: list of aliases of fields to get (if not provided, all - * fields are obtained). - * - filters: mapping of dimension names to their possible values. - * Results will only be included whose values for each of the - * given dimensions match one of the corresponding given values. - * - offset: starting row index of data to get. + * It can also contain the following optional parameters: + * - fields: list of aliases of fields to get (if not provided, all + * fields are obtained). + * - filters: mapping of dimension names to their possible values. + * Results will only be included whose values for each of the + * given dimensions match one of the corresponding given values. + * - offset: starting row index of data to get. * * If successful, the response will be a stream of chunks of data of type * `text/plain`. The beginning of each chunk is a string of hex digits @@ -2174,8 +2115,7 @@ private function getUserStore(\XDUser $user, $realm) * is of length zero to indicate the end of the stream. * * @param Request $request - * @param Application $app - * @return \Symfony\Component\HttpFoundation\StreamedResponse + * @return StreamedResponse * @throws BadRequestHttpException if any of the required parameters are * not included; if an invalid start date, * end date, realm, field alias, or filter @@ -2183,12 +2123,28 @@ private function getUserStore(\XDUser $user, $realm) * before the start date; or if the offset * is negative. * @throws AccessDeniedException if the user does not have permission to - * get raw data from the requested realm. + * get raw data from the requested realm. + * @throws Exception */ - public function getRawData(Request $request, Application $app) + #[Route('/warehouse/raw-data', methods: ['GET'])] + #[Route('{prefix}warehouse/raw-data', requirements: ['prefix' => '.*'], methods: ['GET'])] + public function getRawData(Request $request): Response { - $user = Tokens::authenticate($request); - $params = $this->validateRawDataParams($request, $user); + $user = $this->tokenHelper->authenticate($request, false); + + /*TODO: Validate that this is supposed to be here. */ + if ($user === null) { + return $this->json(buildError(new Exception('No token provided.')), 401, [ + 'WWW-Authenticate' => 'Bearer' + ]); + } + + try { + $params = $this->validateRawDataParams($request, $user); + } catch (HttpException $e) { + return $this->json(buildError($e), $e->getStatusCode()); + } + $realmManager = new RealmManager(); $queryClass = $realmManager->getRawDataQueryClass($params['realm']); $logger = $this->getRawDataLogger(); @@ -2206,7 +2162,7 @@ public function getRawData(Request $request, Application $app) if ('Jobs' === $params['realm']) { $currentDate = $params['start_date']; while ($currentDate <= $params['end_date']) { - self::echoRawData( + $this->echoRawData( $queryClass, $currentDate, $currentDate, @@ -2227,7 +2183,7 @@ public function getRawData(Request $request, Application $app) } else { // All other realms query the entire date range in a single // query. - self::echoRawData( + $this->echoRawData( $queryClass, $params['start_date'], $params['end_date'], @@ -2242,30 +2198,28 @@ public function getRawData(Request $request, Application $app) ); } }; - return $app->stream( - $streamCallback, - 200, - ['Content-Type' => 'text/plain'] - ); + /*TODO: Validate that this is how to do a streamed response. */ + return new StreamedResponse($streamCallback, 200, ['Content-Type' => 'application/json-seq']); } + + /** * Validate the parameters of the request from the given user to the raw - * data endpoint (@see getRawData()). - * - * @param Request $request + * data endpoint (@param Request $request * @param XDUser $user * @return array of validated parameter values. - * @throws BadRequestHttpException if any of the parameters are invalid. - * @throws AccessDeniedException if the user does not have permission to - * get raw data from the requested realm. + * @throws BadRequestException if any of the parameters are invalid. + * @throws AccessDeniedHttpException if the user does not have permission to + * get raw data from the requested realm. + * @throws Exception if there is a problem retrieving the query descripters. */ - private function validateRawDataParams($request, $user) + private function validateRawDataParams($request, $user): array { $params = []; list( $params['start_date'], $params['end_date'] - ) = $this->validateRawDataDateParams($request); + ) = $this->validateRawDataDateParams($request); $params['realm'] = $this->getStringParam($request, 'realm', true); $allRealmNames = self::getRealmNames(Realms::getRealms()); if (!in_array($params['realm'], $allRealmNames)) { @@ -2296,7 +2250,7 @@ private function validateRawDataParams($request, $user) ); $params['offset'] = $this->getIntParam($request, 'offset', false, 0); if ($params['offset'] < 0) { - throw new BadRequestHttpException('Offset must be non-negative.'); + throw new BadRequestHttpException('Offset must be non-negative.', null); } return $params; } @@ -2304,9 +2258,10 @@ private function validateRawDataParams($request, $user) /** * Generate a database logger for the raw data queries. * - * @return \CCR\Logger + * @return LoggerInterface + * @throws Exception if there's a problem instantiating the Logger */ - private function getRawDataLogger() + private function getRawDataLogger(): LoggerInterface { return Log::factory( 'data-warehouse-raw-data-rest', @@ -2319,8 +2274,7 @@ private function getRawDataLogger() } /** - * Perform an unbuffered database query and echo the result using chunked - * transfer encoding, flushing every 10000 rows. + * Perform an unbuffered database query and echo the result as a JSON text sequence, flushing every 10000 rows. * * @param string $queryClass the fully qualified name of the query class. * @param string $startDate the start date of the query in ISO 8601 format. @@ -2330,30 +2284,31 @@ private function getRawDataLogger() * @param bool $isLastQueryInSeries if true, switch back to MySQL buffered query mode after echoing the last row. * @param array $params validated parameter values from @see validateRawDataParams(). * @param XDUser $user the user making the request. - * @param \CCR\Logger $logger used to log the database request. + * @param LoggerInterface $logger used to log the database request. * @param bool $reachedOffset if true, the requested offset row has been already been reached so don't keep * checking for it, instead just echo all rows. Otherwise, keep checking for the * offset row and only start echoing rows once it is reached. * @param int $i the number of rows iterated so far plus one — used to keep track of whether the offset has been * reached and when to flush. * @param int $offset the number of rows to ignore before echoing. - * @return null + * @return void * @throws Exception if $startDate or $endDate are invalid ISO 8601 dates, if there is an error connecting to * or querying the database, or if invalid fields have been specified in the query parameters. */ - private static function echoRawData( - $queryClass, - $startDate, - $endDate, - $isFirstQueryInSeries, - $isLastQueryInSeries, - $params, - $user, - $logger, - &$reachedOffset, - &$i, - &$offset - ) { + private function echoRawData( + string $queryClass, + string $startDate, + string $endDate, + bool $isFirstQueryInSeries, + bool $isLastQueryInSeries, + array $params, + XDUser $user, + LoggerInterface $logger, + bool &$reachedOffset, + int &$i, + int &$offset + ): void + { $query = new $queryClass( [ 'start_date' => $startDate, @@ -2361,8 +2316,8 @@ private static function echoRawData( ], 'batch' ); - $query = self::setRawDataQueryFilters($query, $params); - $dataset = self::getRawBatchDataset( + $query = $this->setRawDataQueryFilters($query, $params); + $dataset = $this->getRawBatchDataset( $user, $params, $query, @@ -2407,18 +2362,19 @@ private static function echoRawDataRow($row) { * * @param XDUser $user * @param array $params validated parameter values. - * @param \DataWarehouse\Query\RawQuery $query - * @param \CCR\Logger + * @param RawQuery $query + * @param LoggerInterface $logger * @return BatchDataset * @throws Exception if the `fields` parameter contains invalid field * aliases. */ - private static function getRawBatchDataset( + private function getRawBatchDataset( $user, $params, $query, $logger - ) { + ): BatchDataset + { try { $dataset = new BatchDataset( $query, @@ -2437,18 +2393,17 @@ private static function getRawBatchDataset( } /** - * Validate the `start_date` and `end_date` parameters of the given request - * to the raw data endpoint (@see getRawData()). - * - * @param Request $request + * Validate the 'start_date' and 'end_date' parameters of the given request + * to the raw data endpoint (@param Request $request * @return array containing the validated start and end dates in Y-m-d * format. - * @throws BadRequestHttpException if the start and/or end dates are not - * provided or are not valid ISO 8601 dates - * or the end date is less than the start - * date. + * @throws BadRequestException if the start and/or end dates are not + * provided or are not valid ISO 8601 dates or + * the end date is less than the start date. + * @see getRawData()). + * */ - private function validateRawDataDateParams($request) + private function validateRawDataDateParams(Request $request): array { $startDate = $this->getDateFromISO8601Param( $request, @@ -2461,9 +2416,7 @@ private function validateRawDataDateParams($request) true ); if ($endDate < $startDate) { - throw new BadRequestHttpException( - 'End date cannot be less than start date.' - ); + throw new BadRequestHttpException('End date cannot be less than start date.', null); } return [$startDate->format('Y-m-d'), $endDate->format('Y-m-d')]; } @@ -2475,7 +2428,8 @@ private function validateRawDataDateParams($request) * @param array $realms array of Realm\Realm objects. * @return array of string realm names. */ - private static function getRealmNames(array $realms) { + private static function getRealmNames(array $realms): array + { return array_map( function ($realm) { return $realm->getName(); @@ -2486,14 +2440,14 @@ function ($realm) { /** * Get the array of field aliases from the given request to the raw data - * endpoint (@see getRawData()), e.g., the parameter `fields=foo,bar,baz` - * results in `['foo', 'bar', 'baz']`. - * - * @param Request $request + * endpoint (@param Request $request * @return array|null containing the field aliases parsed from the request, * if provided. + * @see getRawData()), e.g., the parameter `fields=foo,bar,baz` + * results in `['foo', 'bar', 'baz']`. + * */ - private function getRawDataFieldsArray($request) + private function getRawDataFieldsArray(Request $request): ?array { $fields = null; $fieldsStr = $this->getStringParam($request, 'fields', false); @@ -2512,13 +2466,16 @@ private function getRawDataFieldsArray($request) * @param array $queryDescripters the set of dimensions the user is * authorized to see based on their assigned * ACLs. - * @return array whose keys are the validated filter keys (they must be + * @return array|null whose keys are the validated filter keys (they must be * valid dimensions the user is authorized to see) and whose * values are arrays of the provided string values. * @throws BadRequestHttpException if any of the filter keys are invalid - * dimension names. + * dimension names. + * @see getRawData()), e.g., the parameter + * `filters[foo]=bar,baz` results in `['foo' => ['bar', 'baz']]`. + * */ - private function validateRawDataFiltersParams($request, $queryDescripters) + private function validateRawDataFiltersParams(Request $request, array $queryDescripters): ?array { $filters = null; $filtersParam = $request->get('filters'); @@ -2540,21 +2497,20 @@ private function validateRawDataFiltersParams($request, $queryDescripters) * values, set the query to filter out records whose value for the given * dimension does not match any of the provided values. * - * @param \DataWarehouse\Query\RawQuery $query + * @param RawQuery $query * @param array $params containing a `filters` key whose value is an * associative array of dimensions and dimension * values. - * @return \DataWarehouse\Query\RawQuery the query with the filters - * applied. + * @return RawQuery the query with the filters applied. */ - private static function setRawDataQueryFilters($query, $params) + private function setRawDataQueryFilters(RawQuery $query, array $params): RawQuery { if (is_array($params['filters']) && count($params['filters']) > 0) { $f = new stdClass(); $f->{'data'} = []; foreach ($params['filters'] as $dimension => $values) { foreach ($values as $value) { - $f->{'data'}[] = (object) [ + $f->{'data'}[] = (object)[ 'id' => "$dimension=$value", 'value_id' => $value, 'dimension_id' => $dimension, @@ -2573,27 +2529,102 @@ private static function setRawDataQueryFilters($query, $params) * of values for that filter (e.g., `foo,bar,baz` becomes `['foo', 'bar', * 'baz']`). * - * @param Request $request * @param array $queryDescripters the set of dimensions the user is * authorized to see based on their assigned * ACLs. * @param string $filterKey the label of a dimension. - * @param string $filerValuesStr a comma-separated string. + * @param string $filterValuesStr a comma-separated string. * @return array - * @throws BadRequestHttpException if the filter key is an invalid - * dimension name. + * @throws BadRequestHttpException if the filter key is an invalid dimension name. + * @see getRawData()), and return the parsed array + * of values for that filter (e.g., `foo,bar,baz` becomes `['foo', 'bar', + * 'baz']`). + * */ private function validateRawDataFilterParam( - $queryDescripters, - $filterKey, - $filterValuesStr - ) { + array $queryDescripters, + string $filterKey, + string $filterValuesStr + ): array + { if (!in_array($filterKey, array_keys($queryDescripters))) { throw new BadRequestHttpException( - 'Invalid filter key \'' . $filterKey . '\'.' + 'Invalid filter key \'' . $filterKey . '\'.', null ); } - $filterValuesArray = explode(',', $filterValuesStr); - return $filterValuesArray; + return explode(',', $filterValuesStr); + } + + /** + * Helper function that creates a Response object that will result in a file download on the client. + * + * @param string $content + * @param string $filename + * @param string|null $mimetype + * @return Response + */ + protected function sendAttachment(string $content, string $filename, string $mimetype = null): Response + { + if ($mimetype === null) { + $finfo = new \finfo(FILEINFO_MIME_TYPE); + $mimetype = $finfo->buffer($content); + } + + $response = new Response( + $content, + Response::HTTP_OK, + ['Content-Type' => $mimetype] + ); + $response->headers->set( + 'Content-Disposition', + $response->headers->makeDisposition( + ResponseHeaderBag::DISPOSITION_ATTACHMENT, + $filename + ) + ); + + return $response; + } + + /** + * Endpoint to get the maximum number of rows that can be returned in a + * single response from the raw data endpoint + * + * + * + * @param Request $request + * + * @return JsonResponse + * + * @throws Exception if there is no setting for 'rest_raw_row_limit' in the 'datawarehouse' section of + * portal_settings.ini. + */ + #[Route('/warehouse/raw-data/limit', methods: ['GET'])] + #[Route('{prefix}warehouse/raw-data/limit', requirements: ['prefix' => '.*'], methods: ['GET'])] + public function getRawDataLimit(Request $request): JsonResponse + { + $this->tokenHelper->authenticate($request); + + $limit = $this->getConfiguredRawDataLimit(); + + return $this->json([ + 'success' => true, + 'data' => $limit + ]); + } + + /** + * Get the value configured in the portal settings for the maximum number + * of rows that can be returned in a single response from the raw data + * endpoint. + * + * @return int + * @throws Exception if the 'datawarehouse' section and/or the + * 'rest_raw_row_limit' option have not been set in the + * portal configuration. + */ + private function getConfiguredRawDataLimit(): int + { + return intval($this->parameters->get('xdmod.portal_settings.datawarehouse.rest_raw_row_limit')); } } diff --git a/classes/Rest/Controllers/WarehouseExportControllerProvider.php b/src/Controller/WarehouseExportController.php similarity index 69% rename from classes/Rest/Controllers/WarehouseExportControllerProvider.php rename to src/Controller/WarehouseExportController.php index d93f2510ed..b7d2540e44 100644 --- a/classes/Rest/Controllers/WarehouseExportControllerProvider.php +++ b/src/Controller/WarehouseExportController.php @@ -1,103 +1,78 @@ '.*'])] +class WarehouseExportController extends BaseController { - // Constants used in log messages. - const LOG_MODULE = 'data-warehouse-export'; - /** - * @var DataWarehouse\Export\QueryHandler + * */ - private $queryHandler; + private const LOG_MODULE = 'data-warehouse-export'; + /** - * @var DataWarehouse\Export\RealmManager + * @var RealmManager */ - private $realmManager; + private RealmManager $realmManager; /** - * @var LoggerInterface + * @var QueryHandler */ - private $logger; + private QueryHandler $queryHandler; - public function __construct(array $params = []) + /** + * @throws Exception if unable to instantiate the logger. + */ + public function __construct(LoggerInterface $logger, Environment $twig, Tokens $tokenHelper, ContainerBagInterface $parameters) { - parent::__construct($params); - $this->logger = Log::factory( - 'data-warehouse-export-rest', - [ - 'console' => false, - 'file' => false, - 'mail' => false - ] - ); + parent::__construct($logger, $twig, $tokenHelper, $parameters); + $this->realmManager = new RealmManager(); $this->queryHandler = new QueryHandler($this->logger); } - /** - * Set up data warehouse export routes. - * - * @param Application $app - * @param ControllerCollection $controller - */ - public function setupRoutes( - Application $app, - ControllerCollection $controller - ) { - $root = $this->prefix; - $current = get_class($this); - $conversions = '\Rest\Utilities\Conversions'; - - $controller->get("$root/realms", "$current::getRealms"); - $controller->post("$root/request", "$current::createRequest"); - $controller->get("$root/requests", "$current::getRequests"); - $controller->delete("$root/requests", "$current::deleteRequests"); - - $controller->get("$root/download/{id}", "$current::getExportedDataFile") - ->assert('id', '\d+') - ->convert('id', "$conversions::toInt"); - - $controller->delete("$root/request/{id}", "$current::deleteRequest") - ->assert('id', '\d+') - ->convert('id', "$conversions::toInt"); - } /** - * Get all the realms available for exporting for the current user. * * @param Request $request - * @param Application $app - * @return \Symfony\Component\HttpFoundation\JsonResponse - * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException + * @return Response + * @throws Exception if user is not authorized to access this route. */ - public function getRealms(Request $request, Application $app) + #[Route('/realms', methods: ['GET'])] + public function getRealms(Request $request): Response { $user = null; // We need to wrap the token authentication because we want the token authentication to be optional, proceeding // to the normal session authentication if a token is not provided. try { - $user = Tokens::authenticate($request); + $user = $this->tokenHelper->authenticate($request, false); } catch (Exception $e) { // NOOP } @@ -121,7 +96,7 @@ function ($realm) use ($config) { $this->realmManager->getRealmsForUser($user) ); - return $app->json( + return $this->json( [ 'success' => true, 'data' => array_values($realms), @@ -131,18 +106,16 @@ function ($realm) use ($config) { } /** - * Get all the existing export requests for the current user. - * * @param Request $request - * @param Application $app - * @return \Symfony\Component\HttpFoundation\JsonResponse - * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException + * @return Response + * @throws Exception */ - public function getRequests(Request $request, Application $app) + #[Route('/requests', methods: ['GET'])] + public function getRequests(Request $request): Response { $user = $this->authorize($request); $results = $this->queryHandler->listUserRequestsByState($user->getUserId()); - return $app->json( + return $this->json( [ 'success' => true, 'data' => $results, @@ -152,15 +125,15 @@ public function getRequests(Request $request, Application $app) } /** - * Create a new export request for the current user. * * @param Request $request - * @param Application $app - * @return \Symfony\Component\HttpFoundation\JsonResponse - * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException + * @return Response + * @throws UnauthorizedHttpException if user is not authorized to access this route. * @throws BadRequestHttpException + * @throws Exception */ - public function createRequest(Request $request, Application $app) + #[Route('/request', methods: ['POST'])] + public function createRequest(Request $request): Response { $user = $this->authorize($request); $realm = $this->getStringParam($request, 'realm', true); @@ -205,17 +178,17 @@ function ($realm) { try { $id = $this->queryHandler->createRequestRecord( - $user->getUserId(), + $user->getUserID(), $realm, $startDate->format('Y-m-d'), $endDate->format('Y-m-d'), $format ); } catch (Exception $e) { - throw new BadRequestHttpException('Failed to create export request: ' . $e->getMessage()); + throw new BadRequestHttpException('Failed to create export request'); } - return $app->json([ + return $this->json([ 'success' => true, 'message' => 'Created export request', 'data' => [['id' => $id]], @@ -224,23 +197,24 @@ function ($realm) { } /** - * Get the requested data. + * * * @param Request $request - * @param Application $app * @param int $id - * @return \Symfony\Component\HttpFoundation\BinaryFileResponse - * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException - * @throws AccessDeniedHttpException - * @throws NotFoundHttpException - * @throws BadRequestHttpException - */ - public function getExportedDataFile(Request $request, Application $app, $id) + * @return Response + * @throws AccessDeniedHttpException if the file for the request identified by the provided id is not readable. + * @throws NotFoundHttpException if there were no requests for the provided id. + * @throws NotFoundHttpException if the file for the request identified by the provided id is not found on the file system. + * @throws BadRequestHttpException if the request that corresponds to the provided id is not in the Available state. + * @throws Exception if the user is not authorized for this route. + */ + #[Route('/download/{id}', requirements: ["id" => "\d+"], methods: ['GET'])] + public function getExportedDataFile(Request $request, int $id): Response { $user = $this->authorize($request); $requests = array_filter( - $this->queryHandler->listUserRequestsByState($user->getUserId()), + $this->queryHandler->listUserRequestsByState($user->getUserID()), function ($request) use ($id) { return $request['id'] == $id; } @@ -283,8 +257,7 @@ function ($request) use ($id) { if ($request['downloaded_datetime'] === null) { $this->queryHandler->updateDownloadedDatetime($request['id']); } - - return $app->sendFile( + return new BinaryFileResponse( $file, 200, [ @@ -301,16 +274,17 @@ function ($request) use ($id) { * Delete a single request. * * @param Request $request - * @param Application $app - * @param int $id - * @return \Symfony\Component\HttpFoundation\JsonResponse - * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException + * @param string $id + * @return Response + * @throws UnauthorizedHttpException * @throws NotFoundHttpException + * @throws \Exception */ - public function deleteRequest(Request $request, Application $app, $id) + #[Route('/request/{id}', requirements: ["id" => "\w+"], methods: ['DELETE'])] + public function deleteRequest(Request $request, string $id): Response { $user = $this->authorize($request); - $count = $this->queryHandler->deleteRequest($id, $user->getUserId()); + $count = $this->queryHandler->deleteRequest($id, $user->getUserID()); if ($count === 0) { throw new NotFoundHttpException('Export request not found'); @@ -324,7 +298,7 @@ public function deleteRequest(Request $request, Application $app, $id) 'Users.id' => $user->getUserId() ]); - return $app->json([ + return $this->json([ 'success' => true, 'message' => 'Deleted export request', 'data' => [['id' => $id]], @@ -338,19 +312,24 @@ public function deleteRequest(Request $request, Application $app, $id) * The request body content must be a JSON encoded array of request IDs. * * @param Request $request - * @param Application $app - * @return \Symfony\Component\HttpFoundation\JsonResponse - * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException - * @throws NotFoundHttpException + * @return Response + * @throws Exception if the user is not authorized to access this route. + * @throws BadRequestHttpException if the provided request ids are not in a json decodable format + * @throws BadRequestHttpException if the provided request ids are not in a json array. + * @throws BadRequestHttpException if any of the provided request ids are not integers. + * @throws HttpException if the sql delete operation fails. + * @throws NotFoundHttpException if any of the provided request ids are not found. + * */ - public function deleteRequests(Request $request, Application $app) + #[Route('/requests', methods: ['DELETE'])] + public function deleteRequests(Request $request): Response { $user = $this->authorize($request); $requestIds = []; try { - $requestIds = @json_decode($request->getContent()); + $requestIds = @json_decode($request->get('ids')); if ($requestIds === null) { throw new Exception('Failed to decode JSON'); @@ -401,7 +380,7 @@ public function deleteRequests(Request $request, Application $app) throw new BadRequestHttpException('Failed to delete export requests'); } - return $app->json([ + return $this->json([ 'success' => true, 'message' => 'Deleted export requests', 'data' => array_map( diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 0000000000..9f616adfe4 --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,143 @@ +username = $username; + $this->xdRoles = $roles; + $this->userId = $userId; + $this->token = $token; + $this->password = $password; + } + + + /** + * @inheritDoc + **/ + public function getRoles(): array + { + $roles = $this->xdRoles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + if (in_array('mgr', $this->xdRoles)) { + $roles[] = 'ROLE_ALLOWED_TO_SWITCH'; + $roles[] = 'ROLE_ADMIN'; + } + + return array_unique($roles); + } + + /** + * @inheritDoc + **/ + public function getPassword(): ?string + { + return $this->password; + } + + /** + * @inheritDoc + **/ + public function eraseCredentials(): void + { + // TODO: Implement eraseCredentials() method. + } + + /** + * @return string + */ + public function getUsername(): string + { + return $this->username; + } + + /** + * @inheritDoc + **/ + public function getUserIdentifier(): string + { + return $this->username; + } + + /** + * @return int + */ + public function getUserId(): int + { + return $this->userId; + } + + /** + * @return string + */ + public function getToken(): string + { + return $this->token; + } + + /** + * @return bool + */ + public function isPublicUser(): bool + { + return in_array('pub', $this->xdRoles); + } + + /** + * @param \XDUser $xdUser + * @return User + */ + public static function fromXDUser(\XDUser $xdUser): User + { + return new User( + $xdUser->getUsername(), + $xdUser->getRoles(), + $xdUser->getUserID(), + $xdUser->getToken(), + $xdUser->getPassword() + ); + } +} diff --git a/src/EnvVar/DefaultXdmodEnvVarLoader.php b/src/EnvVar/DefaultXdmodEnvVarLoader.php new file mode 100644 index 0000000000..d2199497b0 --- /dev/null +++ b/src/EnvVar/DefaultXdmodEnvVarLoader.php @@ -0,0 +1,27 @@ +parameters->get('xdmod.portal_settings.general.application_secret'); + $debugMode = $this->parameters->get('xdmod.portal_settings.general.debug_mode'); + $appEnv = $debugMode === null || $debugMode === 'off' ? 'prod' : 'dev'; + return [ + 'APP_ENV' => $appEnv, + 'APP_SECRET' => $appSecret ?? hash('sha512', (string) time()) + ]; + } +} diff --git a/src/EnvVar/ReCaptchaEnvVarLoader.php b/src/EnvVar/ReCaptchaEnvVarLoader.php new file mode 100644 index 0000000000..6f778974fc --- /dev/null +++ b/src/EnvVar/ReCaptchaEnvVarLoader.php @@ -0,0 +1,31 @@ + $siteKey, + 'GOOGLE_RECAPTCHA_SECRET' => $privateKey + ]; + } +} diff --git a/src/Errors/ErrorController.php b/src/Errors/ErrorController.php new file mode 100644 index 0000000000..0b7d252a63 --- /dev/null +++ b/src/Errors/ErrorController.php @@ -0,0 +1,75 @@ +logger = $logger; + parent::__construct($kernel, $controller, $errorRenderer); + } + + /** + * Specifically designed to work with instances of FlattenException. We return a JsonResponse due to XDMoD expecting + * errors in this way. + * + * @param Throwable $exception + * @return Response + */ + public function __invoke(Throwable $exception): Response + { + $this->logger->error('Exception Code: '.$exception->getCode()); + $this->logger->error('Message: '.$exception->getMessage()); + $this->logger->error('Origin: '.$exception->getFile().' (line '.$exception->getLine().')'); + + $stringTrace = (get_class($exception) == 'UniqueException') ? $exception->getVerboseTrace() : $exception->getTraceAsString(); + + $this->logger->error("Trace:\n".$stringTrace."\n-------------------------------------------------------"); + + $httpCode = 500; + $headers = array(); + $isServerContext = isset($_SERVER['SERVER_PROTOCOL']); + if ($isServerContext) { + $uncheckedExceptionHttpCode = null; + if ($exception instanceof XDException) { + $uncheckedExceptionHttpCode = $exception->httpCode; + $headers = $exception->headers; + } elseif ($exception instanceof \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface) { + $uncheckedExceptionHttpCode = $exception->getStatusCode(); + $headers = $exception->getHeaders(); + } + + if ($uncheckedExceptionHttpCode !== null) { + if (array_key_exists($uncheckedExceptionHttpCode, HttpCodeMessages::$messages)) { + $httpCode = $uncheckedExceptionHttpCode; + } + } + } + + $message = $exception->getMessage(); + $userPos = strpos($message, 'User'); + $alreadyExistsPos = strpos($message, 'already exists'); + if ($userPos && $alreadyExistsPos) { + return new RedirectResponse('/'); + } + + return new JsonResponse(buildError($exception), $httpCode, $headers); + } + +} diff --git a/src/EventListeners/CORSListener.php b/src/EventListeners/CORSListener.php new file mode 100644 index 0000000000..70f05370c8 --- /dev/null +++ b/src/EventListeners/CORSListener.php @@ -0,0 +1,40 @@ +getRequest(); + $response = $event->getResponse(); + + $origin = $request->headers->get('Origin'); + if ($origin !== null) { + try { + $corsDomains = \xd_utilities\getConfiguration('cors', 'domains'); + if (!empty($corsDomains)) { + $allowedCorsDomains = explode(',', $corsDomains); + if (in_array($origin, $allowedCorsDomains)) { + // If these headers change similar updates will need to be made to the `error` section below + $response->headers->set('Access-Control-Allow-Origin', $origin); + $response->headers->set('Access-Control-Allow-Headers', 'x-requested-with, content-type'); + $response->headers->set('Access-Control-Allow-Credentials', 'true'); + $response->headers->set('Vary', 'Origin'); + } + } + } catch (\Exception $e) { + // this catches if the section or config item does not exist + // in that case we just carry on + } + } + } +} diff --git a/src/EventListeners/LogoutListener.php b/src/EventListeners/LogoutListener.php new file mode 100644 index 0000000000..a562957085 --- /dev/null +++ b/src/EventListeners/LogoutListener.php @@ -0,0 +1,29 @@ +logger = $logger; + } + public function onLogout(LogoutEvent $event): void + { + $this->logger->debug('*** Logging Out w/ Logout Listener *** '); + $request = $event->getRequest(); + $token = $request->getSession()->get('xdmod_token'); + \XDSessionManager::logoutUser($token); + $request->getSession()->invalidate(); + } +} + diff --git a/src/Helper/HttpCodeMessages.php b/src/Helper/HttpCodeMessages.php new file mode 100644 index 0000000000..3f63490267 --- /dev/null +++ b/src/Helper/HttpCodeMessages.php @@ -0,0 +1,54 @@ + 'Continue', + 101 => 'Switching Protocols', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + // 306 Unused + 307 => 'Temporary Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported' + ); +} + +; diff --git a/src/Helper/PasswordResetService.php b/src/Helper/PasswordResetService.php new file mode 100644 index 0000000000..ffd464f6c2 --- /dev/null +++ b/src/Helper/PasswordResetService.php @@ -0,0 +1,62 @@ +generateRID(); + + $title = $this->parameters->get('xdmod.portal_settings.general.title'); + $subject = sprintf('%s: Password Reset', $title); + $siteAddress = $this->parameters->get('xdmod.portal_settings.general.site_address'); + if (!string_ends_with($siteAddress, '/')) { + $siteAddress = "$siteAddress/"; + } + + $body = $this->twig->render( + 'twig/emails/password_reset.html.twig', + [ + 'first_name' => $user->getFirstName(), + 'username' => $user->getUsername(), + 'reset_link' => sprintf('%scontrollers/password_reset.php?rid=%s', $siteAddress, $rid), + 'expiration' => date('%c %Z', (int)explode('|', $rid)[1]), + 'maintainer_signature' => MailWrapper::getMaintainerSignature(), + ] + ); + + MailWrapper::sendMail([ + 'toAddress' => $user->getEmailAddress(), + 'subject' => $subject, + 'body' => $body + ]); + } +} diff --git a/src/Helper/SymfonyCommandHelper.php b/src/Helper/SymfonyCommandHelper.php new file mode 100644 index 0000000000..dadee13d6e --- /dev/null +++ b/src/Helper/SymfonyCommandHelper.php @@ -0,0 +1,92 @@ +bootEnv($envPath); + } catch(FormatException | PathException $e) { + throw new \RuntimeException('Unable to load environment file', $e->getCode(), $e); + } + + // Setup our Kernel / Application + $kernel = new Kernel($env, $debug); + $application = new Application($kernel); + + // we set this so that it doesn't `exit` whatever php script is calling this function. + $application->setAutoExit(false); + + // Set the Symfony command to execute. + array_unshift($options, $command); + + $input = new ArrayInput($options); + $output = new BufferedOutput(); + try { + $statusCode = $application->run($input, $output); + return [$statusCode, $output->fetch()]; + } catch(\Exception $e) { + throw new \RuntimeException("Error while running Symfony Command", $e->getCode(), $e); + } + } + + public static function dumpDotEnv(): void + { + // Make sure to clear the cache before dumping the dotenv so we start clean. + SymfonyCommandHelper::executeCommand('cache:clear'); + + // Dump dotenv data so we don't read .env each time in prod. + // Note: this means that if you want to start debugging stuff you'll need to delete the generated .env. + SymfonyCommandHelper::executeCommand('dotenv:dump'); + } +} diff --git a/src/Kernel.php b/src/Kernel.php new file mode 100644 index 0000000000..8844de28f4 --- /dev/null +++ b/src/Kernel.php @@ -0,0 +1,26 @@ +logger = $logger; + $this->httpUtils = $httpUtils; + $this->urlGenerator = $urlGenerator; + $this->options = array_merge([ + 'username_parameter' => 'username', + 'password_parameter' => 'password', + 'check_paths' => ['xdmod_login', 'xdmod_new_login'], + 'failure_path' => 'xdmod_home', + 'post_only' => true, + 'form_only' => true, + ], $options); + } + + + /** + * This method is overwritten because we specifically only want this Authenticator to apply when the request is a + * POST with a content-type of application/x-www-form-urlencoded w/ a path that matches our `check_path`. + * + * @param Request $request + * @return bool + */ + public function supports(Request $request): bool + { + $postOnly = (!$this->options['post_only'] || $request->isMethod('POST')); + $formOnly = (!$this->options['form_only'] || 'form' === $request->getContentTypeFormat()); + if ($request->attributes->has('_route')) { + $requestPath = $request->attributes->get('_route'); + } else { + $requestPath = $request->getPathInfo(); + } + $this->logger->debug('Checking Path', [$requestPath]); + + $found = false; + foreach ($this->options['check_paths'] as $checkPath) { + $requestPathMatches = $this->httpUtils->checkRequestPath($request, $checkPath); + if ($requestPathMatches) { + $found = true; + break; + } + } + + $this->logger->debug('Checking if FormLoginAuthenticator supports request', [$postOnly, $found, $formOnly]); + + return $postOnly && $found && $formOnly; + } + + /** + * Create a passport for the current request. + * + * The passport contains the user, credentials and any additional information + * that has to be checked by the Symfony Security system. For example, a login + * form authenticator will probably return a passport containing the user, the + * presented password and the CSRF token value. + * + * You may throw any AuthenticationException in this method in case of error (e.g. + * a UserNotFoundException when the user cannot be found). + * + * @param Request $request + * @return Passport + */ + public function authenticate(Request $request): Passport + { + $this->logger->debug('Initiating Form Login Authentication', [$request]); + + $credentials = $this->getCredentials($request); + $this->logger->debug('Attempting to login user ' . $credentials['username'], $credentials); + + return new Passport( + new UserBadge($credentials['username']), + new PasswordCredentials($credentials['password']), + [new RememberMeBadge()] + ); + } + + /** + * Retrieve user credentials from the provided Request. Validates that the username length is less than or equal to + * Security::MAX_USERNAME_LENGTH and if not it throws a BadCredentialsException. If credentials are able to be + * successfully retrieved and they are valid than the Security::LAST_USERNAME session variable is set to the + * retrieved username. + * + * @param Request $request + * @return array containing the username / password retrieved from the provided Request. + * @throws BadRequestHttpException if the username parameter is not a string, or if it's an object that does not provide a __toString method. + * @throws BadCredentialsException if the provided username is longer than Security::MAX_USERNAME_LENGTH. + */ + private function getCredentials(Request $request): array + { + $credentials = []; + + if ($this->options['post_only']) { + $credentials['username'] = ParameterBagUtils::getParameterBagValue($request->request, $this->options['username_parameter']); + $credentials['password'] = ParameterBagUtils::getParameterBagValue($request->request, $this->options['password_parameter']) ?? ''; + } else { + $credentials['username'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['username_parameter']); + $credentials['password'] = ParameterBagUtils::getRequestParameterValue($request, $this->options['password_parameter']) ?? ''; + } + + if (!\is_string($credentials['username']) && (!\is_object($credentials['username']) || !method_exists($credentials['username'], '__toString'))) { + throw new BadRequestHttpException(sprintf('The key "%s" must be a string, "%s" given.', $this->options['username_parameter'], \gettype($credentials['username']))); + } + + $credentials['username'] = trim($credentials['username']); + + if (\strlen($credentials['username']) > Security::MAX_USERNAME_LENGTH) { + $this->logger->error('Username is to long', $credentials); + throw new BadCredentialsException('Invalid username.'); + } + + $request->getSession()->set(Security::LAST_USERNAME, $credentials['username']); + + return $credentials; + } + + /** + * We do the translation from Symfony User to XDUser here by looking for an XDUser that has the same username as + * the authenticated Symfony User. When found, we set the `xdUser` session variable equal to the XDUser's user id. + * + * This should return the Response sent back to the user, like a + * RedirectResponse to the last page they visited. + * + * If you return null, the current request will continue, and the user + * will be authenticated. This makes sense, for example, with an API. + * + * @param Request $request + * @param TokenInterface $token + * @param string $firewallName + * @return Response + * @throws \Exception if unable to find an XDUser that matches the provided Symfony User + */ + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { + return new RedirectResponse($targetPath); + } + $user = $token->getUser(); + $xdUser = XDUser::getUserByUserName($user->getUserIdentifier()); + $xdUser->postLogin(); + $request->getSession()->set('xdUser', $xdUser->getUserID()); + $response = new JsonResponse([ + 'success' => true, + 'results' => [ + 'token' => $xdUser->getToken(), + 'name' => $xdUser->getFormalName() + ] + ]); + $response->headers->setCookie(new Cookie('xdmod_token', $xdUser->getToken())); + return $response; + } + + /** + * Return the URL to the login page. + * @param Request $request + * @return string the login url that this FormLoginAuthenticator supports. + */ + protected function getLoginUrl(Request $request): string + { + return $this->httpUtils->generateUri($request, $this->options['check_path']); + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response + { + if ($request->hasSession()) { + $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception); + } + return new JsonResponse([], 401); + } + + /** + * This is required for the Authenticator to be set as an entrypoint. We need to set an entrypoint because we have + * multiple authenticators setup for our main firewall ( FormLoginAuthenticator, TokenAuthenticator, SSOAuthenticator ) + * + * @param Request $request + * @param AuthenticationException|null $authException + * @return RedirectResponse + */ + public function start(Request $request, AuthenticationException $authException = null): Response + { + return new RedirectResponse($this->urlGenerator->generate('xdmod_home')); + } +} diff --git a/src/Security/Authenticators/SimpleSamlPhpAuthenticator.php b/src/Security/Authenticators/SimpleSamlPhpAuthenticator.php new file mode 100644 index 0000000000..0398efb1d5 --- /dev/null +++ b/src/Security/Authenticators/SimpleSamlPhpAuthenticator.php @@ -0,0 +1,204 @@ +logger = $logger; + $this->httpUtils = $httpUtils; + $this->urlGenerator = $urlGenerator; + $this->parameters = $parameters; + + $this->sources = Source::getSources(); + $this->logger->debug('Auth Sources', [$this->sources]); + if (!empty($this->sources)) { + try { + $authSource = \xd_utilities\getConfiguration('authentication', 'source'); + $this->logger->debug('Found Auth Source', [$authSource]); + } catch (\Exception $e) { + $authSource = null; + } + if (!is_null($authSource) && array_search($authSource, $this->sources) !== false) { + $this->authSourceName = $authSource; + $this->authSource = new \SimpleSAML\Auth\Simple($authSource); + } else { + $this->authSourceName = $this->sources[0]; + $this->authSource = new \SimpleSAML\Auth\Simple($this->authSourceName); + } + } + } + + + /** + * Determine whether or not this authenticator supports the provided $request. + * + * @param Request $request + * @return bool|null + * @throws \Psr\Container\ContainerExceptionInterface + * @throws \Psr\Container\NotFoundExceptionInterface + */ + public function supports(Request $request): ?bool + { + // We only allow SSO Auth when the request is a GET for the home page. + if (!$request->isMethod('GET') || + !$this->httpUtils->checkRequestPath($request, 'xdmod_home')) { + return false; + } + + $referer = $request->headers->get('Referer'); + // And if the referer is not empty. + if (empty($referer)) { + return false; + } + + $authReferrer = $this->parameters->get('xdmod.portal_settings.sso.auth_referer'); + $generalSiteAddress = $this->parameters->get('xdmod.portal_settings.general.site_address'); + if (empty($authReferrer) && !empty($generalSiteAddress)) { + $authReferrer = sprintf( + '%s/simplesaml/module.php/authoauth2/linkback.php', + $generalSiteAddress + ); + } + return str_starts_with($referer, $authReferrer); + } + + public function authenticate(Request $request): Passport + { + if ($this->authSource->isAuthenticated()) { + $attributes = $this->authSource->getAttributes(); + $username = $attributes['username'][0]; + $logger = $this->logger; + return new SelfValidatingPassport( + new UserBadge( + $username, + function($userName, $samlAttributes) use ($logger) { + $logger->debug('Loading SimpleSAMLPHP User'); + + function getOrganizationId($samlAttrs, $personId) + { + if ($personId !== -1 ) { + return Organizations::getOrganizationIdForPerson($personId); + } elseif(!empty($samlAttrs['organization'])) { + return Organizations::getIdByName($samlAttrs['organization'][0]); + } + return -1; + } + + $xdmodUserId = \XDUser::userExistsWithUsername($userName); + $logger->debug('XDMoD UserID ', [$xdmodUserId]); + if ($xdmodUserId !== INVALID) { + $user = \XDUser::getUserByID($xdmodUserId); + $user->setSSOAttrs($samlAttributes); + return User::fromXDUser($user); + } + $logger->debug('Creating New SSO User!'); + // If we've gotten this far then we're creating a new user. Proceed with gathering the + // information we'll need to do so. + $emailAddress = isset($samlAttributes['email_address']) ? $samlAttributes['email_address'][0] : NO_EMAIL_ADDRESS_SET; + $systemUserName = isset($samlAttributes['system_username']) ? $samlAttributes['system_username'][0] : $userName; + $firstName = isset($samlAttributes['first_name']) ? $samlAttributes['first_name'][0] : 'UNKNOWN'; + $middleName = isset($samlAttributes['middle_name']) ? $samlAttributes['middle_name'][0] : null; + $lastName = isset($samlAttributes['last_name']) ? $samlAttributes['last_name'][0] : null; + $personId = \DataWarehouse::getPersonIdFromPII($systemUserName, $samlAttributes['organization'][0]); + + // Attempt to identify which organization this user should be associated with. Prefer + // using the personId if not unknown, then fall back to the saml attributes if the + // 'organization' property is present, and finally defaulting to the Unknown organization + // if none of the preceding conditions are met. + $userOrganization = getOrganizationId($samlAttributes, $personId); + + try { + $newUser = new \XDUser( + $userName, + null, + $emailAddress, + $firstName, + $middleName, + $lastName, + array(ROLE_ID_USER), + ROLE_ID_USER, + $userOrganization, + $personId, + $samlAttributes + ); + } catch (\Exception $e) { + throw new \Exception('An account is currently configured with this information, please contact an administrator.'); + } + + $newUser->setUserType(SSO_USER_TYPE); + + try { + $newUser->saveUser(); + } catch (\Exception $e) { + $this->logger->error('User creation failed: ' . $e->getMessage()); + throw $e; + } + + return User::fromXDUser($newUser); + }, + $attributes + ) + ); + } + throw new UserNotFoundException(); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + $this->logger->info('SimpleSAMLPHP Authentication Succeeded!'); + return null; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + $this->logger->info('SimpleSAMLPHP Authentication Failed!', [$exception]); + return null; + } + + public function start(Request $request, ?AuthenticationException $authException = null): Response + { + return new RedirectResponse($this->urlGenerator->generate('xdmod_home')); + } +} diff --git a/classes/Models/Services/Tokens.php b/src/Security/Helpers/Tokens.php similarity index 57% rename from classes/Models/Services/Tokens.php rename to src/Security/Helpers/Tokens.php index 290d456680..0611c9d93b 100644 --- a/classes/Models/Services/Tokens.php +++ b/src/Security/Helpers/Tokens.php @@ -1,88 +1,142 @@ logger = $logger; + } /** - * Attempt to authenticate a user via an authentication token included in a given request. + * Perform token authentication for the provided $userId & $token combo. If the authentication is successful, an + * XDUser object will be returned for the provided $userId. If not, an exception will be thrown. * - * @param \Symfony\Component\HttpFoundation\Request $request + * @param int|string $userId The id used to look up the the users hashed token. + * @param string $token The value to be checked against the retrieved hashed token. * - * @return XDUser the succesfully authenticated user. + * @return XDUser for the provided $userId, if the authentication is successful else an exception will be thrown. * - * @throws \Exception if unable to retrieve a database connection. - * @throws UnauthorizedHttpException if the token is missing, malformed, invalid, or expired. + * @throws Exception if unable to retrieve a database connection. + * @throws UnauthorizedHttpException if no token can be found for the provided $userId, + * if the stored token for $userId has expired, or + * if the provided $token doesn't match the stored hash. */ - public static function authenticate($request) + /*public function authenticate($userId, string $token): ?XDUser { - $token = null; - // Try to extract the token from the header. - if ($request->headers->has('Authorization')) { - $token = self::getTokenFromHeader($request->headers->get('Authorization')); + $this->logger->info(sprintf('Beginning Authentication for %s', $userId)); + + $db = DB::factory('database'); + $query = <<query($query, array(':user_id' => $userId)); + + if (count($row) === 0) { + $this->logger->debug('User (%s) does not have an active token.'); + throw new UnauthorizedHttpException(Tokens::HEADER_KEY, 'Invalid API token.'); } - // If the token is not in the header, then fall back to extracting from - // the GET/POST params. - if (empty($token)) { - $token = $request->get('Bearer'); + + $expectedToken = $row[0]['token']; + $expiresOn = $row[0]['expires_on']; + $dbUserId = $row[0]['user_id']; + + // Check that expected token isn't expired. + $now = new DateTime(); + $expires = new DateTime($expiresOn); + if ($expires < $now) { + $this->logger->debug(sprintf('User\'s (%s) token is expired.', $userId)); + throw new UnauthorizedHttpException(Tokens::HEADER_KEY, 'Token has expired.', null, 0); } - // If we still haven't found a token, then authentication fails. - if (empty($token)) { - self::throwUnauthorized(self::MISSING_TOKEN_MESSAGE); + + // finally check that the provided token matches it's stored hash. + if (!password_verify($token, $expectedToken)) { + $this->logger->debug(sprintf('User\'s (%s) token is invalid.', $userId)); + throw new UnauthorizedHttpException(Tokens::HEADER_KEY, 'Invalid token.'); } - return self::authenticateToken($token, $request->getPathInfo()); - } + + // and if we've made it this far we can safely return the requested Users data. + return XDUser::getUserByID($dbUserId); + }*/ /** * This function is a stop-gap that is meant to be used to protect controller endpoints until they can be moved to * the new REST stack. * - * @return XDUser the successfully authenticated user. + * @return XDUser|null if the authentication is successful then an XDUser instance for the authenticated user will + * be returned, if the authentication is not successful then null will be returned. * - * @throws \Exception if unable to retrieve a database connection. - * @throws UnauthorizedHttpException if the token is missing, malformed, invalid, or expired. + * @throws \Exception if there is a problem w/ authenticating the token for this request. */ - public static function authenticateController() + public function authenticate(Request $request, $strict = true): ?XDUser { $token = null; // Try to extract the token from the header. - $headers = getallheaders(); - if (!empty($headers['Authorization'])) { - $token = self::getTokenFromHeader($headers['Authorization']); + if ($request->headers->has('Authorization')) { + $token = self::getTokenFromHeader($request->headers->get('Authorization')); } // If the token is not in the header, then fall back to extracting from // the GET/POST params. if (empty($token)) { - if (isset($_GET['Bearer']) && is_string($_GET['Bearer'])) { - $token = $_GET['Bearer']; - } elseif (isset($_POST['Bearer']) && is_string($_POST['Bearer'])) { - $token = $_POST['Bearer']; - } + $token = $request->get('Bearer'); } + // If we still haven't found a token, then authentication fails. if (empty($token)) { - self::throwUnauthorized(self::MISSING_TOKEN_MESSAGE); + // if we're being strict about things, throw an exception + if ($strict) { + self::throwUnauthorized(self::MISSING_TOKEN_MESSAGE); + } + + // else, this is for endpoints that have optional token authentication. By returning null we allow normal + // authentication to continue. + return null; } - return self::authenticateToken($token); + + return self::authenticateToken($token, $request->getPathInfo()); } /** @@ -96,7 +150,7 @@ public static function authenticateController() * @throws \Exception if unable to retrieve a database connection. * @throws UnauthorizedHttpException if the token is missing, malformed, invalid, or expired. */ - private static function authenticateToken($rawToken, $endpoint = null) + private static function authenticateToken(string $rawToken, string $endpoint = null): XDUser { // Determine token type $tokenParts = explode('.', $rawToken); @@ -145,8 +199,9 @@ private static function authenticateToken($rawToken, $endpoint = null) * @return XDUser The successfully authenticated user. * * @throws UnauthorizedHttpException if the token is malformed, invalid, or expired + * @throws \Exception if there is an error encountered constructing the $expires DateTime. */ - private static function authenticateAPIToken($userId, $token) + private static function authenticateAPIToken($userId, $token): XDUser { $db = DB::factory('database'); $query = <<sub; @@ -227,9 +284,9 @@ private static function authenticateJSONWebToken($jwt) * @param string $header * @return string | null the token if the header has the 'Bearer' key, null otherwise. */ - public static function getTokenFromHeader($header) + public static function getTokenFromHeader(string $header): ?string { - if (0 !== strpos($header, 'Bearer ')) { + if (!str_starts_with($header, 'Bearer ')) { return null; } return substr($header, strlen('Bearer') + 1); @@ -242,7 +299,7 @@ public static function getTokenFromHeader($header) * @param string $message * @throws UnauthorizedHttpException */ - public static function throwUnauthorized($message) + public static function throwUnauthorized(string $message) { throw new UnauthorizedHttpException('Bearer', $message); } diff --git a/src/Security/TokenUserProvider.php b/src/Security/TokenUserProvider.php new file mode 100644 index 0000000000..3f34f9a8bf --- /dev/null +++ b/src/Security/TokenUserProvider.php @@ -0,0 +1,80 @@ +logger = $logger; + } + + /** + * {@inheritDoc} + */ + public function refreshUser(UserInterface $user): UserInterface + { + $this->logger->debug('Refreshing User', [$user]); + try { + return User::fromXDUser(XDUser::getUserByUserName($user->getUserIdentifier())); + } catch (\Exception $e) { + throw new UserNotFoundException(sprintf('No user found for username %s', $user->getUserIdentifier()), $e->getCode(), $e); + } + } + + /** + * {@inheritDoc} + */ + public function supportsClass(string $class): bool + { + return $class === User::class || is_subclass_of($class, User::class); + } + + /** + * {@inheritDoc} + */ + public function loadUserByUsername(string $username): UserInterface + { + try { + return User::fromXDUser( XDUser::getUserByUserName($username)); + } catch (\Exception $e) { + throw new UserNotFoundException(sprintf('No user found for username %s', $username), $e->getCode(), $e); + } + + } + + /** + * {@inheritDoc} + */ + public function loadUserByIdentifier(string $identifier): UserInterface + { + $user = XDUser::getUserByToken($identifier); + + if (null === $user) { + throw new UserNotFoundException(); + } + + return User::fromXDUser($user); + } +} diff --git a/src/Security/UsernameUserProvider.php b/src/Security/UsernameUserProvider.php new file mode 100644 index 0000000000..b9f9e91667 --- /dev/null +++ b/src/Security/UsernameUserProvider.php @@ -0,0 +1,146 @@ +logger = $logger; + } + + + /** + * @inheritDoc + */ + public function refreshUser(UserInterface $user): UserInterface + { + $this->logger->debug('Refreshing User ' . $user->getUserIdentifier(), [$user]); + try { + return $user; + } catch (\Exception $e) { + throw new UserNotFoundException($e->getMessage()); + } + + } + + /** + * @inheritDoc + */ + public function supportsClass(string $class): bool + { + return $class === User::class || is_subclass_of($class, User::class); + } + + /** + * @inheritDoc + * @throws \Exception + */ + public function loadUserByIdentifier(string $identifier): UserInterface + { + + $this->logger->debug("Loading User By Identifier: $identifier"); + $isSamlUser = $this->classesContains('saml', (new \Exception())->getTrace()); + try { + $xdUser = XDUser::getUserByUserName($identifier); + + if ($isSamlUser && $xdUser->getUserType() !== SSO_USER_TYPE) { + $this->logger->error('SSO User attempted to log in as a local user.'); + throw new InsufficientAuthenticationException(); + } + } catch (\Exception $e) { + $this->logger->debug("Loading User By Id instead"); + $xdUser = XDUser::getUserByID($identifier); + if ($isSamlUser && isset($xdUser) && $xdUser->getUserType() !== SSO_USER_TYPE) { + $this->logger->error('SSO User attempted to log in as a local user.'); + throw new InsufficientAuthenticationException(); + } + if (!isset($xdUser)) { + $this->logger->debug(sprintf('User %s not found', $identifier)); + throw new UserNotFoundException("Unable to find User identified by $identifier"); + } + } + + $this->logger->debug("XDUser found by username: {$xdUser->getUserID()} {$xdUser->getUsername()}"); + $foundUser = User::fromXDUser($xdUser); + $this->logger->debug(sprintf('Final User Found: %s %s', $foundUser->getUserIdentifier(), $foundUser->getPassword())); + return $foundUser; + } + + /** + * @inerhitDoc + */ + public function loadUserByUsername(string $username): ?UserInterface + { + $this->logger->debug("Loading User By Username: $username"); + return $this->loadUserByIdentifier($username); + } + + /** + * Upgrades the hashed password of a user, typically for using a better hash algorithm. + * + * This method should persist the new password in the user storage and update the $user object accordingly. + * Because you don't want your users not being able to log in, this method should be opportunistic: + * it's fine if it does nothing or if it fails without throwing any exception. + */ + public function upgradePassword(UserInterface|PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void + { + $this->logger->debug('Attempting to upgrade password'); + } + + /** + * @param $classPart + * @param $trace + * @return bool + */ + private function classesContains($classPart, $trace): bool + { + if (is_null($classPart)) { + return false; + } + $classes = $this->getCallingClasses($trace); + foreach($classes as $class) { + if (is_null($class)) { + continue; + } + $pos = strpos(strtolower($class), strtolower($classPart)); + if ($pos !== false && is_numeric($pos)) { + return true; + } + } + return false; + } + + private function getCallingClasses($trace): array + { + return array_reduce( + $trace, + function ($carry, $item) { + $value = array_key_exists('class', $item) ? $item['class'] : null; + $carry[] = $value; + return $carry; + }, + [] + ); + } +} diff --git a/symfony.lock b/symfony.lock new file mode 100644 index 0000000000..afaec77ff8 --- /dev/null +++ b/symfony.lock @@ -0,0 +1,166 @@ +{ + "doctrine/deprecations": { + "version": "1.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "87424683adc81d7dc305eefec1fced883084aab9" + } + }, + "google/recaptcha": { + "version": "1.3", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.1", + "ref": "e5a4aa21f2e98d7440ae9aab6b56e307f99dd084" + }, + "files": [ + "config/packages/google_recaptcha.yaml" + ] + }, + "nyholm/psr7": { + "version": "1.8", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "4a8c0345442dcca1d8a2c65633dcf0285dd5a5a2" + }, + "files": [ + "config/packages/nyholm_psr7.yaml" + ] + }, + "phpunit/phpunit": { + "version": "9.6", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "9.6", + "ref": "6a9341aa97d441627f8bd424ae85dc04c944f8b4" + }, + "files": [ + ".env.test", + "phpunit.dist.xml", + "tests/bootstrap.php" + ] + }, + "symfony/console": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461" + }, + "files": [ + "bin/console" + ] + }, + "symfony/flex": { + "version": "2.8", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.4", + "ref": "52e9754527a15e2b79d9a610f98185a1fe46622a" + }, + "files": [ + ".env", + ".env.dev" + ] + }, + "symfony/framework-bundle": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.4", + "ref": "32126346f25e1cee607cc4aa6783d46034920554" + }, + "files": [ + "config/packages/cache.yaml", + "config/packages/framework.yaml", + "config/preload.php", + "config/routes/framework.yaml", + "config/services.yaml", + "html/index.php", + "src/Controller/.gitignore", + "src/Kernel.php" + ] + }, + "symfony/maker-bundle": { + "version": "1.64", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" + } + }, + "symfony/monolog-bundle": { + "version": "3.10", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.7", + "ref": "aff23899c4440dd995907613c1dd709b6f59503f" + }, + "files": [ + "config/packages/monolog.yaml" + ] + }, + "symfony/routing": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.2", + "ref": "e0a11b4ccb8c9e70b574ff5ad3dfdcd41dec5aa6" + }, + "files": [ + "config/packages/routing.yaml", + "config/routes.yaml" + ] + }, + "symfony/security-bundle": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.4", + "ref": "2ae08430db28c8eb4476605894296c82a642028f" + }, + "files": [ + "config/packages/security.yaml", + "config/routes/security.yaml" + ] + }, + "symfony/twig-bundle": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.4", + "ref": "cab5fd2a13a45c266d45a7d9337e28dee6272877" + }, + "files": [ + "config/packages/twig.yaml", + "templates/base.html.twig" + ] + }, + "symfony/web-profiler-bundle": { + "version": "6.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.1", + "ref": "8b51135b84f4266e3b4c8a6dc23c9d1e32e543b7" + }, + "files": [ + "config/packages/web_profiler.yaml", + "config/routes/web_profiler.yaml" + ] + } +} diff --git a/templates/apache.conf b/templates/apache.conf index d6203529eb..27019c8afa 100644 --- a/templates/apache.conf +++ b/templates/apache.conf @@ -1,34 +1,3 @@ -# TEMPLATE Apache configuration file for Open XDMoD. This file should -# be copied to the Apache configuration directory and -# edited to specify the correct site-specific settings. -# -# On Rocky 8 and RHEL 8, this file should be copied -# to: -# /etc/httpd/conf.d/xdmod.conf -# -# For other Linux distributions consult the distribtion documentation -# to determine the path to the webserver configuration files. -# -# This template file must be modified to update site specific settings: -# -# The ServerName setting should be updated. -# -# The SSLCertificateFile and SSLCertificateKeyFile settings should -# be updated to specify paths to the valid SSL certificates for the -# site. -# -# Optionally the port number in the VirtualHost section can be updated -# from 443 to the desired listen port. -# -# The server name and port number in the Apache configuration must match -# the site_address and user_manual settings in the Open XDMoD portal_settings.ini -# configuration file. -# - -# If the server is not already configured to listen on port 443 then the -# following Listen command should be uncommented. -#Listen 443 - # The ServerName and ServerAdmin parameters should be updated. ServerName localhost @@ -52,32 +21,22 @@ Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" DocumentRoot /usr/share/xdmod/html - - Options FollowSymLinks - AllowOverride All - DirectoryIndex index.php - - - Require all granted - - - - - RewriteEngine On - RewriteRule (.*) index.php [L] + AllowOverride None + Require all granted + FallbackResource /index.php ## SimpleSAML Single Sign On authentication. - #SetEnv SIMPLESAMLPHP_CONFIG_DIR /etc/xdmod/simplesamlphp/config - #Alias /simplesaml /usr/share/xdmod/vendor/simplesamlphp/simplesamlphp/www - # - # Options FollowSymLinks - # AllowOverride All - # - # Require all granted - # - # +# SetEnv SIMPLESAMLPHP_CONFIG_DIR /usr/share/xdmod/vendor/simplesamlphp/simplesamlphp/config +# Alias /simplesaml /usr/share/xdmod/vendor/simplesamlphp/simplesamlphp/public +# +# Options FollowSymLinks +# AllowOverride All +# +# Require all granted +# +# # Update the path to rotatelogs if it is different on your system. ErrorLog "|/usr/sbin/rotatelogs -n 5 /var/log/xdmod/apache-error.log 1M" diff --git a/templates/portal_settings.template b/templates/portal_settings.template index cd2cbe6ef5..368fb4fa90 100644 --- a/templates/portal_settings.template +++ b/templates/portal_settings.template @@ -54,6 +54,7 @@ user_dashboard = "[:features_user_dashboard:]" [sso] ; Set to "on" to enable the local user option in login modal. show_local_login = "off" +auth_referer=https://xdmod:7000 [internal] dw_desc_cache = "off" diff --git a/templates/twig/about/federated.html.twig b/templates/twig/about/federated.html.twig new file mode 100644 index 0000000000..b8af89466f --- /dev/null +++ b/templates/twig/about/federated.html.twig @@ -0,0 +1,65 @@ +

Federated Open XDMoD

+

+ Federated XDMoD supports the collection and aggregation of data from a number of fully-functional and individually + managed XDMoD instances into a single federated instance of XDMoD capable of displaying federation-wide metrics. + Each participating institution deploys an XDMoD instance through which local data will be collected and shipped to a + central Federation Hub where it is aggregated to provide a federation-wide view of the data. + Data particular to an individual center is available from the Hub by applying filters and drill-downs. +

+

+

+ Diagram of an example Federated XDMoD deployment +
+ + + Example data flow from heterogeneous computing resources to an XDMoD federated hub. + XDMoD instances X and Y ingest data into their databases from the computing resources that they monitor. + Following ingestion on the satellite instances, job data are replicated to the federated hub's database, + where they are aggregated for use in the federated XDMoD user interface. + + +
+
+

+

+ A simple example use of the federated module is: + Three academic institutions each with their own HPC resource. + Each institution has its own XDMoD instance which contains the accounting data for only their HPC resource. + These institutions federate their data to a central hub. + HPC accounting data for all three HPC resources is shown on the central hub. + This central hub can then be used to report on the combined data. +

+

+ This example illustrates only one use case. + The federated module supports cloud data as well as HPC. Support for other data realms is planned. + There are no pre-defined limits on the number of instances that can be part of a federation. +

+

+ For more information see Section II of Federating XDMoD to + Monitor Affiliated Computing Resources. +

+

+ Documentation available at https://federated.xdmod.org. +

+

+ Source code and downloads at https://github.com/ubccr/xdmod-federated. +

+{% if federated_role is not empty %} + {% if federated_role == 'instance' %} +

This instance is part of a federation

+ Federation Hub: {{ hub_url }} + {% elseif federaged_role == 'hub' %} +

Instances that are part of this Federation

+
    + {% for instance in instances %} +
  • +

    {{ instance['url'] }}

    + last event retrieved ({{ instance['lastCloudEvent'] }}) +
  • + {% endfor %} +
+ {% endif %} +{% else %} + This installation is not part of a federation. +{% endif %} + diff --git a/templates/twig/about/links.html.twig b/templates/twig/about/links.html.twig new file mode 100644 index 0000000000..7a70163336 --- /dev/null +++ b/templates/twig/about/links.html.twig @@ -0,0 +1,40 @@ +

Links

+ + + + + + + + + + + + + +
+ + + + + + + +
+ + + + + + + +
+ + + + + + + +
+ diff --git a/templates/twig/about/open_xdmod.html.twig b/templates/twig/about/open_xdmod.html.twig new file mode 100644 index 0000000000..7dc9d1156f --- /dev/null +++ b/templates/twig/about/open_xdmod.html.twig @@ -0,0 +1,44 @@ +

Open XDMoD

+
+

While initially focused on the NSF XSEDE program, an open source version of XDMoD that provides similar functionality + for academic and industrial HPC centers is available and undergoing continued development, namely Open XDMoD. Open + XDMoD for use by academic and industrial HPC centers is available for download through GitHub (http://open.xdmod.org).

+

Highlights include:

+
    +
  • A graphical user interface with extensive graphic and analytical capability.
  • +
  • Detailed utilization metrics including number of jobs, CPU hours, wait times, job size, etc.
  • +
  • Customizable Metric Explorer where users can generate custom plots comparing multiple metrics
  • +
  • A custom report builder for the automatic generation of detailed periodic reports.
  • +
  • Support for resource managers includes
  • +
      +
    • SLURM, SGE/UGE, PBS/TORQUE/PBS Pro, LSF
    • +
    +
  • Optional modules supported
  • + +
+
+ + + + + + + + + + + + + + + +
+
Fig.1 Open Source XDMoD Summary Tab

+
Fig.2 Open Source XDMoD Usage Tab

diff --git a/templates/twig/about/presentations.html.twig b/templates/twig/about/presentations.html.twig new file mode 100644 index 0000000000..7109122b39 --- /dev/null +++ b/templates/twig/about/presentations.html.twig @@ -0,0 +1,148 @@ + +

Presentations

+
+ +
PEARC '25
+
    +
  • Nikolay A. Simakov. "Enhancing an HPC Resources Modeling Framework with a Realistic, Slurm-Like, HPC Resource Model". Presentation available at doi:10.13140/RG.2.2.16351.98724.
  • +
+
Supercomputing 2024 (SC24), Atlanta, GA
+
    +
  • Nikolay A. Simakov. "Benchmarking and Continuous Performance Monitoring of HPC Resources using the XDMoD Application Kernel Module." SIGHPC Systems Professionals Workshop HPCSYSPROS24 at SC24. November 22, 2024. The presentation is available at doi:10.13140/RG.2.2.13362.62409.
  • +
+ +
2024-12-12 Internet2 Technical Exchange: Boston, MA
+
    +
  • Jennifer Schopf, "Understanding Globus Data Transfers with NetSage"
  • +
+ +
ACCESS Resource Provider Workshop September 2024
+
    +
  • Aaron Weeden, "What We Do in ACCESS Metrics"
  • +
+ +
PEARC24: Providence, RI
+
    +
  • Nikolay A. Simakov, "Modeling Users on High-Performance Computing Resource"
  • +
  • Tom Furlani, "ACCESS Metrics Overview and Career Guidance"
  • +
+ +
Research Computing at Smaller Institutions Conference, Swarthmore College, June 2024
+
    +
  • Joseph White, "Making the Case: Monitoring and Metrics"
  • +
+ +
ACCESS Resource Provider Forum May 2024
+
    +
  • Aaron Weeden, "Plans for reporting on NAIRR Pilot usage"
  • +
+ +
HPC Asia 2024: Nagoya, Japan
+
    +
  • N.A. Simakov, "First Impressions of the NVIDIA Grace CPU Superchip and NVIDIA Grace Hopper Superchip and Scientific Workloads"
  • +
+ +
2023-10-26 ACCESS RP Forum (virtual)
+
    +
  • How to leverage ACCESS XDMoD to facilitate Campus Champion support for campus researchers
  • +
+ +
2023-09-19 Campus Champions All Champions Call (virtual)
+
    +
  • How to leverage ACCESS XDMoD to facilitate Resource Provider Operations
  • +
+ +
Metrics2023: Denver, CO
+
    +
  • Dr. Abani Patra, "Measuring Performance and Usage - Evolution of the Measuring and Monitoring of NSF Supercomputing"
  • +
  • N.A. Simakov, "Feasibility of Application-Agnostic Performance per Currency Metric on an Example of Gromacs, a Molecular Dynamics Application"
  • +
  • Aaron Weeden, "The Data Analytics Framework for XDMoD"
  • +
+ +
PEARC23: Portland, OR
+
    +
  • Open OnDemand, XDMoD, and ColdFront: an HPC center management toolset (tutorial)
  • +
  • Introduction to CI usage and performance data analysis with XDMoD and the new Analytics Framework. (tutorial)
  • +
  • N.A Simakov, "The Taming of the Wolf - how to use the Ookami Cray Apollo 80 system and Fujitsu A64FX processors" (workshop)
  • +
  • Dr. Jennifer M. Schopf, Doug Southworth, "EPOC Support for Cyberinfrastructure and Data Movement" (Panel discussion)
  • +
+ +
Cray User Group meeting (CUG) 2023 in Helsinki, Finland, May 7 – 11, 2023
+
    +
  • N.A. Simakov, "Benchmarking High-End ARM Systems with Scientific Applications. Performance and Energy Efficiency"
  • +
+ +
ISC High Performance 2023 (ISC23): Hamburg, Germany
+ + +
ARM HPC User Group (AHUG) Symposium at SC 2022
+
    +
  • N.A. Simakov, “Are we ready for broader adoption of ARM in the HPC community: Benchmarks and Applications on High-End ARM Systems with XDMoD Application Kernels”
  • +
+ +
PEARC22: Boston, MA
+ + +
PEARC21: (virtual)
+ + +
Supercomputing 2020 (SC'20): Atlanta, GA (virtual), November 18, 2020
+ + +
Gateways20: Bethesda, MD (virtual), October 13, 2020
+ + +
NYSERNet 2020: (virtual), October 2, 2020
+ + +
PEARC20: Portland, OR (virtual)
+ + +
PEARC19: Chicago, IL
+ + +
2018-09-05 Research Computing Campus Champions Presentation
+ + +
SC17: Denver, CO
+ + +
SC16: Salt Lake City, UT
+ + +
XSEDE16: Miami, FL
+ + +
XSEDE15: Saint Louis, MO
+ diff --git a/html/about/publications.html b/templates/twig/about/publications.html.twig similarity index 100% rename from html/about/publications.html rename to templates/twig/about/publications.html.twig diff --git a/templates/twig/about/roadmap.html.twig b/templates/twig/about/roadmap.html.twig new file mode 100644 index 0000000000..8b1625342e --- /dev/null +++ b/templates/twig/about/roadmap.html.twig @@ -0,0 +1,17 @@ +{% if header is not empty %} +

{{ header }}

+{% endif %} + +{% if url is not empty %} + +{% else %} +
+
+

Roadmap Not Configured

+

+ Please contact your Systems Administrator if you believe this is + in error. +

+
+
+{% endif %} diff --git a/templates/twig/about/supremm.html.twig b/templates/twig/about/supremm.html.twig new file mode 100644 index 0000000000..b514a17112 --- /dev/null +++ b/templates/twig/about/supremm.html.twig @@ -0,0 +1,24 @@ + +

SUPReMM Program

+
+

The SUPReMM program is designed integrate job level performance data into the XDMoD framework so it is available for detailed analysis. Initially an independently funded NSF program, SUPReMM was subsequently merged into the TAS program. The goal of the SUPReMM program is to develop the TACC_Stats and Lariat data sources and pipe this data into the XDMoD data warehouse.

+

Lariat captures application information at the time that jobs are launched. TACC_Stats uses collectors sampled at the beginning and end of every job and at 10 minute intervals to provide a wide variety of job performance information including memory, I/O file data, CPU data and network data. Accordingly, with this data system personnel will have at their fingertips detailed performance data for every job that runs on the HPC resource. Starting with XDMoD 4.0, this job performance information has been available in the XDMoD SUPReMM data realm.

+
+ + + + + + + + + + + + + + + +
Fig 1. SUPReMM data workflow diagram
+
Fig 2. Serial Data Copy causing a large dropoff of performance.
+ diff --git a/templates/twig/about/team.html.twig b/templates/twig/about/team.html.twig new file mode 100644 index 0000000000..39a157553d --- /dev/null +++ b/templates/twig/about/team.html.twig @@ -0,0 +1,27 @@ +

XMS Team

+
+

University at Buffalo

+

Dr. Matthew D. Jones: coPI, XMS Technical Project Manager

+

Dr. Robert L. DeLeon: XMS Project Manager

+

Dr. Joseph P. White: Job level performance data integration & analytics and XDMoD data warehouse

+

Mr. Jeffrey T. Palmer: Open XDMoD development, XDMoD portal development and XDMoD data warehouse development

+

Dr. Nikolay Simakov: Application kernel development and performance data modeling

+

Mr. Gregary Dean: XDMoD portal development

+

Mr. Ryan Rathsam: XDMoD portal development and XDMoD data warehouse development

+

Ms. Hannah Taylor: XDMoD portal development

+

Mr. Conner Saeli: Job level performance data integration

+
+

Roswell Park Cancer Institute

+

Dr. Thomas R. Furlani: PI, Oversees XMS Program

+

Mr. Steven M. Gallo: coPI, Oversees development and implementation of the XDMoD portal infrastructure

+
+

Tufts University

+

Dr. Abani Patra: coPI

+
+

Indiana University

+

Dr. Gregor von Laszewski: coPI, Scientific Impact Analysis

+

Mr. Fugang Wang: Scientific Impact Analysis

+
+

Texas Advanced Computing Center

+

Dr. Todd Evans: Job level performance data integration

+

Dr. Bill Barth: Job level performance data integration

diff --git a/templates/twig/about/xdmod.html.twig b/templates/twig/about/xdmod.html.twig new file mode 100644 index 0000000000..2748dfa787 --- /dev/null +++ b/templates/twig/about/xdmod.html.twig @@ -0,0 +1,47 @@ +
+ +
+

XDMoD: Comprehensive HPC System Management Tool

+
+ +

The University at Buffalo Center for Computational Research (CCR) has been at the forefront of the development of + open source tools for use by national and campus level high performance computing (HPC) centers to help ensure their + optimal operation as well as provide metrics to demonstrate the utility, service, competitive advantage, and return + on investment that these centers provide.

+

The XDMoD (XD Metrics on Demand) tool provides HPC center personnel and senior leadership with the ability to easily + obtain detailed operational metrics of HPC systems coupled with extensive analytical capability to optimize + performance at the system and job level, ensure quality of service, and provide accurate data to guide system + upgrades and acquisitions.

+
+ + + + + + + +
XDMoD Summary Tab

+
+

+ Funded by the National Science Foundation, XDMoD (https://xdmod.ccr.buffalo.edu/) + is designed to audit and facilitate the operation and utilization of XSEDE, the most advanced and robust collection + of + integrated advanced digital resources and services in the world. Similarly, Open XDMoD (http://open.xdmod.org), the open + source version of XDMoD, is designed to provide similar capability to academic and industrial HPC centers.

+ +

+ + When referencing XDMoD, please cite the following publication:

+ Jeffrey T. Palmer, Steven M. Gallo, Thomas R. Furlani, Matthew D. Jones, Robert L. DeLeon, Joseph P. White, + Nikolay Simakov, Abani K. Patra, Jeanette Sperhac, Thomas Yearke, Ryan Rathsam, Martins Innus, Cynthia D. + Cornelius, James C. Browne, William L. Barth, Richard T. Evans, + "Open XDMoD: A Tool for the Comprehensive Management of High-Performance Computing Resources", + Computing in Science & Engineering, Vol 17, Issue 4, 2015, pp. 52-62. + 10.1109/MCSE.2015.68 +
+

+ +

XDMoD Version: {{ xdmod_version }}

diff --git a/html/about/release_notes/xdmod.html b/templates/twig/about/xdmod_release_notes.html.twig similarity index 100% rename from html/about/release_notes/xdmod.html rename to templates/twig/about/xdmod_release_notes.html.twig diff --git a/templates/twig/base.html.twig b/templates/twig/base.html.twig new file mode 100644 index 0000000000..1b7fe10fe8 --- /dev/null +++ b/templates/twig/base.html.twig @@ -0,0 +1,18 @@ + + + + + {% block title %}Welcome!{% endblock %} + + {% block stylesheets %} + {{ encore_entry_link_tags('app') }} + {% endblock %} + + {% block javascripts %} + {{ encore_entry_script_tags('app') }} + {% endblock %} + + + {% block body %}{% endblock %} + + diff --git a/templates/twig/emails/new_user.html.twig b/templates/twig/emails/new_user.html.twig new file mode 100644 index 0000000000..445201b894 --- /dev/null +++ b/templates/twig/emails/new_user.html.twig @@ -0,0 +1,12 @@ +Welcome to the {{ page_title }}. Your account has been created. + +Your username is: {{ username }} + +Please visit the following page to create your password: + +{{ site_address }}controllers/password_reset.php?mode=new&rid={{ rid }} + +For assistance on using the portal, please consult the User Manual: +{{ site_address }}user_manual + +The XDMoD Team diff --git a/templates/twig/emails/password_reset.html.twig b/templates/twig/emails/password_reset.html.twig new file mode 100644 index 0000000000..10071fc63b --- /dev/null +++ b/templates/twig/emails/password_reset.html.twig @@ -0,0 +1,15 @@ +Dear {{ first_name }}, + +Your username is: {{ username }} + +To reset your password, please navigate to the following link: + +{{ reset_link }} + +This link will expire at: {{ expiration }}. + +(Please note that once you update your password, the above link will no +longer be valid) + +Sincerely, +{{ maintainer_signature }} diff --git a/templates/twig/index.html.twig b/templates/twig/index.html.twig new file mode 100644 index 0000000000..bd208ccc99 --- /dev/null +++ b/templates/twig/index.html.twig @@ -0,0 +1,394 @@ + + + + + {{ title }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if user_logged_in %} + + {% endif %} + + + + {% if user_logged_in %} + + {% endif %} + + + + + + + + + + + + + + + + + + + + + + + {% if user_logged_in %} + + {% endif %} + + {% if user_logged_in %} + + {% endif %} + + {% if user_logged_in %} + + + + + + + + + + + {% endif %} + + + + + + + + + + + + + + {% if user_logged_in %} + + {% endif %} + + + + + + + + + + + {% if not user_logged_in %} + + {% endif %} + + + + {# Profile Editor #} + {% if user_logged_in %} + + + + + + {% endif %} + + + + + + + + + + {% if user_logged_in %} + + + + + + + + + + + + + + + + + {% endif %} + + + + + + + + + {% if user_logged_in %} + + {% endif %} + + + + + + + + {% if user_logged_in %} + + + + + + {% endif %} + + + + + + + + + + {% if user_logged_in %} + + {% endif %} + + + + + + + + + + + + + + + {% if user_logged_in %} + + + {% endif %} + + + + + {% if not user_logged_in %} + + {% endif %} + + + + {% if user_logged_in %} + + {% endif %} + + + {% if user_logged_in and user_dashboard and user.getPersonID() != PERSON_ID_UNASSOCIATED %} + + {% else %} + + {% endif %} + + + + {% if user_logged_in %} + + + {% endif %} + + + {% if user_logged_in %} + + + + {% if raw_data_realms|length > 0 %} + + + + + + + + + + + + + + + + {% endif %} + {% endif %} + + {% if use_center_logo %} + + {% endif %} + + + {% if user_logged_in and not is_public_user %} + {{ asset_paths | raw }} + {% endif %} + + + + + {% if user_logged_in %} + + {% endif %} + {% if captcha_site_key|length > 0 %} + + {% endif %} + + + + + +
+ + +
+ + +
+ + + + diff --git a/templates/twig/internal_dashboard.html.twig b/templates/twig/internal_dashboard.html.twig new file mode 100644 index 0000000000..2434d8eae1 --- /dev/null +++ b/templates/twig/internal_dashboard.html.twig @@ -0,0 +1,206 @@ + + + + + XDMoD Internal Dashboard + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {# The script block below is from gui/js/Error.js.php #} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if user is not null and not is_public_user %} + {{ asset_paths | raw }} + {% endif %} + + + + + {% if has_app_kernels %} + + {% endif %} + + + diff --git a/templates/twig/internal_dashboard_login.html.twig b/templates/twig/internal_dashboard_login.html.twig new file mode 100644 index 0000000000..42c194cedc --- /dev/null +++ b/templates/twig/internal_dashboard_login.html.twig @@ -0,0 +1,130 @@ + + + + XDMoD Internal Dashboard + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ XDMoD Internal Dashboard

+ + + + + + + + + + + + + + + + + + + + + + +
Please Sign In Below
Username: + +
Password: + +
+ +
+
+ + + + + diff --git a/templates/twig/password_reset.html.twig b/templates/twig/password_reset.html.twig new file mode 100644 index 0000000000..d0bff82596 --- /dev/null +++ b/templates/twig/password_reset.html.twig @@ -0,0 +1,108 @@ + + + + + + {{ title }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Password ImageGo To XDMoD +
+ +
+ +

+ Welcome, {{ first_name }}. To {{ mode }} your password, supply a new password below and click + on Update.

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ mode | capitalize }} Your + Password
Password: + + 5 characters min.
  + + + + + +
+
password not specified
+
+
Password Again: + + 5 characters min. +
+ +
+
+ +
+ +
+ + + diff --git a/templates/twig/password_reset_expired.html.twig b/templates/twig/password_reset_expired.html.twig new file mode 100644 index 0000000000..6999812c7d --- /dev/null +++ b/templates/twig/password_reset_expired.html.twig @@ -0,0 +1,35 @@ + + + + + {{ title }} + + + + + + + +
Go To XDMoD +
+ +
+ +

+ + The page you are trying to access has already expired.

+ If you still need to reset your password, visit the login page and + click on Problem Logging In? below the login prompt. +
+ +
+ + + diff --git a/tests/artifacts/xdmod/controllers/input/enum_target_addresses-update_enum_user_types_and_roles.json b/tests/artifacts/xdmod/controllers/input/enum_target_addresses-update_enum_user_types_and_roles.json index 7a0a4f4cc0..a371d8f6ee 100644 --- a/tests/artifacts/xdmod/controllers/input/enum_target_addresses-update_enum_user_types_and_roles.json +++ b/tests/artifacts/xdmod/controllers/input/enum_target_addresses-update_enum_user_types_and_roles.json @@ -404,7 +404,7 @@ "expected": { "file": "enum_target_addresses__-update_enum_user_types_and_roles", "http_code": 200, - "content_type": "text/html; charset=UTF-8" + "content_type": "application/json" } } ], @@ -429,7 +429,7 @@ "expected": { "file": "enum_target_addresses_rand_char(120)_rand_char(120)-update_enum_user_types_and_roles", "http_code": 200, - "content_type": "text/html; charset=UTF-8" + "content_type": "application/json" } } ], diff --git a/tests/artifacts/xdmod/controllers/output/enum_target_addresses__-update_enum_user_types_and_roles.json b/tests/artifacts/xdmod/controllers/output/enum_target_addresses__-update_enum_user_types_and_roles.json index 9be6195f7d..83861e0213 100644 --- a/tests/artifacts/xdmod/controllers/output/enum_target_addresses__-update_enum_user_types_and_roles.json +++ b/tests/artifacts/xdmod/controllers/output/enum_target_addresses__-update_enum_user_types_and_roles.json @@ -1,5 +1,5 @@ { - "success": true, - "count": 0, - "response": [] + "success": true, + "count": 0, + "response": [] } diff --git a/tests/ci/bootstrap.sh b/tests/ci/bootstrap.sh index e6b7273dcf..64bc55ee96 100755 --- a/tests/ci/bootstrap.sh +++ b/tests/ci/bootstrap.sh @@ -185,3 +185,6 @@ then # Restart so that the above changes take effect. ~/bin/services restart fi + +# Clearing the Symfony cache so that we start fresh. +console cache:clear diff --git a/tests/ci/samlSetup.sh b/tests/ci/samlSetup.sh index 08e6bcc7fc..26bd629ee1 100755 --- a/tests/ci/samlSetup.sh +++ b/tests/ci/samlSetup.sh @@ -9,229 +9,347 @@ DEFAULT_VENDOR_DIR=$DEFAULT_INSTALL_DIR/vendor INSTALL_DIR=${INSTALL_DIR:-$DEFAULT_INSTALL_DIR} VENDOR_DIR=${VENDOR_DIR:-$DEFAULT_VENDOR_DIR} -httpd -k stop -cd /tmp - -echo "installing saml idp server" -if [ -f $CACHE_FILE ]; -then - echo "using cached copy" - tar -zxf $CACHE_FILE - cd saml-idp -else - git clone https://github.com/mcguinness/saml-idp/ - cd saml-idp - git checkout 8ff807a91f4badc3c0a10551e1d789df140a66cc - rm -f package-lock.json - npm set progress=false - npm install --quiet --silent -fi +HOSTNAME="" +PORT="" +# valid values: local, keycloak +DEFAULT_TYPE=local +while getopts h:p:t: flag +do + case "${flag}" in + h) HOSTNAME=${OPTARG};; + p) PORT=${PORT:-${OPTARG}};; + t) TYPE=${DEFAULT_TYPE:-${OPTARG}};; + *) echo "Invalid argument"; exit 1; + esac +done -openssl req -x509 -new -newkey rsa:2048 -nodes -subj '/C=US/ST=New York/L=Buffalo/O=UB/CN=CCR Test Identity Provider' -keyout idp-private-key.pem -out idp-public-cert.pem -days 7300 - -cat > /tmp/saml-idp/config.js <> /var/log/xdmod/saml_setup.log + + # output the pretty colored messages for when we're watching the build / setup process. + prefix=$(color $header 'green') + printf "[$prefix] $message\n" } + +function configurePortalSettings() +{ + host=$1; + log "xdmod" "Configuring /etc/xdmod/portal_settings.ini" + + grep -ie "auth_referer=https://xdmod:7000" /etc/xdmod/portal_settings.ini + exit_code=$? + if [[ $exit_code -eq 1 ]]; then + log "xdmod" "Updating auth_referer in portal_settings.ini" + # Add the auth_referer property to portal_settings.ini + sed -i "s|auth_referer=|auth_referer=$host|g" /etc/xdmod/portal_settings.ini + + # Make sure that the cache is reset so that `auth_referer` shows up in Symfony at runtime. + log "xdmod" "Clearing Symfony cache" + console cache:clear + else + log "xdmod" "portal_settings already has an auth_referer, skipping" + fi + + log "xdmod" "portal_settings.ini configured!" +} + +function configureSimplesamlPHP() +{ + log "SimpleSamlPHP" "Stopping HTTPD" + httpd -k stop + + log "SimpleSamlPHP" "Bootstrapping SimplesAMLPHP cache directory" + # Directory Setup, SimpleSamlPHP want's it's own cache directory so we create that here. + mkdir -p /var/cache/simplesamlphp/core + chown -R apache:root /var/cache/simplesamlphp + + log "SimpleSamlPHP" "Uncommenting SimpleSamlPHP in HTTPD config." + # Uncomment the simplesamlphp endpoint + sed -i -- "s%# SetEnv SIMPLESAMLPHP_CONFIG_DIR $DEFAULT_VENDOR_DIR/simplesamlphp/simplesamlphp/config% SetEnv SIMPLESAMLPHP_CONFIG_DIR $DEFAULT_VENDOR_DIR/simplesamlphp/simplesamlphp/config%" /etc/httpd/conf.d/xdmod.conf + sed -i -- "s%# Alias /simplesaml $DEFAULT_VENDOR_DIR/simplesamlphp/simplesamlphp/public% Alias /simplesaml $DEFAULT_VENDOR_DIR/simplesamlphp/simplesamlphp/public%" /etc/httpd/conf.d/xdmod.conf + sed -i -- "s%# % %" /etc/httpd/conf.d/xdmod.conf + sed -i -- "s%# Options FollowSymLinks% Options FollowSymLinks%" /etc/httpd/conf.d/xdmod.conf + sed -i -- "s%# AllowOverride All% AllowOverride All%" /etc/httpd/conf.d/xdmod.conf + sed -i -- "s%# % %" /etc/httpd/conf.d/xdmod.conf + sed -i -- "s%# Require all granted% Require all granted%" /etc/httpd/conf.d/xdmod.conf + sed -i -- "s%# % %" /etc/httpd/conf.d/xdmod.conf + sed -i -- "s%# % %" /etc/httpd/conf.d/xdmod.conf + + # Copy in the default SimplesSamlPHP config file. + log "SimpleSamlPHP" "Copying SimpleSamlPHP config file into place" + cp "$VENDOR_DIR/simplesamlphp/simplesamlphp/config/config.php.dist" "$VENDOR_DIR/simplesamlphp/simplesamlphp/config/config.php" + + log "SimpleSamlPHP" "Configuring trusted url domains and a default admin password for testing." + # Ensure that we add `localhost` and 'xdmod' to the trusted url domains + sed -i -- "s|'trusted.url.domains' => \[\]|'trusted.url.domains' => \['localhost', 'xdmod'\]|g" "$VENDOR_DIR/simplesamlphp/simplesamlphp/config/config.php" + + # Change the default password so that we can test authentication + sed -i -- "s%'auth.adminpassword' => '123'%'auth.adminpassword' => 'zaq12wsx'%" "$VENDOR_DIR/simplesamlphp/simplesamlphp/config/config.php" + + log "SimpleSamlPHP" "Copying authsources config into place." + # Create the authsources config file for saml Authentication + cat > "$VENDOR_DIR/simplesamlphp/simplesamlphp/config/authsources.php" < array( + 'saml:SP', + 'entityID' => 'https://$HOSTNAME/xdmod-sp', + 'idp' => 'urn:example:idp', + //'signature.algorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', + 'authproc' => array( + 40 => array( + 'class' => 'core:AttributeMap', + 'email' => 'email_address', + 'firstName' => 'first_name', + 'middleName' => 'middle_name', + 'lastName' => 'last_name', + 'personId' => 'person_id', + 'orgId' => 'organization', + 'fieldOfScience' => 'field_of_science', + 'itname' => 'username' + ) + ) + ), + 'admin' => array( + // The default is to use core:AdminPassword, but it can be replaced with + // any authentication source. + 'core:AdminPassword', + ), + ); EOF -sed -i -- "s%#Alias /simplesaml $DEFAULT_VENDOR_DIR/simplesamlphp/simplesamlphp/www%Alias /simplesaml $VENDOR_DIR/simplesamlphp/simplesamlphp/www%" /etc/httpd/conf.d/xdmod.conf -sed -i -- "s%#%%" /etc/httpd/conf.d/xdmod.conf -sed -i -- 's/# Options FollowSymLinks/ Options FollowSymLinks/' /etc/httpd/conf.d/xdmod.conf -sed -i -- 's/# AllowOverride All/ AllowOverride All/' /etc/httpd/conf.d/xdmod.conf -sed -i -- 's/# / /' /etc/httpd/conf.d/xdmod.conf -sed -i -- 's/# Require all granted/ Require all granted/' /etc/httpd/conf.d/xdmod.conf -sed -i -- 's%# % %' /etc/httpd/conf.d/xdmod.conf -sed -i -- 's%#%%' /etc/httpd/conf.d/xdmod.conf - - -cp "$VENDOR_DIR/simplesamlphp/simplesamlphp/config-templates/config.php" "$VENDOR_DIR/simplesamlphp/simplesamlphp/config/config.php" -sed -i -- "s/'trusted.url.domains' => array(),/'trusted.url.domains' => array('localhost'),/" "$VENDOR_DIR/simplesamlphp/simplesamlphp/config/config.php" - -cat > "$VENDOR_DIR/simplesamlphp/simplesamlphp/config/authsources.php" < array( - 'saml:SP', - 'idp' => 'urn:example:idp', - //'signature.algorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', - 'authproc' => array( - 40 => array( - 'class' => 'core:AttributeMap', - 'email' => 'email_address', - 'firstName' => 'first_name', - 'middleName' => 'middle_name', - 'lastName' => 'last_name', - 'personId' => 'person_id', - 'orgId' => 'organization', - 'fieldOfScience' => 'field_of_science', - 'itname' => 'username' + log "SimpleSamlPHP" "Retrieving the new x509 Cert to be used in SimpleSamlPHP" + NEW_CERT=`sed -n '2,21p' idp-public-cert.pem | perl -ne 'chomp and print'` + + log "SimpleSamlPHP" "Copying config file for SimpleSamlPHP remote IDP" + cat > "$VENDOR_DIR/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php" < 'urn:example:idp', + 'contacts' => + array ( ), - 60 => array( - 'class' => 'authorize:Authorize', - 'username' => array( - '/\S+/' + 'metadata-set' => 'saml20-idp-remote', + 'SingleSignOnService' => + array ( + 0 => + array ( + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + 'Location' => 'https://$HOSTNAME:7000', + ), + 1 => + array ( + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + 'Location' => 'https://$HOSTNAME:7000', ), ), - 61 => array( - 'class' => 'authorize:Authorize', - 'organization' => array( - '/\S+/' - ) - ) - ) - ), - 'admin' => array( - // The default is to use core:AdminPassword, but it can be replaced with - // any authentication source. - 'core:AdminPassword', - ), -); + 'SingleLogoutService' => + array ( + 0 => + array ( + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', + 'Location' => 'https://$HOSTNAME:7000/signout', + ), + ), + 'ArtifactResolutionService' => + array ( + ), + 'NameIDFormats' => + array ( + 0 => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', + 1 => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', + 2 => 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', + ), + 'keys' => + array ( + 0 => + array ( + 'encryption' => false, + 'signing' => true, + 'type' => 'X509Certificate', + 'X509Certificate' => '$NEW_CERT', + ), + ), + ); EOF + sleep 1 + log "SimpleSamlPHP" "Starting HTTPD" + httpd -k start +} + +localSSO() { + # For using the SSO locally we need the HOSTNAME to be localhost + #HOSTNAME=localhost:8181 + # For using the SSO via playwright then we need `xdmod` + #HOSTNAME=$(hostname) + + cd /tmp || exit + + log "setup" "installing saml idp server" + if [[ -f $CACHE_FILE ]]; + then + log "setup" "using cached copy" + tar -zxf $CACHE_FILE + cd saml-idp || exit + else + git clone https://github.com/mcguinness/saml-idp/ + cd saml-idp || exit + git checkout 8ff807a91f4badc3c0a10551e1d789df140a66cc + rm -f package-lock.json + npm set progress=false + npm install --quiet --silent + fi -CERTCONTENTS=`sed -n '2,21p' idp-public-cert.pem | perl -ne 'chomp and print'` -HOSTNAME=$(hostname) - -cat > "$VENDOR_DIR/simplesamlphp/simplesamlphp/metadata/saml20-idp-remote.php" < 'urn:example:idp', - 'contacts' => - array ( - ), - 'metadata-set' => 'saml20-idp-remote', - 'SingleSignOnService' => - array ( - 0 => - array ( - 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', - 'Location' => 'https://$HOSTNAME:7000', - ), - 1 => - array ( - 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', - 'Location' => 'https://$HOSTNAME:7000', - ), - ), - 'SingleLogoutService' => - array ( - 0 => - array ( - 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', - 'Location' => 'https://$HOSTNAME:7000/signout', - ), - ), - 'ArtifactResolutionService' => - array ( - ), - 'NameIDFormats' => - array ( - 0 => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', - 1 => 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', - 2 => 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', - ), - 'keys' => - array ( - 0 => - array ( - 'encryption' => false, - 'signing' => true, - 'type' => 'X509Certificate', - 'X509Certificate' => '$CERTCONTENTS', - ), - ), -); + log "setup" "Generating new x509 cert" + openssl req -x509 -new -newkey rsa:2048 -nodes -subj '/C=US/ST=New York/L=Buffalo/O=UB/CN=CCR Test Identity Provider' -keyout idp-private-key.pem -out idp-public-cert.pem -days 7300 + + log "setup" "Adding IDP config file" + cat > /tmp/saml-idp/config.js < /var/log/xdmod/samlidp.log 2>&1 & + EXIT_CODE=$? + exit $EXIT_CODE +} + +keycloakSSO() { + echo "" +} + + +if [[ "$TYPE" == 'local' ]]; then + log "settings" "Type: $TYPE" + log "settings" "Host: $HOSTNAME" + log "settings" "Port: $PORT" + localSSO +elif [[ "$TYPE" == "keycloak" ]]; then + keycloakSSO +else + echo "You must provide a type of setup ( -t ) to continue "; +fi + + + -node app.js --acs https://$HOSTNAME/simplesaml/module.php/saml/sp/saml2-acs.php/xdmod-sp --aud https://$HOSTNAME/simplesaml/module.php/saml/sp/metadata.php/xdmod-sp --httpsPrivateKey idp-private-key.pem --httpsCert idp-public-cert.pem --https true > /var/log/xdmod/samlidp.log 2>&1 & -httpd -k start diff --git a/tests/component/lib/ETL/IngestorTest.php b/tests/component/lib/ETL/IngestorTest.php index b8ffe16046..c27f3ab22d 100644 --- a/tests/component/lib/ETL/IngestorTest.php +++ b/tests/component/lib/ETL/IngestorTest.php @@ -48,7 +48,7 @@ public function testLoadDataInfileWarnings() { if ( ! empty($result['stdout']) ) { foreach ( explode(PHP_EOL, trim($result['stdout'])) as $line ) { - $this->assertMatchesRegularExpression('/\[warning\]/', $line); + $this->assertMatchesRegularExpression('/[Ww][Aa][Rr][Nn][Ii][Nn][Gg]/', $line); $numWarnings++; } } @@ -77,7 +77,7 @@ public function testSqlWarnings() { $numWarnings = 0; if ( ! empty($result['stdout']) ) { foreach ( explode(PHP_EOL, trim($result['stdout'])) as $line ) { - if ( false !== strpos($line, '[warning]') ) { + if ( false !== strpos($line, '[WARNING]') ) { $numWarnings++; } } @@ -100,7 +100,7 @@ public function testHideSqlWarnings() { if ( ! empty($result['stdout']) ) { foreach ( explode(PHP_EOL, trim($result['stdout'])) as $line ) { - $this->assertNotRegExp('/\[warning\]/', $line); + $this->assertDoesNotMatchRegularExpression('/\[WARNING\]/', $line); } } @@ -126,7 +126,7 @@ public function testHideSqlWarningCodes() { if ( ! empty($result['stdout']) ) { foreach ( explode(PHP_EOL, trim($result['stdout'])) as $line ) { - if ( false !== strpos($line, '[warning]') ) { + if ( false !== strpos($line, '[WARNING]') ) { $numWarnings++; } } @@ -142,7 +142,7 @@ public function testHideSqlWarningCodes() { if ( ! empty($result['stdout']) ) { foreach ( explode(PHP_EOL, trim($result['stdout'])) as $line ) { - if ( false !== strpos($line, '[warning]') ) { + if ( false !== strpos($line, '[WARNING]') ) { $numWarnings++; } } @@ -191,7 +191,7 @@ public function testStructuredFileIngestorWithSameFile() { $recordsLoaded = array(); foreach ( explode(PHP_EOL, trim($result['stdout'])) as $line ) { - if ( false !== strpos($line, '[notice]') ) { + if ( false !== strpos($line, '[NOTICE]') ) { $matches = array(); if ( preg_match('/xdmod.structured-file.read-people-([0-9])/', $line, $matches) ) { $number = $matches[1]; @@ -288,7 +288,7 @@ private function executeCommand($command) /** * Clean up tables created during the tests * - * @return Nothing + * @return void */ public static function tearDownAfterClass(): void diff --git a/tests/component/lib/Export/FileManagerTest.php b/tests/component/lib/Export/FileManagerTest.php index 101a2325f5..86157809dc 100644 --- a/tests/component/lib/Export/FileManagerTest.php +++ b/tests/component/lib/Export/FileManagerTest.php @@ -150,8 +150,6 @@ public function testWriteDataSetToFile(array $request) ->will($this->onConsecutiveCalls(1, 2, false)); $dataSet->method('valid') ->will($this->onConsecutiveCalls(true, true, false)); - $dataSet->method('next')->willReturn(null); - $dataSet->method('rewind')->willReturn(null); $format = $request['export_file_format']; diff --git a/tests/component/lib/Export/RealmManagerTest.php b/tests/component/lib/Export/RealmManagerTest.php index 4f038ae338..aaef07d317 100644 --- a/tests/component/lib/Export/RealmManagerTest.php +++ b/tests/component/lib/Export/RealmManagerTest.php @@ -88,7 +88,7 @@ public function testGetRealms($realms) fn($realm) => ['name' => $realm->getName(), 'display' => $realm->getDisplay()], self::$realmManager->getRealms() ); - $this->assertEquals( + $this->assertEqualsCanonicalizing( $realms, $actual, sprintf('Expected: %s, Received: %s', json_encode($realms), json_encode($actual)) @@ -107,7 +107,7 @@ public function testGetRealmsForUser($role, $realms) fn($realm) => ['name' => $realm->getName(), 'display' => $realm->getDisplay()], self::$realmManager->getRealmsForUser(self::$users[$role]) ); - $this->assertEquals( + $this->assertEqualsCanonicalizing( $realms, $actual, sprintf('Expected: %s, Received: %s', json_encode($realms), json_encode($actual)) diff --git a/tests/component/lib/LoggerTest.php b/tests/component/lib/LoggerTest.php index 5e960832fb..2020e6ae47 100644 --- a/tests/component/lib/LoggerTest.php +++ b/tests/component/lib/LoggerTest.php @@ -10,10 +10,10 @@ class LoggerTest extends BaseTest public function provideFileOutput() { return array( - array('debug', 'message field', array('other' => 1.2), '/\[debug\] message field \(other: 1.2\)$/'), - array('info', 'single line string', array(), '/\[info\] single line string$/'), - array('warning', '', array('other' => 'comp123'), '/\[warning\] \(other: comp123\)$/'), - array('error', '', array('exceptiontest' => new \Exception('Test Line Exception')), '/\[error\] \(exceptiontest: .*' . str_replace('/', '\\/', __FILE__) . ':' . __LINE__ . '\)\W\[stacktrace\]/') + array('debug', 'message field', array('other' => 1.2), '/\[DEBUG\] message field \(other: 1.2\)$/'), + array('info', 'single line string', array(), '/\[INFO\] single line string$/'), + array('warning', '', array('other' => 'comp123'), '/\[WARNING\] \(other: comp123\)$/'), + array('error', '', array('exceptiontest' => new \Exception('Test Line Exception')), '/\[ERROR\] \(exceptiontest: .*' . str_replace('/', '\\/', __FILE__) . ':' . __LINE__ . '\)\W\[stacktrace\]/') ); } @@ -131,7 +131,7 @@ public function testCombinedOutput() $logger->debug('message portion', array('context' => 'portion')); $output = file_get_contents($conf['file']); - $this->assertStringEndsWith("[debug] message portion (context: portion)\n", $output); + $this->assertStringEndsWith("[DEBUG] message portion (context: portion)\n", $output); $logoutput = $db->query("SELECT priority, message FROM mod_logger.log_table WHERE ident = 'combined-test' AND id > :start_id ORDER BY id ASC", $initial_vals[0]); diff --git a/tests/component/lib/XDUserTest.php b/tests/component/lib/XDUserTest.php index ad2fc92398..e0a52448c2 100644 --- a/tests/component/lib/XDUserTest.php +++ b/tests/component/lib/XDUserTest.php @@ -207,7 +207,7 @@ public function testGetRolesCasual() { $user = XDUser::getUserByUserName(self::CENTER_DIRECTOR_USER_NAME); $roles = $user->getRoles('casual'); - $this->assertNull($roles); + $this->assertEmpty($roles); } public function testSetRolesEmpty() diff --git a/tests/html/.eslintrc.json b/tests/html/.eslintrc.json new file mode 120000 index 0000000000..f10469a2a4 --- /dev/null +++ b/tests/html/.eslintrc.json @@ -0,0 +1 @@ +../../html/.eslintrc.json \ No newline at end of file diff --git a/html/unit_tests/.eslintrc.json b/tests/html/unit_tests/.eslintrc.json similarity index 99% rename from html/unit_tests/.eslintrc.json rename to tests/html/unit_tests/.eslintrc.json index 2316c5085c..cfcb8e5418 100644 --- a/html/unit_tests/.eslintrc.json +++ b/tests/html/unit_tests/.eslintrc.json @@ -6,4 +6,3 @@ "mocha": true } } - diff --git a/html/unit_tests/coverage.html b/tests/html/unit_tests/coverage.html similarity index 98% rename from html/unit_tests/coverage.html rename to tests/html/unit_tests/coverage.html index 25135854ae..c68a9925f6 100644 --- a/html/unit_tests/coverage.html +++ b/tests/html/unit_tests/coverage.html @@ -27,7 +27,7 @@ - + @@ -43,7 +43,7 @@ - +