diff --git a/.circleci/config.yml b/.circleci/config.yml index efa6c98678..756700f19d 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 @@ -187,7 +209,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..7cc2298afd --- /dev/null +++ b/.env @@ -0,0 +1,9 @@ +# Default ENV file +DATABASE_URL= +###> google/recaptcha ### +# To use Google Recaptcha, you must register a site on Recaptcha's admin panel: +# https://www.google.com/recaptcha/admin +GOOGLE_RECAPTCHA_SITE_KEY= +GOOGLE_RECAPTCHA_SECRET= +###< google/recaptcha ### +XDMOD_LOG_DIR=/var/log/xdmod diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 5a65f30d08..5dc30714e4 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -25,7 +25,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/acl-config b/bin/acl-config index 644f832775..c7987f850d 100755 --- a/bin/acl-config +++ b/bin/acl-config @@ -1625,6 +1625,7 @@ SQL; $log->debug($query); $log->debug('', $params); + $log->debug('Params', $params); if ($dryRun) { $log->info($successMsg); 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 +_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 index a936141499..4d56db0d54 100644 --- a/classes/CCR/Logger.php +++ b/classes/CCR/Logger.php @@ -19,75 +19,4 @@ */ class Logger extends MLogger implements LoggerInterface { - /** - * @param $level - * @param $message - * @param array $context - * @return bool - * @throws \DateInvalidTimeZoneException - */ - public function addRecord($level, $message, array $context = array()) - { - if (!$this->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..170e6b8832 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; @@ -205,7 +205,7 @@ public function getDatasets($limit, $offset, $summarize) * @param integer $normalizeBy The total number of series to be summarized. * @return array the sql fragment, series name and summariation algorthm type. */ - protected function getSummaryOp($column_name, $normalizeBy) + protected function getSummaryOp(string $column_name, $normalizeBy) { $series_name = "All $normalizeBy Others"; $sql = "SUM(t.$column_name)"; 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..6b3ac30d4c 100644 --- a/classes/DataWarehouse/Visualization/AggregateChart.php +++ b/classes/DataWarehouse/Visualization/AggregateChart.php @@ -1017,6 +1017,9 @@ public function configure( $labelsAllocated = 0; $pieSum = array_sum($yValues); for ($i = 0; $i < count($xValues); $i++) { + if (is_null($yValues[$i])) { + $yValues[$i] = 0.0; + } if ($isThumbnail || ($labelsAllocated < $labelLimit && (($yValues[$i] / $pieSum) * 100) >= 2.0)) { $label = $xValues[$i]; // Truncate long data labels to improve visibility. diff --git a/classes/DataWarehouse/Visualization/TimeseriesChart.php b/classes/DataWarehouse/Visualization/TimeseriesChart.php index 67a7b982c0..f241524e2a 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 = number_format(0.0, $decimals, '.', ','); + } 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 bfba88f09a..db2113d718 100644 --- a/classes/ETL/Aggregator/pdoAggregator.php +++ b/classes/ETL/Aggregator/pdoAggregator.php @@ -603,12 +603,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) ) { @@ -667,16 +667,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"; @@ -883,7 +883,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..f86b2e9626 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..c64cd1ec62 --- /dev/null +++ b/classes/OpenXdmod/Migration/DotEnvConfigMigration.php @@ -0,0 +1,49 @@ +apply([ + 'app_secret' => hash('sha512', time()) + ]); + file_put_contents(BASE_DIR . '/.env', $envTemplate->getContents()); + + $cmdBase = 'APP_ENV=prod APP_DEBUG=0'; + $console = BIN_DIR .'/console'; + + // Make sure to clear the cache before dumping the dotenv so we start clean. + $this->executeCommand("$cmdBase $console 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. + $this->executeCommand("$cmdBase $console dotenv:dump"); + } + } + + protected function executeCommand($command) + { + $output = array(); + $returnVar = 0; + + exec($command . ' 2>&1', $output, $returnVar); + + if ($returnVar != 0) { + $msg = "Command exited with non-zero return status:\n" + . "command = $command\noutput =\n" . implode("\n", $output); + throw new \Exception($msg); + } + + return $output; + } + + +} diff --git a/classes/OpenXdmod/Migration/MigrationFactory.php b/classes/OpenXdmod/Migration/MigrationFactory.php index c51f21df0f..c9c4ad29bf 100644 --- a/classes/OpenXdmod/Migration/MigrationFactory.php +++ b/classes/OpenXdmod/Migration/MigrationFactory.php @@ -93,6 +93,7 @@ function ($class) use ($databasesMigrationName) { } $migrations[] = new AclConfigMigration($fromVersion, $toVersion); + $migrations[] = new DotEnvConfigMigration($fromVersion, $toVersion); $migration = new CompositeMigration( $fromVersion, diff --git a/classes/OpenXdmod/Setup/GeneralSetup.php b/classes/OpenXdmod/Setup/GeneralSetup.php index 19c6403441..5eaa99f88b 100644 --- a/classes/OpenXdmod/Setup/GeneralSetup.php +++ b/classes/OpenXdmod/Setup/GeneralSetup.php @@ -5,6 +5,8 @@ namespace OpenXdmod\Setup; +use Xdmod\Template; + /** * General setup. */ @@ -124,5 +126,21 @@ public function handle() ); $this->saveIniConfig($settings, 'portal_settings'); + + $envTemplate = new Template('env'); + $envTemplate->apply([ + 'app_secret' => hash('sha512', time()) + ]); + $this->saveTemplate($envTemplate, BASE_DIR . '/.env'); + + $cmdBase = 'APP_ENV=prod APP_DEBUG=0'; + $console = BIN_DIR .'/console'; + + // Make sure to clear the cache before dumping the dotenv so we start clean. + $this->executeCommand("$cmdBase $console 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. + $this->executeCommand("$cmdBase $console dotenv:dump"); } } diff --git a/classes/Realm/Realm.php b/classes/Realm/Realm.php index 40d9001887..aba378d976 100644 --- a/classes/Realm/Realm.php +++ b/classes/Realm/Realm.php @@ -366,7 +366,7 @@ private static function getSortedObjectList( // Skip disabled configs - if ( isset($config->disabled) && $config->disabled ) { + if (isset($config->disabled) && $config->disabled) { continue; } @@ -374,29 +374,29 @@ 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 ) { + $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/DashboardControllerProvider.php b/classes/Rest/Controllers/DashboardControllerProvider.php deleted file mode 100644 index ffef4bda29..0000000000 --- a/classes/Rest/Controllers/DashboardControllerProvider.php +++ /dev/null @@ -1,430 +0,0 @@ -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 - */ - private function getLayout($user) - { - $defaultLayout = null; - $defaultColumnCount = 2; - - if ($user->isPublicUser() === false) { - $layoutStore = new \UserStorage($user, 'summary_layout'); - $record = $layoutStore->getById(0); - if ($record) { - $defaultLayout = $record['layout']; - $defaultColumnCount = $record['columns']; - } - } - - return new \CCR\ColumnLayout($defaultColumnCount, $defaultLayout); - } - - private function getConfigVariables($user) - { - $person_id = $user->getPersonID(true); - $obj_warehouse = new \XDWarehouse(); - - return array( - 'PERSON_ID' => $person_id, - 'PERSON_NAME' => $obj_warehouse->resolveName($person_id) - ); - } - - /** - * The individual dashboard components have a namespace prefix to simplify - * the implementation of the algorithm that determines which - * components to display. There are two sources of configuration data for - * the components. The roles configuration file and the user configuration - * (in the database). The user configuration only contains chart components. - * The user configuration is handled via the "Show in Summary tab" checkbox - * in the metric explorer. - * - * Non-chart components and the full-width components are defined in the roles - * configuration file and are not overrideable. - * - * Chart components are handled as follows: - * - All user charts with "show in summary tab" checked will be displayed - * - 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.'; - - public function getComponents(Request $request, Application $app) - { - $user = $this->getUserFromRequest($request); - - $dashboardComponents = array(); - - $mostPrivilegedAcl = Acls::getMostPrivilegedAcl($user)->getName(); - - $layout = $this->getLayout($user); - - $roleConfig = \Configuration\XdmodConfiguration::assocArrayFactory( - 'roles.json', - CONFIG_DIR, - null, - array('config_variables' => $this->getConfigVariables($user)) - ); - - $presets = $roleConfig['roles'][$mostPrivilegedAcl]; - - if (isset($presets['dashboard_components'])) { - - foreach($presets['dashboard_components'] as $component) { - - $componentType = self::NON_CHART_COMPONENT; - - if (isset($component['region']) && $component['region'] === 'top') { - $componentType = self::TOP_COMPONENT; - $chartLocation = $componentType . $component['name']; - $column = -1; - } else { - if ($component['type'] === 'xdmod-dash-chart-cmp') { - $componentType = self::CHART_COMPONENT; - $component['config']['name'] = $component['name']; - $component['config']['chart']['featured'] = true; - } - - $defaultLayout = null; - if (isset($component['location']) && isset($component['location']['row']) && isset($component['location']['column'])) { - $defaultLayout = array($component['location']['row'], $component['location']['column']); - } - - list($chartLocation, $column) = $layout->getLocation($componentType . $component['name'], $defaultLayout); - } - - $dashboardComponents[$chartLocation] = array( - 'name' => $componentType . $component['name'], - 'type' => $component['type'], - 'config' => isset($component['config']) ? $component['config'] : array(), - 'column' => $column - ); - } - } - - if ($user->isPublicUser() === false) - { - $queryStore = new \UserStorage($user, 'queries_store'); - $queries = $queryStore->get(); - - if ($queries != null) { - foreach ($queries as $query) { - if (!isset($query['config']) || !isset($query['name'])) { - continue; - } - - $queryConfig = json_decode($query['config']); - - if (!isset($queryConfig->featured) || !$queryConfig->featured) { - continue; - } - - $name = self::CHART_COMPONENT . $query['name']; - - list($chartLocation, $column) = $layout->getLocation($name); - - $dashboardComponents[$chartLocation] = array( - 'name' => $name, - 'type' => 'xdmod-dash-chart-cmp', - 'config' => array( - 'name' => $query['name'], - 'chart' => $queryConfig - ), - 'column' => $column - ); - } - } - } - - ksort($dashboardComponents); - - return $app->json(array( - 'success' => true, - 'total' => count($dashboardComponents), - 'portalConfig' => array('columns' => $layout->getColumnCount()), - 'data' => array_values($dashboardComponents) - )); - } - - /** - * set the layout metadata - * - */ - public function setLayout(Request $request, Application $app) - { - $user = $this->authorize($request); - - $content = json_decode($this->getStringParam($request, 'data', true), true); - - if ($content === null || !isset($content['layout']) || !isset($content['columns'])) { - throw new BadRequestHttpException('Invalid data parameter'); - } - - $storage = new \UserStorage($user, 'summary_layout'); - - return $app->json(array( - 'success' => true, - 'total' => 1, - 'data' => $storage->upsert(0, $content) - )); - } - - /** - * clear the layout metadata - * - */ - public function resetLayout(Request $request, Application $app) - { - $user = $this->authorize($request); - - $storage = new \UserStorage($user, 'summary_layout'); - - $storage->del(); - - return $app->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) - { - $user = $this->authorize($request); - $viewedTour = $this->getIntParam($request, 'viewedTour', true); - - if (!in_array($viewedTour, [0,1])) { - throw new BadRequestHttpException('Invalid data parameter'); - } - - $storage = new \UserStorage($user, 'viewed_user_tour'); - - return $app->json(array( - 'success' => true, - 'total' => 1, - 'msg' => $storage->upsert(0, ['viewedTour' => $viewedTour]) - )); - } - - /** - * Get charts based on role. - **/ - public function getRoleReport(Request $request, Application $app) - { - $user = $this->authorize($request); - $role = $user->getMostPrivilegedRole()->getName(); - $report_id_suffix = 'autogenerated-' . $role; - $report_id = $user->getUserID() . '-' . $report_id_suffix; - if (isset($user)) { - $userReport = null; - $rm = new \XDReportManager($user); - $reports = $rm->fetchReportTable(); - foreach ($reports as &$report) { - if ($report['report_id'] === $report_id) { - $userReport = $report; - } - } - if (is_null($userReport)){ - $availTemplates = $rm->enumerateReportTemplates(array($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->buildReportFromTemplate($_REQUEST, $report_id_suffix); - $reports = $rm->fetchReportTable(); - foreach ($reports as &$report) { - if ($report['report_id'] === $report_id) { - $userReport = $report; - } - } - } - $data = $rm->loadReportData($userReport['report_id']); - $count = 0; - foreach($data['queue'] as $queue) { - $chart_id = explode("&", $queue['chart_id']); - $chart_id_parsed = array(); - foreach($chart_id as $value) { - list($key, $value) = explode("=", $value); - $key = urldecode($key); - $value = urldecode($value); - $json = json_decode($value, true); - - if ($key === 'timeseries') { - $value = $value === 'y' || $value === 'true'; - } elseif ($json !== null) { - $value = $json; - } - $chart_id_parsed[$key] = $value; - } - $data['queue'][$count]['chart_id'] = $chart_id_parsed; - $count++; - } - return $app->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) - { - $user = $this->authorize($request); - $storage = new \UserStorage($user, 'viewed_user_tour'); - return $app->json(array( - 'success' => true, - 'total' => 1, - 'data' => $storage->get() - )); - } - /** - * Get saved charts and reports. - **/ - public function getSavedChartsReports(Request $request, Application $app) - { - $user = $this->authorize($request); - if (isset($user)) { - // fetch charts - $queries = new \UserStorage($user, 'queries_store'); - $data = $queries->get(); - foreach ($data as &$query) { - $query['name'] = htmlspecialchars($query['name'], ENT_COMPAT, 'UTF-8', false); - $query['type'] = 'Chart'; - } - // fetch reports - $rm = new \XDReportManager($user); - $reports = $rm->fetchReportTable(); - foreach ($reports as &$report) { - $tmp = array(); - $tmp['type'] = 'Report'; - $tmp['name'] = $report['report_name']; - $tmp['chart_count'] = $report['chart_count']; - $tmp['charts_per_page'] = $report['charts_per_page']; - $tmp['creation_method'] = $report['creation_method']; - $tmp['report_delivery'] = $report['report_delivery']; - $tmp['report_format'] = $report['report_format']; - $tmp['report_id'] = $report['report_id']; - $tmp['report_name'] = $report['report_name']; - $tmp['report_schedule'] = $report['report_schedule']; - $tmp['report_title'] = $report['report_title']; - $tmp['ts'] = $report['last_modified']; - $tmp['config'] = $report['report_id']; - $data[] = $tmp; - } - return $app->json(array( - 'success' => true, - 'total' => count($data), - 'data' => $data - )); - } - } - - /* - * Retrieve summary statistics - * - * @param Request $request - * @param Application $app - * @return \Symfony\Component\HttpFoundation\JsonResponse - * @throws Exception - */ - public function getStatistics(Request $request, Application $app) - { - $user = $this->getUserFromRequest($request); - - $aggregationUnit = $request->get('aggregation_unit', 'auto'); - - $startDate = $this->getStringParam($request, 'start_date', true); - $endDate = $this->getStringParam($request, 'end_date', true); - - $this->checkDateRange($startDate, $endDate); - - // 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' - ); - - $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; - } - } catch (Exception $e) { - throw new BadRequestHttpException($e->getMessage()); - } - - $rawRoles = XdmodConfiguration::assocArrayFactory('roles.json', CONFIG_DIR); - - $mostPrivileged = $user->getMostPrivilegedRole()->getName(); - $formats = $rawRoles['roles'][$mostPrivileged]['statistics_formats']; - - return $app->json( - array( - 'totalCount' => 1, - 'success' => true, - 'message' => '', - 'formats' => $formats, - 'data' => array($result) - ) - ); - } -} 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..076863c9d9 100644 --- a/classes/XDReportManager.php +++ b/classes/XDReportManager.php @@ -1140,7 +1140,8 @@ private function ripTransform(&$arr, $item) public function fetchChartBlob( $type, $insertion_rank, - $chart_id_cache_file = null + $chart_id_cache_file = null, + $logger = null ) { $pdo = DB::factory('database'); $trace = ""; @@ -1153,7 +1154,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 +1207,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 +1233,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 +1243,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( " @@ -1437,10 +1433,13 @@ public function generateChartBlob( $type, $insertion_rank, $start_date, - $end_date + $end_date, + $logger = null ) { $pdo = DB::factory('database'); - + if (!is_null($logger)) { + $logger->debug("Generating Chart Blob - Type: $type"); + } switch ($type) { case 'volatile': $temp_file = $this->generateCachedFilename( @@ -1451,6 +1450,9 @@ public function generateChartBlob( $temp_file = str_replace('.png', '.xrc', $temp_file); $iq = array(); + if (!is_null($logger)) { + $logger->debug("Checking if Volatile File Exists; $temp_file"); + } if (file_exists($temp_file) == true) { $chart_id_config = file($temp_file); @@ -1465,7 +1467,6 @@ public function generateChartBlob( ); } break; - case 'chart_pool': $iq = $pdo->query( " @@ -1499,7 +1500,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..c32c441839 100644 --- a/classes/XDSessionManager.php +++ b/classes/XDSessionManager.php @@ -88,10 +88,10 @@ public static function recordLogin($user) ':last_active' => $init_time, )); - $_SESSION['xdInit'] = $init_time; - $_SESSION['xdUser'] = $user_id; - - $_SESSION['session_token'] = $session_token; + $session = \xd_security\SessionSingleton::getSession(); + $session->set('xdInit', $init_time); + $session->set('xdUser', $user_agent); + $session->set('session_token', $session_token); return $session_token; } @@ -107,12 +107,13 @@ public static function logoutUser($token = "") \xd_security\start_session(); } + $session = \xd_security\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 +130,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 +144,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..561212d4cc 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 // --------------------------- @@ -876,10 +888,18 @@ public function getInsertQuery($updateToken = false, $includePassword = false) */ public function arrayToString($array = array()) { + $values = array_reduce( + array_values($array), + function ($carry, $item) { + $carry[] = var_export($item, true); + return $carry; + }, + [] + ); $result = 'Keys [ '; $result .= implode(', ', array_keys($array)) . ']'; $result .= 'Values [ '; - $result .= implode(', ', array_values($array)) . ']'; + $result .= implode(', ', $values) . ']'; return $result; } @@ -955,15 +975,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); @@ -1202,15 +1225,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 +1527,7 @@ public function enumAllAvailableRoles() "A PDOException was thrown in 'XDUser::enumAllAvailableRoles'", array( 'exception' => $e, - 'sql'=> $query + 'sql' => $query ) ); @@ -1780,7 +1802,7 @@ public function getActiveOrganization() * */ - public function getRoles($flag = 'informal') + public function getRoles($flag = 'informal'): array { if ($flag == 'informal') { @@ -1816,7 +1838,7 @@ public function getRoles($flag = 'informal') return $roles; } - + return []; }//getRoles // --------------------------- @@ -1895,7 +1917,7 @@ function getAllRoles($includePublicRole = false) public function getUserID() { - return (empty($this->_id)) ? '0' : $this->_id; + return (empty($this->_id)) ? 0 : (int)$this->_id; } /* @@ -1913,13 +1935,14 @@ 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 = \xd_security\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)) { + return $assumedPersonId; } } @@ -1993,7 +2016,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 +2150,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 +2256,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 +2277,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 +2290,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 +2376,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 +2450,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 +2462,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 +2493,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 +2517,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 +2565,6 @@ public function synchronizeOrganization() ) ); } - // Update / save the user with their new organization $this->setOrganizationId($expectedOrganization); $this->saveUser(); @@ -2560,14 +2583,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 +2692,114 @@ 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)) { + return password_hash($password, PASSWORD_DEFAULT); + } else { + return $this->hasher->hash($password); + } + } + + /** + * 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..c4b83e3398 100644 --- a/composer.json +++ b/composer.json @@ -1,43 +1,57 @@ { - "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", + "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": "*", + "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 +215,10 @@ "external_libraries/{$name}": [ "zendframework/zendframework-minimal" ] + }, + "public-dir": "html", + "symfony": { + "docker": false } }, "config": { @@ -209,15 +227,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 +257,6 @@ "classes/XDChartPool.php", "classes/XDStatistics.php", "classes/SessionExpiredException.php", - "classes/XDController.php", "classes/XDUser.php", "classes/UniqueException.php", "classes/XDError.php", @@ -249,7 +273,7 @@ ], "psr-4": { "Authentication\\": "classes/Authentication/", - "CCR\\": "classes/CCR/", + "CCR\\": ["classes/CCR/", "src/"], "Common\\": "classes/Common/", "Configuration\\": "classes/Configuration/", "DataWarehouse\\": "classes/DataWarehouse/", @@ -260,9 +284,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..9efd30571f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "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": "a50a7ac9c87d0fb8292f93eaed72241c", "packages": [ { "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 +27,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 +64,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 +80,7 @@ "type": "tidelift" } ], - "time": "2024-03-15T14:00:32+00:00" + "time": "2025-05-26T15:08:54+00:00" }, { "name": "composer/installers", @@ -233,33 +233,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 +340,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 +356,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 +404,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 +415,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 +484,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" }, - "time": "2023-12-01T16:26:39+00:00" + "replace": { + "ocramius/proxy-manager": "^2.1" + }, + "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 +630,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 +672,7 @@ } ], "description": "PHP gettext manager", - "homepage": "https://github.com/oscarotero/Gettext", + "homepage": "https://github.com/php-gettext/Gettext", "keywords": [ "JS", "gettext", @@ -542,23 +683,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 +723,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 +763,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 +775,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 +901,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 +923,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,7 +961,123 @@ "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": "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", @@ -780,95 +1126,51 @@ "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" - }, + "name": "jquery/jquery-min-file", + "version": "3.7.1", "dist": { - "type": "zip", - "url": "https://api.github.com/repos/jaimeperez/twig-configurable-i18n/zipball/75d4926fd102c9e62219ad7f94a6136d2f2ccd93", - "reference": "75d4926fd102c9e62219ad7f94a6136d2f2ccd93", - "shasum": "" + "type": "file", + "url": "https://code.jquery.com/jquery-3.7.1.min.js", + "shasum": "ee48592d1fff952fcf06ce0b666ed4785493afdc" }, "require": { - "twig/extensions": "^1.3" + "composer/installers": "~1.0" }, - "type": "project", - "autoload": { - "psr-4": { - "JaimePerez\\TwigConfigurableI18n\\": "src/" - } + "type": "vanilla-plugin", + "extra": { + "installer-name": "jquery" }, - "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-2.1" - ], - "authors": [ - { - "name": "Jaime Perez", - "email": "jaime.perez@uninett.no" - } - ], - "description": "This is an extension on top of Twig's i18n extension, allowing you to customize which functions to use for translations.", - "keywords": [ - "extension", - "gettext", - "i18n", - "internationalization", - "translation", - "twig" - ], - "support": { - "issues": "https://github.com/jaimeperez/twig-configurable-i18n/issues", - "source": "https://github.com/jaimeperez/twig-configurable-i18n" - }, - "abandoned": "simplesamlphp/twig-configurable-i18n", - "time": "2016-10-03T12:34:15+00:00" - }, - { - "name": "jquery/jquery-min-file", - "version": "3.7.1", - "dist": { - "type": "file", - "url": "https://code.jquery.com/jquery-3.7.1.min.js", - "shasum": "ee48592d1fff952fcf06ce0b666ed4785493afdc" - }, - "require": { - "composer/installers": "~1.0" - }, - "type": "vanilla-plugin", - "extra": { - "installer-name": "jquery" - }, - "license": [ - "MIT" + "MIT" ], "homepage": "https://jquery.com" }, { "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 +1178,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0.x-dev" + "dev-master": "6.x-dev" } }, "autoload": { @@ -907,44 +1209,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 +1251,7 @@ { "name": "Rafael Kassner", "email": "kassner@gmail.com", - "homepage": "http://www.kassner.com.br/", + "homepage": "https://www.kassner.com.br/", "role": "Developer" } ], @@ -971,35 +1267,169 @@ ], "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": "2024-08-20T20:01:20+00:00" + }, + { + "name": "laminas/laminas-code", + "version": "4.16.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-code.git", + "reference": "1793e78dad4108b594084d05d1fb818b85b110af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-code/zipball/1793e78dad4108b594084d05d1fb818b85b110af", + "reference": "1793e78dad4108b594084d05d1fb818b85b110af", + "shasum": "" + }, + "require": { + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "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": { + "Laminas\\Code\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Extensions to the PHP Reflection API, static code scanning, and code generation", + "homepage": "https://laminas.dev", + "keywords": [ + "code", + "laminas", + "laminasframework" + ], + "support": { + "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" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2024-11-20T13:15:13+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" + ] }, - "time": "2019-02-04T07:43:30+00:00" + "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.11.1", + "version": "v1.12.1", "source": { "type": "git", "url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git", - "reference": "1e66f73ffcf25e17c7a910a1317e9720a95497c7" + "reference": "815939e006b7e68062b540ec9e86aaa8be2b6ce4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/1e66f73ffcf25e17c7a910a1317e9720a95497c7", - "reference": "1e66f73ffcf25e17c7a910a1317e9720a95497c7", + "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" + "ext-maxminddb": "<1.11.1 || >=2.0.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.*" }, @@ -1036,29 +1466,29 @@ ], "support": { "issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues", - "source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.11.1" + "source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.12.1" }, - "time": "2023-12-02T00:09:23+00:00" + "time": "2025-05-05T20:56:32+00:00" }, { "name": "maxmind/web-service-common", - "version": "v0.9.0", + "version": "v0.10.0", "source": { "type": "git", "url": "https://github.com/maxmind/web-service-common-php.git", - "reference": "4dc5a3e8df38aea4ca3b1096cee3a038094e9b53" + "reference": "d7c7c42fc31bff26e0ded73a6e187bcfb193f9c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maxmind/web-service-common-php/zipball/4dc5a3e8df38aea4ca3b1096cee3a038094e9b53", - "reference": "4dc5a3e8df38aea4ca3b1096cee3a038094e9b53", + "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": ">=7.2" + "php": ">=8.1" }, "require-dev": { "friendsofphp/php-cs-fixer": "3.*", @@ -1087,9 +1517,9 @@ "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" + "source": "https://github.com/maxmind/web-service-common-php/tree/v0.10.0" }, - "time": "2022-03-28T17:43:20+00:00" + "time": "2024-11-14T23:14:52+00:00" }, { "name": "moment/moment-min-file", @@ -1133,16 +1563,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 +1595,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-master": "1.18.x-dev" } }, "autoload": { @@ -1204,57 +1634,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 +1715,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 +1727,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 +1739,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,134 +1789,130 @@ ], "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": "Brent R. Matzelle" + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" } ], - "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "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/PHPMailer/PHPMailer/issues", - "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.1" + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" }, - "funding": [ - { - "url": "https://github.com/Synchro", - "type": "github" - } - ], - "time": "2023-11-25T22:23:28+00:00" + "time": "2020-06-27T09:03:43+00:00" }, { - "name": "phpoffice/math", - "version": "0.1.0", + "name": "phpdocumentor/reflection-docblock", + "version": "5.6.2", "source": { "type": "git", - "url": "https://github.com/PHPOffice/Math.git", - "reference": "f0f8cad98624459c540cdd61d2a174d834471773" + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/Math/zipball/f0f8cad98624459c540cdd61d2a174d834471773", - "reference": "f0f8cad98624459c540cdd61d2a174d834471773", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/92dde6a5919e34835c506ac8c523ef095a95ed62", + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-xml": "*", - "php": "^7.1|^8.0" + "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": { - "phpstan/phpstan": "^0.12.88 || ^1.0.0", - "phpunit/phpunit": "^7.0 || ^9.0" + "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": { - "PhpOffice\\Math\\": "src/Math/", - "Tests\\PhpOffice\\Math\\": "tests/Math/" + "phpDocumentor\\Reflection\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -1465,82 +1921,318 @@ ], "authors": [ { - "name": "Progi1984", - "homepage": "https://lefevre.dev" + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" } ], - "description": "Math - Manipulate Math Formula", - "homepage": "https://phpoffice.github.io/Math/", - "keywords": [ - "MathML", - "officemathml", - "php" - ], + "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/PHPOffice/Math/issues", - "source": "https://github.com/PHPOffice/Math/tree/0.1.0" + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.2" }, - "time": "2023-09-25T12:08:20+00:00" + "time": "2025-04-13T19:20:35+00:00" }, { - "name": "phpoffice/phpword", - "version": "1.2.0", + "name": "phpdocumentor/type-resolver", + "version": "1.10.0", "source": { "type": "git", - "url": "https://github.com/PHPOffice/PHPWord.git", - "reference": "e76b701ef538cb749641514fcbc31a68078550fa" + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/e76b701ef538cb749641514fcbc31a68078550fa", - "reference": "e76b701ef538cb749641514fcbc31a68078550fa", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-json": "*", - "ext-xml": "*", - "php": "^7.1|^8.0", - "phpoffice/math": "^0.1" + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" }, "require-dev": { - "dompdf/dompdf": "^2.0", - "ext-gd": "*", - "ext-libxml": "*", - "ext-zip": "*", - "friendsofphp/php-cs-fixer": "^3.3", - "mpdf/mpdf": "^8.1", - "phpmd/phpmd": "^2.13", - "phpstan/phpstan-phpunit": "@stable", - "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-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": { - "PhpOffice\\PhpWord\\": "src/PhpWord" + "phpDocumentor\\Reflection\\": "src" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0" + "MIT" ], "authors": [ { - "name": "Mark Baker" - }, - { - "name": "Gabriel Bull", - "email": "me@gabrielbull.com", - "homepage": "http://gabrielbull.com/" + "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" + } + ], + "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.10.0" + }, + "funding": [ + { + "url": "https://github.com/Synchro", + "type": "github" + } + ], + "time": "2025-04-24T15:19:31+00:00" + }, + { + "name": "phpoffice/math", + "version": "0.3.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/Math.git", + "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/Math/zipball/fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", + "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xml": "*", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.88 || ^1.0.0", + "phpunit/phpunit": "^7.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\Math\\": "src/Math/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Progi1984", + "homepage": "https://lefevre.dev" + } + ], + "description": "Math - Manipulate Math Formula", + "homepage": "https://phpoffice.github.io/Math/", + "keywords": [ + "MathML", + "officemathml", + "php" + ], + "support": { + "issues": "https://github.com/PHPOffice/Math/issues", + "source": "https://github.com/PHPOffice/Math/tree/0.3.0" + }, + "time": "2025-05-29T08:31:49+00:00" + }, + { + "name": "phpoffice/phpword", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PHPWord.git", + "reference": "6d75328229bc93790b37e93741adf70646cea958" + }, + "dist": { + "type": "zip", + "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.3" + }, + "require-dev": { + "dompdf/dompdf": "^2.0 || ^3.0", + "ext-libxml": "*", + "friendsofphp/php-cs-fixer": "^3.3", + "mpdf/mpdf": "^7.0 || ^8.0", + "phpmd/phpmd": "^2.13", + "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-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" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpWord\\": "src/PhpWord" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-only" + ], + "authors": [ + { + "name": "Mark Baker" + }, + { + "name": "Gabriel Bull", + "email": "me@gabrielbull.com", + "homepage": "http://gabrielbull.com/" }, { "name": "Franck Lefevre", @@ -1587,62 +2279,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 +2351,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,48 +2388,2879 @@ "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": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "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": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "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" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "robrichards/xmlseclibs", + "version": "3.1.3", + "source": { + "type": "git", + "url": "https://github.com/robrichards/xmlseclibs.git", + "reference": "2bdfd742624d739dfadbd415f00181b4a77aaf07" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/2bdfd742624d739dfadbd415f00181b4a77aaf07", + "reference": "2bdfd742624d739dfadbd415f00181b4a77aaf07", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">= 5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "RobRichards\\XMLSecLibs\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "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/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" + } ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/2.0.2" - }, - "time": "2021-11-05T16:47:00+00:00" + "time": "2025-07-10T08:14:14+00:00" }, { - "name": "psr/log", - "version": "1.1.4", + "name": "symfony/monolog-bundle", + "version": "v3.10.0", "source": { "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + "url": "https://github.com/symfony/monolog-bundle.git", + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", + "reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181", "shasum": "" }, "require": { - "php": ">=5.3.0" + "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" }, - "type": "library", + "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": "1.1.x-dev" + "dev-master": "3.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Log\\": "Psr/Log/" - } + "Symfony\\Bundle\\MonologBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1751,147 +5268,149 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", + "description": "Symfony MonologBundle", + "homepage": "https://symfony.com", "keywords": [ "log", - "psr", - "psr-3" + "logging" ], "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" + "issues": "https://github.com/symfony/monolog-bundle/issues", + "source": "https://github.com/symfony/monolog-bundle/tree/v3.10.0" }, - "time": "2021-05-03T11:20:27+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": "2023-11-06T17:08:13+00:00" }, { - "name": "robrichards/xmlseclibs", - "version": "3.1.1", + "name": "symfony/password-hasher", + "version": "v6.4.24", "source": { "type": "git", - "url": "https://github.com/robrichards/xmlseclibs.git", - "reference": "f8f19e58f26cdb42c54b214ff8a820760292f8df" + "url": "https://github.com/symfony/password-hasher.git", + "reference": "dcab5ac87450aaed26483ba49c2ce86808da7557" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/robrichards/xmlseclibs/zipball/f8f19e58f26cdb42c54b214ff8a820760292f8df", - "reference": "f8f19e58f26cdb42c54b214ff8a820760292f8df", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/dcab5ac87450aaed26483ba49c2ce86808da7557", + "reference": "dcab5ac87450aaed26483ba49c2ce86808da7557", "shasum": "" }, "require": { - "ext-openssl": "*", - "php": ">= 5.4" + "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": { - "RobRichards\\XMLSecLibs\\": "src" - } + "Symfony\\Component\\PasswordHasher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "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": "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": [ - "security", - "signature", - "xml", - "xmldsig" + "hashing", + "password" ], "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" + "source": "https://github.com/symfony/password-hasher/tree/v6.4.24" }, - "license": [ - "GPL-3.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" + } ], - "homepage": "https://www.sencha.com/products/extjs" + "time": "2025-07-10T08:14:14+00:00" }, { - "name": "silex/silex", - "version": "v2.3.0", + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.32.0", "source": { "type": "git", - "url": "https://github.com/silexphp/Silex.git", - "reference": "6bc31c1b8c4ef614a7115320fd2d3b958032f131" + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/silexphp/Silex/zipball/6bc31c1b8c4ef614a7115320fd2d3b958032f131", - "reference": "6bc31c1b8c4ef614a7115320fd2d3b958032f131", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", "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" + "php": ">=7.2" }, - "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" + "suggest": { + "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "2.3.x-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Silex\\": "src/Silex" + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -1900,295 +5419,413 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "name": "Igor Wiedler", - "email": "igor@wiedler.ch" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "The PHP micro-framework based on the Symfony Components", - "homepage": "http://silex.sensiolabs.org", + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", "keywords": [ - "microframework" + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/silexphp/Silex/issues", - "source": "https://github.com/silexphp/Silex/tree/v2.3.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, - "abandoned": "symfony/flex", - "time": "2018-04-20T05:17:01+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": "2024-09-09T11:45:10+00:00" }, { - "name": "simplesamlphp/assert", - "version": "v0.8.0", + "name": "symfony/polyfill-intl-icu", + "version": "v1.33.0", "source": { "type": "git", - "url": "https://github.com/simplesamlphp/assert.git", - "reference": "d3b0f38f4ae083822471c15e3c4a0401ddaeac73" + "url": "https://github.com/symfony/polyfill-intl-icu.git", + "reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/assert/zipball/d3b0f38f4ae083822471c15e3c4a0401ddaeac73", - "reference": "d3b0f38f4ae083822471c15e3c4a0401ddaeac73", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c", + "reference": "bfc8fa13dbaf21d69114b0efcd72ab700fb04d0c", "shasum": "" }, "require": { - "ext-spl": "*", - "php": "^7.4 || ^8.0", - "webmozart/assert": "^1.11" + "php": ">=7.2" }, - "require-dev": { - "simplesamlphp/simplesamlphp-test-framework": "^1.2.1" + "suggest": { + "ext-intl": "For best performance and support of other locales than \"en\"" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "v0.8.x-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "SimpleSAML\\Assert\\": "src/" - } + "Symfony\\Polyfill\\Intl\\Icu\\": "" + }, + "classmap": [ + "Resources/stubs" + ], + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-2.1-or-later" + "MIT" ], "authors": [ { - "name": "Tim van Dijen", - "email": "tvdijen@gmail.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "name": "Jaime Perez Crespo", - "email": "jaimepc@gmail.com" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "A wrapper around webmozart/assert to make it useful beyond checking method arguments", + "description": "Symfony polyfill for intl's ICU-related data and classes", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "icu", + "intl", + "polyfill", + "portable", + "shim" + ], "support": { - "issues": "https://github.com/simplesamlphp/assert/issues", - "source": "https://github.com/simplesamlphp/assert/tree/v0.8.0" + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.33.0" }, - "time": "2022-09-20T20:18:55+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-06-20T22:24:30+00:00" }, { - "name": "simplesamlphp/composer-module-installer", - "version": "v1.3.4", + "name": "symfony/polyfill-intl-idn", + "version": "v1.32.0", "source": { "type": "git", - "url": "https://github.com/simplesamlphp/composer-module-installer.git", - "reference": "36508ed9580a30c4d5ab0bb3c25c00d0b5d42946" + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/composer-module-installer/zipball/36508ed9580a30c4d5ab0bb3c25c00d0b5d42946", - "reference": "36508ed9580a30c4d5ab0bb3c25c00d0b5d42946", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", "shasum": "" }, "require": { - "composer-plugin-api": "^1.1 || ^2.0", - "php": "^7.4 || ^8.0", - "simplesamlphp/assert": "^0.8.0 || ^1.0" + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" }, - "require-dev": { - "composer/composer": "^2.4", - "simplesamlphp/simplesamlphp-test-framework": "^1.2.1" + "suggest": { + "ext-intl": "For best performance" }, - "type": "composer-plugin", + "type": "library", "extra": { - "class": "SimpleSAML\\Composer\\ModuleInstallerPlugin" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "SimpleSAML\\Composer\\": "src/" + "Symfony\\Polyfill\\Intl\\Idn\\": "" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-2.1-only" + "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" ], - "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/symfony/polyfill-intl-idn/tree/v1.32.0" }, - "time": "2023-03-08T20:58:22+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": "2024-09-10T14:38:51+00:00" }, { - "name": "simplesamlphp/saml2", - "version": "v3.2.6", + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.32.0", "source": { "type": "git", - "url": "https://github.com/simplesamlphp/saml2.git", - "reference": "a56e46ef8e0c5245a4ca7facc3d308b493215751" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/saml2/zipball/a56e46ef8e0c5245a4ca7facc3d308b493215751", - "reference": "a56e46ef8e0c5245a4ca7facc3d308b493215751", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", "shasum": "" }, "require": { - "ext-dom": "*", - "ext-openssl": "*", - "ext-zlib": "*", - "php": ">=5.4", - "psr/log": "~1.0", - "robrichards/xmlseclibs": "^3.0" + "php": ">=7.2" }, - "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" + "suggest": { + "ext-intl": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-master": "v3.1.x-dev" + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { "files": [ - "src/_autoload.php" + "bootstrap.php" ], - "psr-0": { - "SAML2\\": "src/" - } + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-2.1-or-later" + "MIT" ], "authors": [ { - "name": "Andreas Åkre Solberg", - "email": "andreas.solberg@uninett.no" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "SAML2 PHP library from SimpleSAMLphp", + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], "support": { - "issues": "https://github.com/simplesamlphp/saml2/issues", - "source": "https://github.com/simplesamlphp/saml2/tree/master" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, - "time": "2018-11-20T11:11:28+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": "2024-09-09T11:45:10+00:00" }, { - "name": "simplesamlphp/simplesamlphp", - "version": "1.16.3", + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", "source": { "type": "git", - "url": "https://github.com/simplesamlphp/simplesamlphp.git", - "reference": "abc208dbc9c94eb8bab8266825ca035cc96072ba" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/simplesamlphp/simplesamlphp/zipball/abc208dbc9c94eb8bab8266825ca035cc96072ba", - "reference": "abc208dbc9c94eb8bab8266825ca035cc96072ba", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "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" + "ext-iconv": "*", + "php": ">=7.2" }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.2", - "mikey179/vfsstream": "~1.6", - "phpunit/phpunit": "~4.8.35" + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } }, - "type": "project", "autoload": { "files": [ - "lib/_autoload_modules.php" + "bootstrap.php" ], "psr-4": { - "SimpleSAML\\": "lib/SimpleSAML" + "Symfony\\Polyfill\\Mbstring\\": "" } }, "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": "Nicolas Grekas", + "email": "p@tchwork.com" }, { - "name": "Jaime Perez", - "email": "jaime.perez@uninett.no" + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "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": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", "keywords": [ - "SAML2", - "idp", - "oauth", - "shibboleth", - "sp", - "ws-federation" + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/simplesamlphp/simplesamlphp/issues", - "source": "https://github.com/simplesamlphp/simplesamlphp" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, - "time": "2018-12-20T16:49:03+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": "2024-12-23T08:48:59+00:00" }, { - "name": "symfony/debug", - "version": "v4.4.44", + "name": "symfony/polyfill-php80", + "version": "v1.33.0", "source": { "type": "git", - "url": "https://github.com/symfony/debug.git", - "reference": "1a692492190773c5310bc7877cb590c04c2f05be" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/1a692492190773c5310bc7877cb590c04c2f05be", - "reference": "1a692492190773c5310bc7877cb590c04c2f05be", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { - "php": ">=7.1.3", - "psr/log": "^1|^2|^3" - }, - "conflict": { - "symfony/http-kernel": "<3.4" - }, - "require-dev": { - "symfony/http-kernel": "^3.4|^4.0|^5.0" + "php": ">=7.2" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Component\\Debug\\": "" + "Symfony\\Polyfill\\Php80\\": "" }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2197,18 +5834,28 @@ ], "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": "Provides tools to ease debugging PHP code", + "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/debug/tree/v4.4.44" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, "funding": [ { @@ -2219,44 +5866,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" } ], - "abandoned": "symfony/error-handler", - "time": "2022-07-28T16:29:46+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { - "name": "symfony/deprecation-contracts", - "version": "v2.5.3", + "name": "symfony/polyfill-php81", + "version": "v1.33.0", "source": { "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "80d075412b557d41002320b96a096ca65aa2c98d" + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/80d075412b557d41002320b96a096ca65aa2c98d", - "reference": "80d075412b557d41002320b96a096ca65aa2c98d", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.2" }, "type": "library", "extra": { "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "2.5-dev" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { "files": [ - "function.php" + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2273,10 +5926,16 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "A generic function and convention to trigger deprecation notices", + "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/deprecation-contracts/tree/v2.5.3" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" }, "funding": [ { @@ -2287,44 +5946,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": "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-php83", + "version": "v1.32.0", "source": { "type": "git", - "url": "https://github.com/symfony/error-handler.git", - "reference": "be731658121ef2d8be88f3a1ec938148a9237291" + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/be731658121ef2d8be88f3a1ec938148a9237291", - "reference": "be731658121ef2d8be88f3a1ec938148a9237291", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", "shasum": "" }, "require": { - "php": ">=7.1.3", - "psr/log": "^1|^2|^3", - "symfony/debug": "^4.4.5", - "symfony/var-dumper": "^4.4|^5.0" - }, - "require-dev": { - "symfony/http-kernel": "^4.4|^5.0", - "symfony/serializer": "^4.4|^5.0" + "php": ">=7.2" }, "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Component\\ErrorHandler\\": "" + "Symfony\\Polyfill\\Php83\\": "" }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2333,18 +5998,24 @@ ], "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 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 +6031,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 +6078,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 +6103,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 +6163,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 +6193,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 +6242,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 +6264,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 +6331,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 +6351,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 +6409,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 +6434,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 +6536,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 +6550,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 +6618,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 +6640,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 +6692,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 +6719,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 +6780,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 +6802,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 +6882,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 +6904,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 +6969,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 +6991,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 +7060,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 +7082,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 +7142,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 +7169,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 +7251,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 +7273,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 +7339,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 +7361,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 +7497,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 +7529,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 +7583,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 +7605,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 +7686,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 +7723,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 +7814,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 +7826,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 +7887,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 +7948,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 +8000,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 +8032,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 +8186,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 +8202,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 +8223,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 +8254,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 +8262,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 +8286,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 +8318,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 +8440,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 +8479,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-main": "9.2.x-dev" } }, "autoload": { @@ -4767,7 +8508,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 +8516,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 +8761,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 +8844,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 +8855,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 +9834,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 +9893,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 +10107,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,10 +10197,10 @@ "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" 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..5cdcf6bd63 --- /dev/null +++ b/config/packages/framework.yaml @@ -0,0 +1,26 @@ +# see https://symfony.com/doc/current/reference/configuration/framework.html +framework: + annotations: + enabled: false + error_controller: CCR\Errors\ErrorController + 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..9b40534baa --- /dev/null +++ b/config/packages/security.yaml @@ -0,0 +1,47 @@ +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 + api: + lazy: true + provider: all_users + json_login: + check_path: /api/login + login_path: /api/login + logout: + path: api_logout + target: / + + + # 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/preload.php b/config/preload.php new file mode 100644 index 0000000000..5ebcdb2153 --- /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[]=" - - -
- - - - - - +declare(strict_types=1); + +use CCR\Kernel; + +require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; + + +// Configurable constants --------------------------- +$orgConfig = \Configuration\XdmodConfiguration::assocArrayFactory( + 'organization.json', + CONFIG_DIR +); +// orgConfig is returned as array(0=>array('name' => '', 'abbrev' => '')) +$org = array_shift($orgConfig); +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/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 @@ ->> 0; - - if (len === 0) { - return false; - } - - var n = fromIndex | 0; - - var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); - - function sameValueZero(x, y) { - return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y)); - } - - while (k < len) { - if (sameValueZero(o[k], valueToFind)) { - return true; - } - k++; - } - - return false; - } - }); -} diff --git a/html/unit_tests/coverage.html b/html/unit_tests/coverage.html deleted file mode 100644 index 25135854ae..0000000000 --- a/html/unit_tests/coverage.html +++ /dev/null @@ -1,91 +0,0 @@ - - - - - Mocha Tests - - - - -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/html/unit_tests/index.html b/html/unit_tests/index.html deleted file mode 100644 index cce0a15919..0000000000 --- a/html/unit_tests/index.html +++ /dev/null @@ -1,79 +0,0 @@ - - - - - Mocha Tests - - - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/html/unit_tests/phantom.js b/html/unit_tests/phantom.js deleted file mode 100644 index 2ab79b36b8..0000000000 --- a/html/unit_tests/phantom.js +++ /dev/null @@ -1,35 +0,0 @@ -/* eslint no-console: "off" */ -var page = require('webpage').create(); - -page.onError = function (msg, trace) { - var msgStack = ['PHANTOM ERROR: ' + msg]; - if (trace && trace.length) { - msgStack.push('TRACE:'); - trace.forEach(function (t) { - msgStack.push(' -> ' + (t.file || t.sourceURL) + ': ' + t.line + (t.function ? ' (in function ' + t.function + ')' : '')); - }); - } - console.log(msgStack.join('\n')); - phantom.exit(1); -}; - -page.onResourceError = function (resourceError) { - console.log('Unable to load resource (#' + resourceError.id + ' URL: ' + resourceError.url + ')'); - console.log('Error code: ' + resourceError.errorCode + '. Description: ' + resourceError.errorString); -}; - -page.onConsoleMessage = function (msg) { - console.log(msg); -}; - -page.open('file://' + phantom.libraryPath + '/index.html', function (status) { - var failures = -1; - if (status === 'success') { - failures = page.evaluate(function () { - return mocha.run().failures; - }); - } - console.log('Javascript Unit Test Failures: ' + failures); - phantom.exit(failures); -}); - diff --git a/html/unit_tests/spec/.eslintrc.json b/html/unit_tests/spec/.eslintrc.json deleted file mode 100644 index 4ed08dc660..0000000000 --- a/html/unit_tests/spec/.eslintrc.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "env": { - "mocha": true - }, - "globals": { - "expect": false - }, - "rules": { - "no-unused-expressions": "off" - } -} - diff --git a/html/unit_tests/spec/CCRTokenizeSpec.js b/html/unit_tests/spec/CCRTokenizeSpec.js deleted file mode 100644 index c5bf57340c..0000000000 --- a/html/unit_tests/spec/CCRTokenizeSpec.js +++ /dev/null @@ -1,71 +0,0 @@ -describe('XDMoD.Viewer', function () { - describe('Various Successful Tokenizations', function () { - it('tab panel / tab', function () { - var token = CCR.tokenize('#main_tab_panel:tg_summary'); - - expect(token).to.deep.equal({ - raw: '#main_tab_panel:tg_summary', - content: 'main_tab_panel:tg_summary', - root: 'main_tab_panel', - tab: 'tg_summary', - subtab: '', - params: '' - }); - }); - - it('tab only', function () { - var token = CCR.tokenize('#tg_summary'); - - expect(token).to.deep.equal({ - raw: '#tg_summary', - content: 'tg_summary', - root: '', - tab: 'tg_summary', - subtab: '', - params: '' - }); - }); - - it('tab only params', function () { - var content = 'tg_usage?node=statistic_Jobs_none_total_cpu_hours'; - var token = CCR.tokenize('#' + content); - - expect(token).to.deep.equal({ - raw: '#' + content, - content: content, - root: '', - tab: 'tg_usage', - subtab: '', - params: 'node=statistic_Jobs_none_total_cpu_hours' - }); - }); - - it('tab panel / tab w/ params', function () { - var content = 'main_tab_panel:job_viewer?realm=SUPREMM&recordid=29&jobid=7193418&infoid=0'; - var token = CCR.tokenize('#' + content); - - expect(token).to.deep.equal({ - raw: '#' + content, - content: content, - root: 'main_tab_panel', - tab: 'job_viewer', - subtab: '', - params: 'realm=SUPREMM&recordid=29&jobid=7193418&infoid=0' - }); - }); - - it('tab panel / tab / subtab w/ params', function () { - var content = 'main_tab_panel:app_kernels:app_kernel_viewer?kernel=29&start=2017-03-01&end=2017-03-31'; - var token = CCR.tokenize('#' + content); - - expect(token).to.deep.equal({ - raw: '#' + content, - content: content, - root: 'main_tab_panel', - tab: 'app_kernels', - subtab: 'app_kernel_viewer', - params: 'kernel=29&start=2017-03-01&end=2017-03-31' - }); - }); - }); -}); diff --git a/html/unit_tests/spec/ChangeStackSpec.js b/html/unit_tests/spec/ChangeStackSpec.js deleted file mode 100644 index 57e3197e78..0000000000 --- a/html/unit_tests/spec/ChangeStackSpec.js +++ /dev/null @@ -1,141 +0,0 @@ -describe("XDMoD.ChangeStack", function() { - var spy = chai.spy(); - - describe("Object Initialization", function() { - - it("empty config", function() { - - var cs = new XDMoD.ChangeStack({}); - - expect(cs.canUndo()).to.be.false; - expect(cs.canRedo()).to.be.false; - expect(cs.isMarked()).to.be.false; - expect(cs.canRevert()).to.be.false; - expect(cs.empty()).to.be.true; - - expect(function() { cs.mark(); }).to.throw(Error); - expect(function() { cs.undo(); }).to.throw(Error); - expect(function() { cs.redo(); }).to.throw(Error); - expect(function() { cs.revertToMarked(); }).to.throw(Error); - expect(function() { cs.add(); }).to.throw(Error); - }); - - it("baseParams", function() { - - var entry = {test: 1}; - - var cs = new XDMoD.ChangeStack({baseParams: entry}); - - expect(cs.canUndo()).to.be.false; - expect(cs.canRedo()).to.be.false; - expect(cs.isMarked()).to.be.false; - expect(cs.canRevert()).to.be.false; - expect(cs.empty()).to.be.false; - - cs.on('update', spy); - - cs.mark(); - expect(spy).to.have.been.called.with(cs, {test: 1}, 'mark'); - - expect(cs.isMarked()).to.be.true; - }); - }); - - describe("Auto commit", function() { - - var cs = new XDMoD.ChangeStack({}); - cs.on('update', spy); - - it("add some changes", function() { - - cs.disableAutocommit(); - - cs.add({test: 1}); - expect(spy).to.have.been.called.with(cs, {test: 1}, 'add'); - - expect(cs.canUndo()).to.be.false; - expect(cs.canRedo()).to.be.false; - expect(cs.empty()).to.be.true; - - cs.add({test: 2}); - expect(spy).to.have.been.called.with(cs, {test: 2}, 'add'); - - cs.commit() - expect(spy).to.have.been.called.with(cs, {test: 2}, 'commit'); - expect(cs.empty()).to.be.false; - - cs.enableAutocommit(); - - cs.commit() - expect(spy).to.have.not.been.called; - - cs.add({test: 3}); - expect(spy).to.have.been.called.with(cs, {test: 3}, 'add'); - - cs.undo(); - expect(spy).to.have.been.called.with(cs, {test: 2}, 'undo'); - - expect(cs.canUndo()).to.be.false; - }); - }); - - describe("Stack Operations", function() { - - var cs = new XDMoD.ChangeStack({}); - cs.on('update', spy); - - it("linear push pop", function() { - - var i; - for(i = 0; i < 10; i++) { - cs.add({test: i}); - } - expect(cs.canRedo()).to.be.false; - expect(cs.canUndo()).to.be.true; - - cs.undo(); - expect(spy).to.have.been.called.with(cs, {test: 8}, 'undo'); - - expect(cs.canRedo()).to.be.true; - expect(cs.canUndo()).to.be.true; - - cs.undo(); - expect(spy).to.have.been.called.with(cs, {test: 7}, 'undo'); - - cs.redo(); - expect(spy).to.have.been.called.with(cs, {test: 8}, 'redo'); - }); - - it("save state", function() { - - cs.undo(); - expect(spy).to.have.been.called.with(cs, {test: 7}, 'undo'); - - expect(cs.canRevert()).to.be.false; - - cs.mark(); - expect(spy).to.have.been.called.with(cs, {test: 7}, 'mark'); - expect(cs.isMarked()).to.be.true; - expect(cs.canRevert()).to.be.false; - - cs.undo(); - expect(spy).to.have.been.called.with(cs, {test: 6}, 'undo'); - expect(cs.isMarked()).to.be.false; - expect(cs.canRevert()).to.be.true; - - cs.undo(); - expect(spy).to.have.been.called.with(cs, {test: 5}, 'undo'); - cs.undo(); - expect(spy).to.have.been.called.with(cs, {test: 4}, 'undo'); - - cs.revertToMarked(); - expect(spy).to.have.been.called.with(cs, {test: 7}, 'reverttomarked'); - expect(cs.isMarked()).to.be.true; - - expect(cs.canRedo()).to.be.false; - - cs.undo(); - expect(spy).to.have.been.called.with(cs, {test: 4}, 'undo'); - }); - }); -}); diff --git a/html/unit_tests/spec/JobViewerSpec.js b/html/unit_tests/spec/JobViewerSpec.js deleted file mode 100644 index 8a135cbcc9..0000000000 --- a/html/unit_tests/spec/JobViewerSpec.js +++ /dev/null @@ -1,91 +0,0 @@ -describe('XDMoD.JobViewer', function () { - var jv = new XDMoD.Module.JobViewer(); - - describe('compareNodePath tests', function () { - it('matching', function () { - var node = { - attributes: { - dtype: 'b', - b: 2 - }, - parentNode: { - attributes: { - dtype: 'a', - a: 1 - }, - parentNode: {} - } - }; - - var path = [{ dtype: 'a', value: '1' }, { dtype: 'b', value: '2' }]; - - expect(jv.compareNodePath(node, path)).to.be.true; - }); - - it('diff dtype', function () { - var node = { - attributes: { - dtype: 'b', - b: 2 - }, - parentNode: { - attributes: { - dtype: 'z', - z: 1 - }, - parentNode: {} - } - }; - - var path = [{ dtype: 'a', value: '1' }, { dtype: 'b', value: '2' }]; - - expect(jv.compareNodePath(node, path)).to.be.false; - }); - - it('diff array longer', function () { - var node = { - attributes: { - dtype: 'b', - b: 2 - }, - parentNode: { - attributes: { - dtype: 'a', - a: 1 - }, - parentNode: {} - } - }; - - var path = [{ dtype: 'a', value: '1' }, { dtype: 'b', value: '2' }, { dtype: 'c', value: '3' }]; - - expect(jv.compareNodePath(node, path)).to.be.false; - }); - - it('diff node path longer', function () { - var node = { - attributes: { - dtype: 'b', - b: 2 - }, - parentNode: { - attributes: { - dtype: 'a', - a: 1 - }, - parentNode: {} - } - }; - - var path = [{ dtype: 'b', value: '2' }]; - - expect(jv.compareNodePath(node, path)).to.be.false; - }); - - it('data format functions', function () { - expect(jv.formatData(60, 'seconds')).to.equal('1 minute '); - expect(jv.formatData(10240, 'B/s')).to.equal('10.00 KiB/s'); - expect(jv.formatData(11100000000, '1')).to.equal('11.1 G'); - }); - }); -}); diff --git a/html/unit_tests/spec/XDMoDFormatSpec.js b/html/unit_tests/spec/XDMoDFormatSpec.js deleted file mode 100644 index d97b900d9e..0000000000 --- a/html/unit_tests/spec/XDMoDFormatSpec.js +++ /dev/null @@ -1,89 +0,0 @@ -describe('XDMoD.Format', function () { - describe('Check Format functions', function () { - it('SI formatting', function () { - var test_cases = [ - [100001, 'B', 3, '100 kB'], - [10100001, 'B', 3, '10.1 MB'], - [0.0001, 'B', 2, '0.0001 B'], - [0.00033, 'B', 2, '0.00033 B'], - [1.00001, 'B', 1, '1 B'], - [1, '', 2, '1 '], - [10, '', 2, '10 '], - [100, '', 2, '100 '], - [1000, '', 2, '1 k'], - [10000, '', 2, '10 k'], - [100000, '', 2, '100 k'], - [1000000, '', 2, '1 M'], - [10000000, '', 2, '10 M'], - [100000000, '', 2, '100 M'], - [1000000000, '', 2, '1 G'], - [9, '', 2, '9 '], - [99, '', 2, '99 '], - [999, '', 2, '1 k'], - [9999, '', 2, '10 k'], - [99999, '', 2, '100 k'], - [999999, '', 2, '1 M'], - [9999999, '', 2, '10 M'], - [99999999, '', 2, '100 M'], - [999999999, '', 2, '1 G'], - [9999999999, '', 2, '10 G'], - [1, '', 4, '1 '], - [10, '', 4, '10 '], - [100, '', 4, '100 '], - [1000, '', 4, '1 k'], - [10000, '', 4, '10 k'], - [100000, '', 4, '100 k'], - [1000000, '', 4, '1 M'], - [10000000, '', 4, '10 M'], - [100000000, '', 4, '100 M'], - [1000000000, '', 4, '1 G'], - [9, '', 4, '9 '], - [99, '', 4, '99 '], - [999, '', 4, '999 '], - [9999, '', 4, '9.999 k'], - [99999, '', 4, '100 k'], - [999999, '', 4, '1 M'], - [9999999, '', 4, '10 M'], - [99999999, '', 4, '100 M'], - [999999999, '', 4, '1 G'], - [9999999999, '', 4, '10 G'] - ]; - - var i; - for (i = 0; i < test_cases.length; i++) { - expect(XDMoD.utils.format.convertToSiPrefix(test_cases[i][0], test_cases[i][1], test_cases[i][2])).to.equal(test_cases[i][3]); - } - }); - - it('Binary formatting', function () { - var test_cases = [ - [1025, 'B', 3, '1.00 KiB'], - [10100001, 'B', 3, '9.63 MiB'], - [0.0001, 'B', 2, '0.00010 B'], - [1.00001, 'B', 1, '1 B'] - ]; - - var i; - for (i = 0; i < test_cases.length; i++) { - expect(XDMoD.utils.format.convertToBinaryPrefix(test_cases[i][0], test_cases[i][1], test_cases[i][2])).to.equal(test_cases[i][3]); - } - }); - - it('Elapsed time', function () { - var test_cases = [ - [1, '1 second '], - [2, '2 seconds '], - [3600, '1 hour 0.0 minute '], - [3601, '1 hour 0.0 minute '], - [3600 + (5 * 60), '1 hour 5.0 minutes '], - [24 * 3600, '1 day 0.0 hour '], - [(3 * 24 * 3600) + 3600, '3 days 1.0 hour '] - ]; - - var i; - for (i = 0; i < test_cases.length; i++) { - expect(XDMoD.utils.format.humanTime(test_cases[i][0])).to.equal(test_cases[i][1]); - } - }); - }); -}); diff --git a/libraries/response.php b/libraries/response.php index e7aaf47a74..a1badc19ba 100644 --- a/libraries/response.php +++ b/libraries/response.php @@ -67,24 +67,3 @@ function presentError($error) { xd_controller\returnJSON(buildError($error)); } - -/** - * Sets response headers appropriate for dynamically-generated JavaScript. - * - * @param boolean $allow_caching Allow the generated JavaScript to be cached. - * (Defaults to false.) - */ -function useDynamicJavascriptHeaders($allow_caching = false) { - // Set the content type of the response to JavaScript. - header('Content-Type: application/javascript'); - - // If desired, prevent this response from being cached. - // See Example #2: http://php.net/manual/en/function.header.php - // See: http://stackoverflow.com/a/13640164 - if (!$allow_caching) { - header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0"); - header("Cache-Control: post-check=0, pre-check=0", false); - header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); - header("Pragma: no-cache"); - } -} diff --git a/libraries/rest.php b/libraries/rest.php deleted file mode 100644 index 0754b1aaa3..0000000000 --- a/libraries/rest.php +++ /dev/null @@ -1,52 +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/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..b6873dca87 --- /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..2cdec83ff5 --- /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..c5e0d8399a --- /dev/null +++ b/src/Controller/AuthenticationController.php @@ -0,0 +1,248 @@ +logger = $logger; + $this->parameters = $parameters; + $this->ssoUrl = $this->parameters->get('sso')['login_link']; + parent::__construct($logger, $twig, $tokenHelper); + } + + /** + * This route is here so that we make sure the XDUser::postLogin function is called and that the users token is set + * in the appropriate location for use throughout the users session. The actual "login" process is handled by + * `src/Authenticators/FormLoginAuthenticator` with configuration located in `config/packages/security.yaml`. + * @return Response + */ + #[Route('{prefix}/login', name: 'xdmod_login', requirements: ['prefix' => '.*'], methods: ['POST'])] + #[Route('/login', name: 'xdmod_new_login', methods: ['POST'])] + public function formLogin(): Response + { + $user = $this->getUser(); + + if (null === $user) { + $this->logger->error('No user found during login.'); + return $this->json([ + 'success' => false, + ], Response::HTTP_UNAUTHORIZED); + } + + // If for some reason we didn't get an \XDUser then fail fast. + // ( Honestly this is really here to make sure auto-complete works for $user ) + if (!($user instanceof \XDUser)) { + $this->logger->error('User instance type mismatched.'); + return $this->json([ + 'success' => false, + 'message' => 'User type mismatch' + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + + try { + $user->postLogin(); + } catch (Exception $e) { + $this->logger->error( + sprintf( + 'An error has occurred during the post-login process for %s', + $user->getUsername() + ) + ); + return $this->json([ + 'success' => false, + 'message' => 'Error occurred during post login process.' + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + + $token = $user->getToken(); + $response = $this->json([ + 'success' => true, + 'results' => [ + 'token' => $token, + 'name' => $user->getFormalName() + ] + ]); + + // This cookie will tell the HomeController that we have a currently logged in user. + $response->headers->setCookie(new Cookie('xdmod_token', $token)); + + $this->logger->info(sprintf('Successful login by %s', $user->getUsername())); + + return $response; + } + + /** + * 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`. + * + * + * + * @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 formLogout(Request $request): Response + { + $this->logger->error('*** FormLogout ***'); + $token = $request->getSession()->get('xdmod_token'); + \XDSessionManager::logoutUser($token); + $request->getSession()->invalidate(); + + $response = $this->redirectToRoute('xdmod_home'); + $response->headers->removeCookie('xdmod_token'); + return $response; + } + + /** + * This route is responsible for logging API users in. The configuration for this route is located in + * `config/packages/security.yaml`. + * + * @param Request $request + * + * @return Response + * @throws Exception + */ + #[Route('{prefix}/api/login', name: 'api_login', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function login(Request $request): Response + { + $user = $this->getUser(); + + if (null === $user) { + return $this->json([ + 'message' => 'missing credentials' + ], Response::HTTP_UNAUTHORIZED); + } + + $xdUser = \XDUser::getUserByUserName($user->getUserIdentifier()); + + $xdUser->postLogin(); + + $request->getSession()->set('xdUser', $xdUser->getUserID()); + + + $response = $this->json([ + 'user' => $user->getUserIdentifier(), + 'token' => $xdUser->getToken() + ]); + + + // Make sure that we remove any xdmod_token cookie that already exists so that it can be set with the correct + // token. + $cookies = $response->headers->getCookies(); + foreach ($cookies as $cookie) { + if ($cookie->getName() === 'xdmod_token') { + $response->headers->removeCookie('xdmod_token'); + } + } + + $response->headers->setCookie(Cookie::create('xdmod_token', $xdUser->getToken(), 0, '/', '', true)); + + return $response; + } + + /** + * This Route is responsible for logging API Users out. + * + * @return Response + * + * @throws Exception since this should never be called. + */ + #[Route('{prefix}/api/logout', name: 'api_logout', requirements: ['prefix' => '.*'], methods: ['POST'])] + public function logout(): Response + { + session_destroy(); + throw new Exception("Don't forget to activate logout."); + } + + /** + * @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'); + $value = $this->ssoUrl; + if (!empty($returnTo)) { + $ssoUrl = $this->ssoUrl; + $returnTo = urlencode($returnTo); + $value = "{$ssoUrl}?ReturnTo=$returnTo"; + $request->getSession()->set('_security.main.target_path', $returnTo); + } + $this->logger->debug('IDP Redirect', [$value]); + return new Response($value, Response::HTTP_OK, ['Content-Type' => 'text/plain']); + } + + + #[Route('/jwt-redirect', methods: ['GET'])] + public function redirectWithJwt(Request $request): Response + { + 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/src/Controller/BaseController.php b/src/Controller/BaseController.php new file mode 100644 index 0000000000..0922f4453b --- /dev/null +++ b/src/Controller/BaseController.php @@ -0,0 +1,809 @@ +logger = $logger; + $this->twig = $twig; + $this->tokenHelper = $tokenHelper; + } + + + /** + * 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()); + $this->logger->debug( + sprintf( + 'Attempting to authorize user: %s (%s) with requirements: %s', + $user->getUsername(), + var_export($user->getAclNames(), true), + var_export($requiredAcls, true) + ) + ); + // 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 Session $session + * @return XDUser + * @throws Exception + */ + protected function getXDUser(Session $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(); + } + + switch ($failover_methods[0]) { + case XDUser::PUBLIC_USER: + if ( + (isset($_REQUEST['public_user']) && $_REQUEST['public_user'] === 'true') || + ($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 ( + (isset($_REQUEST['public_user']) && $_REQUEST['public_user'] === 'true') || + ($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 = ''; + $captchaSecret = ''; + try { + $captchaSiteKey = \xd_utilities\getConfiguration('mailer', 'captcha_public_key'); + $captchaSecret = \xd_utilities\getConfiguration('mailer', 'captcha_private_key'); + } catch (\Exception $e) { + } + + $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..616d3ea295 --- /dev/null +++ b/src/Controller/ChartPoolController.php @@ -0,0 +1,107 @@ +authorize($request); + } catch (Exception $e) { + return $this->json(buildError(new \SessionExpiredException()), 401); + } + + $operation = $this->getStringParam($request, 'operation', true); + switch ($operation) { + case 'add_to_queue': + return $this->addToQueue($request, $user); + case 'remove_from_queue': + return $this->removeFromQueue($request, $user); + default: + throw new BadRequestHttpException('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/src/Controller/DashboardController.php b/src/Controller/DashboardController.php new file mode 100644 index 0000000000..ef461ea563 --- /dev/null +++ b/src/Controller/DashboardController.php @@ -0,0 +1,519 @@ + '.*'])] +class DashboardController extends BaseController +{ + /** + * The individual dashboard components have a namespace prefix to simplify + * the implementation of the algorithm that determines which + * components to display. There are two sources of configuration data for + * the components. The roles configuration file and the user configuration + * (in the database). The user configuration only contains chart components. + * The user configuration is handled via the "Show in Summary tab" checkbox + * in the metric explorer. + * + * Non-chart components and the full-width components are defined in the roles + * configuration file and are not overrideable. + * + * Chart components are handled as follows: + * - All user charts with "show in summary tab" checked will be displayed + * - 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. + */ + private const TOP_COMPONENT = 't.'; + private const CHART_COMPONENT = 'c.'; + private const NON_CHART_COMPONENT = 'p.'; + + /** + * @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->getXDUser($request->getSession()); + + $dashboardComponents = []; + + $mostPrivilegedAcl = Acls::getMostPrivilegedAcl($user)->getName(); + + $layout = $this->getLayout($user); + + $roleConfig = \Configuration\XdmodConfiguration::assocArrayFactory( + 'roles.json', + CONFIG_DIR, + null, + ['config_variables' => $this->getConfigVariables($user)] + ); + + $presets = $roleConfig['roles'][$mostPrivilegedAcl]; + + if (isset($presets['dashboard_components'])) { + + foreach($presets['dashboard_components'] as $component) { + + $componentType = self::NON_CHART_COMPONENT; + + if (isset($component['region']) && $component['region'] === 'top') { + $componentType = self::TOP_COMPONENT; + $chartLocation = $componentType . $component['name']; + $column = -1; + } else { + if ($component['type'] === 'xdmod-dash-chart-cmp') { + $componentType = self::CHART_COMPONENT; + $component['config']['name'] = $component['name']; + $component['config']['chart']['featured'] = true; + } + + $defaultLayout = null; + if (isset($component['location']) && isset($component['location']['row']) && isset($component['location']['column'])) { + $defaultLayout = array($component['location']['row'], $component['location']['column']); + } + + list($chartLocation, $column) = $layout->getLocation($componentType . $component['name'], $defaultLayout); + } + + $dashboardComponents[$chartLocation] = array( + 'name' => $componentType . $component['name'], + 'type' => $component['type'], + 'config' => isset($component['config']) ? $component['config'] : array(), + 'column' => $column + ); + } + } + + if ($user->isPublicUser() === false) + { + $queryStore = new \UserStorage($user, 'queries_store'); + $queries = $queryStore->get(); + + if ($queries != null) { + foreach ($queries as $query) { + if (!isset($query['config']) || !isset($query['name'])) { + continue; + } + + $queryConfig = json_decode($query['config']); + + if (!isset($queryConfig->featured) || !$queryConfig->featured) { + continue; + } + + $name = self::CHART_COMPONENT . $query['name']; + + list($chartLocation, $column) = $layout->getLocation($name); + + $dashboardComponents[$chartLocation] = [ + 'name' => $name, + 'type' => 'xdmod-dash-chart-cmp', + 'config' => [ + 'name' => $query['name'], + 'chart' => $queryConfig + ], + 'column' => $column + ]; + } + } + } + + ksort($dashboardComponents); + + return $this->json([ + 'success' => true, + 'total' => count($dashboardComponents), + 'portalConfig' => ['columns' => $layout->getColumnCount()], + 'data' => array_values($dashboardComponents) + ]); + } + + /** + * @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. + */ + #[Route('/layout', methods: ['POST'])] + public function setLayout(Request $request): Response + { + $user = $this->authorize($request); + + $content = json_decode($this->getStringParam($request, 'data', true), true); + + if ($content === null || !isset($content['layout']) || !isset($content['columns'])) { + throw new BadRequestException('Invalid data parameter'); + } + + $storage = new \UserStorage($user, 'summary_layout'); + + return $this->json([ + 'success' => true, + 'total' => 1, + 'data' => $storage->upsert(0, $content) + ]); + } + + /** + * @param Request $request + * @return Response + * @throws Exception if there is a problem authorizing the current user. + */ + #[Route('/layout', methods: ['DELETE'])] + public function resetLayout(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $user = $this->authorize($request); + + $storage = new \UserStorage($user, 'summary_layout'); + + $storage->del(); + + return $this->json([ + 'success' => true, + 'total' => 1 + ]); + } + + /** + * @param Request $request + * @return Response + * @throws Exception if there is a problem authorizing the current user. + */ + #[Route('/rolereport', methods: ['GET'])] + public function getRoleReport(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $user = $this->authorize($request); + + $role = $user->getMostPrivilegedRole()->getName(); + $report_id_suffix = 'autogenerated-' . $role; + $report_id = $user->getUserID() . '-' . $report_id_suffix; + $userReport = null; + $rm = new \XDReportManager($user); + $reports = $rm->fetchReportTable(); + foreach ($reports as &$report) { + if ($report['report_id'] === $report_id) { + $userReport = $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->buildReportFromTemplate($_REQUEST, $report_id_suffix); + $reports = $rm->fetchReportTable(); + foreach ($reports as &$report) { + if ($report['report_id'] === $report_id) { + $userReport = $report; + } + } + } + $data = $rm->loadReportData($userReport['report_id']); + $count = 0; + foreach($data['queue'] as $queue) { + $chart_id = explode('&', $queue['chart_id']); + $chart_id_parsed = array(); + foreach($chart_id as $value) { + list($key, $value) = explode('=', $value); + $key = urldecode($key); + $value = urldecode($value); + $json = json_decode($value, true); + + if ($key === 'timeseries') { + $value = $value === 'y' || $value === 'true'; + } elseif ($json !== null) { + $value = $json; + } + $chart_id_parsed[$key] = $value; + } + $data['queue'][$count]['chart_id'] = $chart_id_parsed; + $count++; + } + return $this->json([ + 'success' => true, + 'total' => count($data), + 'data' => $data + ]); + } + + /** + * @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 + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $user = $this->authorize($request); + // fetch charts + $queries = new \UserStorage($user, 'queries_store'); + $data = $queries->get(); + foreach ($data as &$query) { + $query['name'] = htmlspecialchars($query['name'], ENT_COMPAT, 'UTF-8', false); + $query['type'] = 'Chart'; + } + // fetch reports + $rm = new \XDReportManager($user); + $reports = $rm->fetchReportTable(); + foreach ($reports as &$report) { + $tmp = []; + $tmp['type'] = 'Report'; + $tmp['name'] = $report['report_name']; + $tmp['chart_count'] = $report['chart_count']; + $tmp['charts_per_page'] = $report['charts_per_page']; + $tmp['creation_method'] = $report['creation_method']; + $tmp['report_delivery'] = $report['report_delivery']; + $tmp['report_format'] = $report['report_format']; + $tmp['report_id'] = $report['report_id']; + $tmp['report_name'] = $report['report_name']; + $tmp['report_schedule'] = $report['report_schedule']; + $tmp['report_title'] = $report['report_title']; + $tmp['ts'] = $report['last_modified']; + $tmp['config'] = $report['report_id']; + $data[] = $tmp; + } + return $this->json([ + 'success' => true, + 'total' => count($data), + 'data' => $data + ]); + } + + /** + * @param Request $request + * @return Response + */ + #[Route('/viewedUserTour', methods: ['POST'])] + public function setViewedUserTour(Request $request): Response + { + $user = $this->authorize($request); + $viewedTour = $this->getIntParam($request, 'viewedTour', true); + + if (!in_array($viewedTour, [0,1])) { + throw new BadRequestHttpException('Invalid data parameter'); + } + + $storage = new \UserStorage($user, 'viewed_user_tour'); + + return $this->json([ + 'success' => true, + 'total' => 1, + 'msg' => $storage->upsert(0, ['viewedTour' => $viewedTour]) + ]); + } + + /** + * + * @param Request $request + * @return Response + */ + #[Route('/viewedUserTour', methods: ['GET'])] + public function getViewedUserTour(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $user = $this->authorize($request); + $storage = new \UserStorage($user, 'viewed_user_tour'); + return $this->json([ + 'success' => true, + 'total' => 1, + 'data' => $storage->get() + ]); + } + + /** + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('/statistics', methods: ['GET'])] + public function getStatistics(Request $request): Response + { + try { + $user = $this->authorize($request); + } catch (Exception $e) { + $user = XDUser::getPublicUser(); + } + + $aggregationUnit = $request->get('aggregation_unit', 'auto'); + + $startDate = $this->getStringParam($request, 'start_date', true); + $endDate = $this->getStringParam($request, 'end_date', true); + + $this->checkDateRange($startDate, $endDate); + + $this->logger->debug('Date Range is Copacetic!'); + // 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 { + $this->logger->debug('Running Aggregate Query!'); + $query = new \DataWarehouse\Query\AggregateQuery( + 'Jobs', + $aggregationUnit, + $startDate, + $endDate, + 'none', + 'all' + ); + + $result = $query->execute(); + } catch (PDOException $e) { + $this->logger->debug('Exception while running query: %s', buildError($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; + } + } catch (Exception $e) { + $this->logger->debug('Exception while running query: %s', buildError($e)); + throw new BadRequestHttpException($e->getMessage()); + } + + $this->logger->debug('Successfully ran query!'); + $rawRoles = XdmodConfiguration::assocArrayFactory('roles.json', CONFIG_DIR); + + $mostPrivileged = $user->getMostPrivilegedRole()->getName(); + $formats = $rawRoles['roles'][$mostPrivileged]['statistics_formats']; + + $this->logger->debug('Returning Data'); + return $this->json( + [ + 'totalCount' => 1, + 'success' => true, + 'message' => '', + 'formats' => $formats, + 'data' => [$result] + ] + ); + } + + /* + * Get the column layout manager for the user + * + * @return \CCR\ColumnLayout + */ + /** + * @param XDUser $user + * @return ColumnLayout + */ + private function getLayout(XDUser $user): ColumnLayout + { + $defaultLayout = null; + $defaultColumnCount = 2; + + if ($user->isPublicUser() === false) { + $layoutStore = new \UserStorage($user, 'summary_layout'); + $record = $layoutStore->getById(0); + if ($record) { + $defaultLayout = $record['layout']; + $defaultColumnCount = $record['columns']; + } + } + + return new ColumnLayout($defaultColumnCount, $defaultLayout); + } + + /** + * 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) + { + $this->logger->debug('Checking Date Rage'); + $startTimestamp = $this->getTimestamp($startDate, 'start_date'); + $endTimestamp = $this->getTimestamp($endDate, 'end_date'); + + $this->logger->debug(sprintf('Start Timestamp: %s', $startTimestamp)); + $this->logger->debug(sprintf('End Timestamp: %s', $endTimestamp)); + + 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') + { + $this->logger->debug(sprintf('Getting Timestamp for %s %s', $date, $format)); + + $parsed = date_parse_from_format($format, $date); + $this->logger->debug(sprintf('Parsed: %s', var_export($parsed, true))); + if ($parsed['year'] === false || $parsed['month'] === false || $parsed['day'] === false) { + $this->logger->debug(sprintf('Unable to parse %s', $paramName)); + 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'] + ); + $this->logger->debug(sprintf('Date: %s', var_export($date, true))); + if ($date === false || $parsed['error_count'] > 0) { + $this->logger->debug('Unable to get timestamp!'); + throw new BadRequestHttpException("Unable to parse $paramName"); + } + + $this->logger->debug('Successfully made timestamp!'); + return $date; + } + + /** + * @param XDUser $user + * @return array + */ + private function getConfigVariables(XDUser $user): array + { + $person_id = $user->getPersonID(true); + $obj_warehouse = new \XDWarehouse(); + + return [ + 'PERSON_ID' => $person_id, + 'PERSON_NAME' => $obj_warehouse->resolveName($person_id) + ]; + } +} diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php new file mode 100644 index 0000000000..f9e0ad45f6 --- /dev/null +++ b/src/Controller/HomeController.php @@ -0,0 +1,359 @@ + [ + 'entityId', + 'singleSignOnService' => [ + 'url', + 'binding' + ], + 'singleLogoutService' => [ + 'url', + 'binding' + ] + ], + 'sp' => [ + 'entityId', + 'assertionConsumerService' => [ + 'url', + 'binding' + ], + 'singleLogoutService' => [ + 'url', + 'binding' + ] + ] + ]; + private $parameters; + + public function __construct(LoggerInterface $logger, Environment $twig, Tokens $tokenHelper, ContainerBagInterface $parameters) + { + parent::__construct($logger, $twig, $tokenHelper); + $this->parameters = $parameters; + } + + /** + * 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 = []; + $ssoSettings = $this->getParameter('sso'); + 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 + try { + $jupyterHubURL = \xd_utilities\getConfiguration('jupyterhub', 'url'); + $jupyterIsEnabled = !empty($jupyterHubURL); + } catch (\Exception $e) { + $jupyterIsEnabled = false; + $jupyterHubURL = ''; + } + + $params = [ + 'user_logged_in' => $userLoggedIn, + 'user' => $user, + 'person_name' => sprintf('%s, %s', $personInfo[0]['last_name'], $personInfo[0]['first_name']), + 'title' => \xd_utilities\getConfiguration('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', + \xd_utilities\getConfiguration('rest', 'base'), + \xd_utilities\getConfiguration('rest', 'version') + ), + 'realms' => $realms, + 'tech_support_recipient' => \xd_utilities\getConfiguration('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' => $ssoSettings['show_local_login'], + 'sso_direct_link' => $ssoSettings['direct_link'], + '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 = \xd_utilities\getConfiguration('mailer', 'captcha_public_key'); + $captchaSecret = \xd_utilities\getConfiguration('mailer', 'captcha_private_key'); + if ('' !== $captchaSiteKey && '' !== $captchaSecret) { + $result = $captchaSiteKey; + } + } + + return $result; + } + + + public function getLogoData() + { + try { + $logo = \xd_utilities\getConfiguration('general', 'center_logo'); + $logo_width = \xd_utilities\getConfiguration('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..0602999346 --- /dev/null +++ b/src/Controller/InternalDashboard/InternalDashboardController.php @@ -0,0 +1,463 @@ +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'), + ]; + + 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'); + switch ($operation) { + case 'get_menu': + return $this->getMenus($request); + default: + throw new BadRequestHttpException(); + } + } + + /** + * + * @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'); + switch ($operation) { + case 'get_summary': + return $this->getUserSummary($request); + default: + throw new BadRequestHttpException(); + } + } + + /** + * @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', true); + 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([ + 'success' => false, + 'message' => 'operation not recognized' + ]); + } + + /** + * 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..07a2097f7b --- /dev/null +++ b/src/Controller/InternalDashboard/LogController.php @@ -0,0 +1,184 @@ +get('operation'); + switch ($operation) { + case 'get_levels': + return $this->getLevels($request); + case 'get_summary': + return $this->getSummary($request); + case 'get_messages': + return $this->getMessages($request); + default: + throw new BadRequestHttpException(); + } + } + + /** + * + * @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..548ffc1162 --- /dev/null +++ b/src/Controller/InternalDashboard/MailerController.php @@ -0,0 +1,114 @@ +denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + $operation = $this->getStringParam($request, 'operation', true); + + switch ($operation) { + case 'enum_target_addresses': + return $this->enumTargetAddresses($request); + case 'send_plain_mail': + return $this->sendPlainMail($request); + default: + throw new BadRequestHttpException('Unknown operation.'); + } + } + + /** + * 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..486b33ce76 --- /dev/null +++ b/src/Controller/InternalDashboard/SummaryController.php @@ -0,0 +1,307 @@ +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', true); + + switch ($operation) { + case 'get_config': + return $this->getConfig($request); + case 'get_portlets': + return $this->getPortlets($request); + default: + throw new NotFoundHttpException('Unknown Operation Provided'); + } + } + + /** + * @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..b337dd411d --- /dev/null +++ b/src/Controller/InternalDashboard/UserAdminController.php @@ -0,0 +1,981 @@ +denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $this->authorize($request, ['mgr']); + + $operation = $this->getStringParam($request, 'operation', true); + 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); + } + throw new BadRequestHttpException('invalid operation specified'); + } + + /** + * + * @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->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['provider'] = $selected_user->getOrganization(); + $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 + ]); + } + + /** + * @throws RuntimeError + * @throws SyntaxError + * @throws LoaderError + * @throws Exception + */ + private function sendPasswordResetEmail(XDUser $user): void + { + $rid = $user->generateRID(); + + $subject = sprintf('%s: Password Reset', \xd_utilities\getConfiguration('general', 'title')); + $body = $this->twig->render( + 'twig/emails/password_reset.html.twig', + [ + 'first_name' => $user->getFirstName(), + 'username' => $user->getUsername(), + 'reset_link' => sprintf( + '%spassword_reset.php?rid=%s', + \xd_utilities\getConfigurationUrlBase('general', 'site_address'), + $rid + ), + 'expiration' => strftime('%c %Z', explode('|', $rid)[1]), + 'maintainer_signature' => MailWrapper::getMaintainerSignature(), + ] + ); + + MailWrapper::sendMail([ + 'toAddress' => $user->getEmailAddress(), + 'subject' => $subject, + 'body' => $body + ]); + } + + /** + * @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..524cb82f8e --- /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..d503ba6acd --- /dev/null +++ b/src/Controller/MailController.php @@ -0,0 +1,225 @@ +getUserFromRequest($request); + $operation = $this->getStringParam($request, 'operation', true); + + switch ($operation) { + case 'contact': + return $this->contact($request, $user); + case 'sign_up': + return $this->signUp($request); + default: + throw new BadRequestHttpException('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' => \xd_utilities\getConfiguration('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 = << $message, + 'subject' => '[' . \xd_utilities\getConfiguration('general', 'title') . '] A visitor has signed up', + 'toAddress' => \xd_utilities\getConfiguration('general', 'contact_page_recipient'), + 'fromAddress' => $_POST['email'], + 'fromName' => $_POST['last_name'] . ', ' . $_POST['first_name'] + ]); + $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..c60b390550 --- /dev/null +++ b/src/Controller/MetricExplorerController.php @@ -0,0 +1,997 @@ +getStringParam($request, 'operation', true); + + 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); + } + + return $this->json([ + 'success' => false, + 'message' => 'Unknown Operation provided.' + ]); + } + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('{prefix}/metrics/explorer/queries', requirements: ['prefix' => '.*'], methods: ['GET'])] + public function getQueries(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $action = 'getQueries'; + $payload = [ + 'success' => false, + 'action' => $action + ]; + $statusCode = 401; + + try { + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + if (isset($user) && $user instanceof XDUser) { + $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 (BadRequestException|HttpException|Exception $exception) { + $payload['message'] = $exception->getMessage(); + $statusCode = (get_class($exception) === 'Exception') ? 500 : $exception->getStatusCode(); + } + + return $this->json($payload, $statusCode); + } + + /** + * + * @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 + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $action = 'getQueryById'; + $payload = array( + 'success' => false, + 'action' => $action, + ); + $statusCode = 401; + + try { + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + if (isset($user) && $user instanceof XDUser) { + $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 (BadRequestException|HttpException|Exception $exception) { + $payload['message'] = $exception->getMessage(); + $statusCode = (get_class($exception) === 'Exception') ? 500 : $exception->getStatusCode(); + } + + return $this->json($payload, $statusCode); + } + + /** + * + * @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 { + $data = $request->get('data', null); + if ($data === null) { + throw new BadRequestHttpException('data is a required parameter.'); + } + if ($this->getUser() !== null) { + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + if (isset($user) && $user instanceof XDUser) { + $queries = new \UserStorage($user, self::QUERIES_STORE); + if (!is_string($data)) { + throw new BadRequestHttpException('Invalid value for data. Must be a(n) string.'); + } + $data = is_string($data) ? json_decode($data, true) : $data; + $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 (BadRequestException|HttpException|Exception $exception) { + $payload['message'] = $exception->getMessage(); + if (get_class($exception) === 'Exception') { + $statusCode = 500; + } elseif (method_exists($exception, 'getStatusCode')) { + $statusCode = $exception->getStatusCode(); + } + } + + return $this->json($payload, $statusCode); + } + + /** + * + * @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 { + if ($this->getUser() === null) { + $payload['message'] = self::DEFAULT_ERROR_MESSAGE; + } else { + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + if (isset($user) && $user instanceof XDUser) { + $this->logger->error(sprintf("Updating Query for: %s",$user->getUsername())); + $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 (BadRequestException|HttpException|Exception $exception) { + $payload['message'] = $exception->getMessage(); + if (get_class($exception) === 'Exception') { + $statusCode = 500; + } elseif (method_exists($exception, 'getStatusCode')) { + $statusCode = $exception->getStatusCode(); + } + } + + 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 + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + + $action = 'deleteQueryById'; + $payload = array( + 'success' => false, + 'action' => $action, + 'message' => 'success' + ); + $statusCode = 401; + + try { + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + if (isset($user) and $user instanceof XDUser) { + $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; + $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 (BadRequestException|HttpException|Exception $exception) { + $payload['message'] = $exception->getMessage(); + $statusCode = (get_class($exception) === 'Exception') ? 500 : $exception->getStatusCode(); + } + + return $this->json($payload, $statusCode); + } + + + /** + * @param XDUser $user + * @param array $query + * @return void + * @throws Exception + */ + 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); + } + + /** + * + * @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]); + + $m = new \DataWarehouse\Access\MetricExplorer($_REQUEST); + 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 + ); + } + $this->logger->warning('User retrieved ', [$user->getUserIdentifier()]); + + $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 = \xd_utilities\getConfiguration('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 = []; + // NOTE: this variable is never utilized after being updated. can probably be removed. + $groupByObjects = []; + + $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(); + + $groupByObjects[$query_descriptor_realm . '_' . $groupByName] = [ + 'object' => $group_by_object, + 'permittedStats' => $permittedStatistics + ]; + $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 + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $returnData = []; + + 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 { + $config = []; + foreach ($request->request->all() as $key => $value) { + $config[$key] = $value; + } + + $configParam = $this->getStringParam($request, 'config'); + if (!empty($configParam)) { + $configJson = json_decode($configParam, true); + $config = array_merge($config, $configJson); + } + + $requestedFormat = $this->getStringParam($request, 'format'); + $format = DataWarehouse\ExportBuilder::validateFormat($requestedFormat, 'jsonstore', ['jsonstore']); + $inline = $this->getBooleanParam($request, 'inline', false, true); + $dataSetId = $this->getStringParam($request, 'datasetId', true); + $datapoint = $this->getStringParam($request, 'datapoint', true); + $showContextMenu = $this->getBooleanParam($request, 'showContextMenu', false, false); + $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); + } + + $title = $this->getStringParam($request, 'title'); + + $requestedGlobalFilters = $this->getStringParam($request, 'global_filters'); + + $globalFilters = (object)['data' => [], 'total' => 0]; + if (!empty($requestedGlobalFilters)) { + $globalFiltersDecoded = urldecode($requestedGlobalFilters); + $globalFiltersJson = json_decode($globalFiltersDecoded, true); + $this->logger->warning('Global Filters Decoded', [var_export($globalFiltersDecoded, true)]); + $this->logger->warning('Global FIlters Json', [json_encode($globalFiltersJson)]); + + 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); + + $filterOpts = array('options' => array('default' => null, 'min_range' => 0)); + + $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); + + $query = $privquery->getQueryString(); + + $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..967fcfaa76 --- /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..24a8aaa598 --- /dev/null +++ b/src/Controller/PasswordResetController.php @@ -0,0 +1,63 @@ + '.*'])] +class PasswordResetController extends BaseController +{ + private static $validModes = ['new', 'reset']; + + /** + * + * @param Request $request + * @return Response + * @throws Exception + */ + #[Route('', methods: ['GET'])] + public function index(Request $request): Response + { + $validationCheck = [ + 'status' => INVALID, + 'user_first_name' => 'INVALID', + 'user_id' => INVALID + ]; + + $mode = $this->getStringParam($request, 'mode', false, 'reset'); + $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' => $site_address = \xd_utilities\getConfigurationUrlBase('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..a60ea83a25 --- /dev/null +++ b/src/Controller/PersonController.php @@ -0,0 +1,37 @@ + '.*'])] +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..4767eb2176 --- /dev/null +++ b/src/Controller/ReportBuilderController.php @@ -0,0 +1,703 @@ +getStringParam($request, 'operation'); + + 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); + } + + return $this->json([]); + } + + /** + * + * + * @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 + * @return Response + * @throws Exception + */ + #[Route('/reports/builder/download', methods: ['GET'])] + public function downloadReport(Request $request): 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->error('Saving Report', ['volatile', $insertion_rank, $start_date, $end_date]); + $cached_blob = $start_date . ',' . $end_date . ';' + . $reportManager->generateChartBlob('volatile', $insertion_rank, $start_date, $end_date, $this->logger); + } 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->warning('Generating a Report Image'); + + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + + $userId = null; + try { + $this->logger->warning('Report Image Authenticated'); + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + + $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->warning('Report Image Volatile / chart Pool'); + $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)); + } + + $this->logger->warning('Valid User Request'); + + $reportManager = null; + try { + $this->logger->warning('Instantiating XDREportManager'); + $reportManager = new XDReportManager($user); + } catch (Exception $exception) { + $this->logger->error('Error instantiating Report Manager'); + } + + $this->logger->warning('After Report Manager.'); + + if (!empty($reportManager)) { + $this->logger->warning('Fetching Chart Blob', [$type, $insertionRank]); + $blob = $reportManager->fetchChartBlob($type, $insertionRank, null, $this->logger); + $this->logger->warning('Substringing Blob'); + $image_data_header = substr($blob, 0, 8); + $this->logger->warning('Chart BLob Fetched!'); + + if ($image_data_header != "\x89PNG\x0d\x0a\x1a\x0a") { + throw new Exception($blob); + } + $this->logger->warning('Blob is a png'); + // 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']; + $this->logger->warning('Returning PNG'); + $this->logger->warning('Headers: ', [$headers]); + return new Response($blob, 200, $headers); + } else { + $this->logger->error('Oops, we shouldnt be here.'); + } + + 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()); + + $this->logger->warning('get Report Data Start'); + $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 @@ + '.*'])] +class UserController extends BaseController { /** @@ -27,12 +29,12 @@ class UserControllerProvider extends BaseControllerProvider * * @var array */ - private static $userSettableProperties = array( - 'first_name', - 'last_name', - 'email_address', - 'password', - ); + private static $userSettableProperties = [ + 'first_name' => 'string', + 'last_name' => 'string', + 'email_address' => 'string', + 'password' => 'string', + ]; /** * A mapping of user properties that can come in with a request to @@ -40,85 +42,155 @@ 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 + * + * + * @param Request $request + * @return Response + * @throws Exception */ - public function setupRoutes(Application $app, \Silex\ControllerCollection $controller) + #[Route('', methods: ['POST'])] + #[Route('/controllers/sab_user.php', name: 'list_users_legacy', methods: ['GET'])] + public function listUsers(Request $request): Response { - $root = $this->prefix; + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + + $start = $this->getIntParam($request, 'start', true); + $limit = $this->getIntParam($request, 'limit', true); + $searchMode = $this->getStringParam($request, 'search_mode', true); + $piOnly = $this->getBooleanParam($request, 'pi_only', true); + $nameFilter = $this->getStringParam($request, 'query'); + $userManagement = $this->getBooleanParam($request, 'userManagement'); + + $universityId = null; + $searchMethod = null; + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + if ($user->hasAcl(ROLE_ID_CAMPUS_CHAMPION) && !isset($userManagement)) { + $universityId = Acls::getDescriptorParamValue($user, ROLE_ID_CAMPUS_CHAMPION, 'provider'); + } + + switch ($searchMode) { + case 'formal_name': + $searchMethod = FORMAL_NAME_SEARCH; + break; + case 'username': + $searchMethod = USERNAME_SEARCH; + } + + $dataWarehouse = new XDWarehouse(); + + list($userCount, $users) = $dataWarehouse->enumerateGridUsers( + $searchMethod, + $start, + $limit, + $nameFilter, + $piOnly, + $universityId + ); - $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'); + $entryId = 0; + $userEntries = []; + foreach ($users as $currentUser) { + $entryId++; + + $personName = 'Invalid'; + $personId = -666; + switch ($searchMode) { + case 'formal_name': + $personName = $currentUser['long_name']; + $personId = $currentUser['id']; + break; + case 'username': + $personName = $currentUser['abusername']; + $personId = sprintf('%s;%s', $currentUser['id'], $currentUser['abusername']); + break; + } + $userEntries[] = [ + 'id' => $entryId, + 'person_id' => $personId, + 'person_name' => $personName + ]; + } + + return $this->json([ + 'success' => true, + 'status' => 'success', + 'message' => 'success', + 'total_user_count' => $userCount, + 'users' => $userEntries + ]); } /** - * 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("/current", name: "get_current_user", 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( + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + + if ((!$user instanceof XDUser)) { + return $this->json([ + 'success' => false, + 'message' => 'Internal Error validating User' + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + + return $this->json([ 'success' => true, - 'results' => $this->extractUserData($this->getUserFromRequest($request)), - )); + 'results' => $this->extractUserData($user) + ]); } /** - * 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("/current", name: "update_current_user", methods: ["PATCH"])] + public function updateCurrentUser(Request $request) { - // Ensure that the user is logged in. - $this->authorize($request); + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + + $user = XDUser::getUserByUserName($this->getUser()->getUserIdentifier()); + + if ((!$user instanceof XDUser)) { + return $this->json([ + 'success' => false, + 'message' => 'Internal Error validating User' + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } - // Attempt to update the user's profile with the given information. $this->updateUser( - $this->getUserFromRequest($request), + $user, $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; - - return $app->json(array( + return $this->json([ 'success' => true, - 'message' => 'User profile updated successfully', - )); + 'message' => 'User profile updated successfully' + ]); } /** @@ -126,14 +198,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 - * @throws \Exception + * @return Response + * @throws Exception */ - public function getCurrentAPIToken(Request $request, Application $app) + #[Route('/current/api/token', methods: ['GET'])] + public function getCurrentAPIToken(Request $request): Response { $user = $this->authorize($request); @@ -143,11 +216,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 + ]); } /** @@ -156,12 +228,13 @@ public function getCurrentAPIToken(Request $request, Application $app) * - They just have authenticated to XDMoD via one of the supported methods. * - 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. + * @throws Exception if there is a problem retrieving a database connection. */ - public function createAPIToken(Request $request, Application $app) + #[Route('/current/api/token', methods: ['POST'])] + public function createAPIToken(Request $request): Response { $user = $this->authorize($request); @@ -169,7 +242,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 +255,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 + * @throws Exception */ - public function revokeAPIToken(Request $request, Application $app) + #[Route('/current/api/token', methods: ['DELETE'])] + public function revokeAPIToken(Request $request): Response { $user = $this->authorize($request); @@ -198,7 +272,7 @@ 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.' )); @@ -208,6 +282,7 @@ public function revokeAPIToken(Request $request, Application $app) throw new Exception('Unable to revoke API token.'); } + /** * Extract information from a user object. * @@ -215,6 +290,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 +312,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,14 +337,26 @@ function ($item) { private function extractUserSettableProperties(Request $request) { $requestProperties = array(); - foreach (self::$userSettableProperties as $propertyName) { - $propertyValue = $this->getStringParam($request, $propertyName); - + $this->logger->debug('Getting User Settable Properties'); + foreach (self::$userSettableProperties as $propertyName => $propertyType) { + $propertyValue = $request->get($propertyName); + $this->logger->debug('Checking Property', [$propertyName, $propertyValue, $propertyType]); 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; } + $this->logger->debug('Returning user settable properties', [var_export($requestProperties, true)]); return $requestProperties; } @@ -286,18 +374,19 @@ 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) { + $this->logger->debug('Checking Update Property', [$propertyName, !array_key_exists($propertyName, self::$propertySettingOptions)]); if (!array_key_exists($propertyName, self::$propertySettingOptions)) { continue; } $propertyOptions = self::$propertySettingOptions[$propertyName]; - + $this->logger->debug(sprintf('Calling %s w/ %s', $propertyOptions['setter'], $propertyValue)); $user->{$propertyOptions['setter']}($propertyValue); } - + $this->logger->debug('Saving User!'); // Attempt to save the user's new details. This will throw an exception // if an error occurs. + $this->logger->debug('Updating User', [$user->getUserId(), $user->getUsername(), var_export($updatedProperties, true)]); $user->saveUser(); } @@ -307,7 +396,7 @@ private function updateUser(XDUser $user, array $updatedProperties) * * @param XDUser $user * @return bool true if the user does not already have a valid API token. - * @throws \Exception if there is a problem retrieving a database connection. + * @throws Exception if there is a problem retrieving a database connection. */ private function canCreateToken(XDUser $user) { @@ -334,8 +423,8 @@ private function canCreateToken(XDUser $user) * * @param XDUser $user whose token data should be retrieved. * @return array in the format array('created_on' => createdOn, 'expiration_date' => expirationDate) - * @throws \Exception if there is a problem retrieving a db connection. - * @throws \Exception if there is a problem executing the SELECT statement. + * @throws Exception if there is a problem retrieving a db connection. + * @throws Exception if there is a problem executing the SELECT statement. */ private function getCurrentAPITokenMetaData(XDUser $user) { @@ -349,7 +438,7 @@ private function getCurrentAPITokenMetaData(XDUser $user) $rows = $db->query($query, array(':user_id' => $user->getUserID())); if (count($rows) !== 1) { - throw new \Exception('Invalid token data returned.'); + throw new Exception('Invalid token data returned.'); } return array( @@ -366,9 +455,9 @@ private function getCurrentAPITokenMetaData(XDUser $user) * * @return array in the format ('token' => newToken, 'expiration_date' => tokenExpirationDate) * - * @throws \Exception if unable to retrieve a database connection or if there is a problem generating a random token. - * @throws \Exception if the api_token.expiration_interval configuration value ( in portal_settings.ini ) is not set. - * @throws \Exception if inserting the newly generated token is unsuccessful. i.e. the number of rows inserted is < 1. + * @throws Exception if unable to retrieve a database connection or if there is a problem generating a random token. + * @throws Exception if the api_token.expiration_interval configuration value ( in portal_settings.ini ) is not set. + * @throws Exception if inserting the newly generated token is unsuccessful. i.e. the number of rows inserted is < 1. */ private function createToken(XDUser $user) { @@ -388,7 +477,7 @@ private function createToken(XDUser $user) $createdOn = date_create()->format('Y-m-d H:m:s'); $expirationInterval = \xd_utilities\getConfiguration('api_token', 'expiration_interval'); if (empty($expirationInterval)) { - throw new \Exception('Expiration Interval not provided.'); + throw new Exception('Expiration Interval not provided.'); } $dateInterval = date_interval_create_from_date_string($expirationInterval); $expirationDate = date_add(date_create(), $dateInterval)->format('Y-m-d H:m:s'); @@ -396,19 +485,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) { - throw new \Exception('Unable to create a new API token.'); + 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, ); } @@ -418,8 +507,8 @@ private function createToken(XDUser $user) * * @param XDUser $user whose active token will be revoked. * @return bool true if 1 row was deleted else false. - * @throws \Exception if there was a problem retrieving a database connection. - * @throws \Exception if there was an error while executing the DELETE statement. + * @throws Exception if there was a problem retrieving a database connection. + * @throws Exception if there was an error while executing the DELETE statement. */ private function revokeToken(XDUser $user) { @@ -430,4 +519,5 @@ private function revokeToken(XDUser $user) return $rows === 1; } + } diff --git a/src/Controller/UserInterfaceController.php b/src/Controller/UserInterfaceController.php new file mode 100644 index 0000000000..03cf4758d9 --- /dev/null +++ b/src/Controller/UserInterfaceController.php @@ -0,0 +1,459 @@ +getStringParam($request, 'operation', true); + 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); + } + + throw new NotFoundHttpException(); + } + + /** + * + * @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($_REQUEST['node']) + && substr($_REQUEST['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 55% rename from classes/Rest/Controllers/WarehouseControllerProvider.php rename to src/Controller/WarehouseController.php index 15251230c4..83953e3ef6 100644 --- a/classes/Rest/Controllers/WarehouseControllerProvider.php +++ b/src/Controller/WarehouseController.php @@ -1,70 +1,55 @@ - array( - "infoid" => \DataWarehouse\Query\RawQueryTypes::ACCOUNTING, - "dtype" => "infoid", - "text" => "Accounting data", - "url" => "/rest/v1.0/warehouse/search/jobs/accounting", - "documentation" => "Shows information about the job that was obtained from the resource manager. + [ + 'infoid' => \DataWarehouse\Query\RawQueryTypes::ACCOUNTING, + 'dtype' => 'infoid', + 'text' => 'Accounting data', + 'url' => '/warehouse/search/jobs/accounting', + 'documentation' => 'Shows information about the job that was obtained from the resource manager. This includes timing information such as the start and end time of the job as well as administrative information such as the user that submitted the job and - the account that was charged.", - "type" => "keyvaluedata", - "leaf" => true - ), + the account that was charged.', + 'type' => 'keyvaluedata', + 'leaf' => true + ], \DataWarehouse\Query\RawQueryTypes::BATCH_SCRIPT => - array( - "infoid" => \DataWarehouse\Query\RawQueryTypes::BATCH_SCRIPT, - "dtype" => "infoid", - "text" => "Job script", - "url" => "/rest/v1.0/warehouse/search/jobs/jobscript", - "documentation" => "Shows the job batch script that was passed to the resource manager when the - job was submitted. The script is displayed verbatim.", - "type" => "utf8-text", - "leaf" => true - ), + [ + 'infoid' => \DataWarehouse\Query\RawQueryTypes::BATCH_SCRIPT, + 'dtype' => 'infoid', + 'text' => 'Job script', + 'url' => '/warehouse/search/jobs/jobscript', + 'documentation' => 'Shows the job batch script that was passed to the resource manager when the + job was submitted. The script is displayed verbatim.', + 'type' => 'utf8-text', + 'leaf' => true + ], \DataWarehouse\Query\RawQueryTypes::EXECUTABLE => - array( - "infoid" => \DataWarehouse\Query\RawQueryTypes::EXECUTABLE, - "dtype" => "infoid", - "text" => "Executable information", - "url" => "/rest/v1.0/warehouse/search/jobs/executable", - "documentation" => "Shows information about the processes that were run on the compute nodes during + [ + 'infoid' => \DataWarehouse\Query\RawQueryTypes::EXECUTABLE, + 'dtype' => 'infoid', + 'text' => 'Executable information', + 'url' => '/warehouse/search/jobs/executable', + 'documentation' => 'Shows information about the processes that were run on the compute nodes during the job. This information includes the names of the various processes and may contain information about the linked libraries, loaded modules and process - environment.", - "type" => "nested", - "leaf" => true), + environment.', + 'type' => 'nested', + 'leaf' => true], \DataWarehouse\Query\RawQueryTypes::PEERS => - array( - "infoid" => \DataWarehouse\Query\RawQueryTypes::PEERS, - "dtype" => "infoid", - "text" => "Peers", - 'url' => '/rest/v1.0/warehouse/search/jobs/peers', + [ + 'infoid' => \DataWarehouse\Query\RawQueryTypes::PEERS, + 'dtype' => 'infoid', + 'text' => 'Peers', + 'url' => '/warehouse/search/jobs/peers', 'documentation' => 'Shows the list of other HPC jobs that ran concurrently using the same shared hardware resources.', 'type' => 'ganttchart', - "leaf" => true - ), + 'leaf' => true + ], \DataWarehouse\Query\RawQueryTypes::NORMALIZED_METRICS => - array( - "infoid" => \DataWarehouse\Query\RawQueryTypes::NORMALIZED_METRICS, - "dtype" => "infoid", - "text" => "Summary metrics", - "url" => "/rest/v1.0/warehouse/search/jobs/metrics", - "documentation" => "shows a table with the performance metrics collected during + [ + 'infoid' => \DataWarehouse\Query\RawQueryTypes::NORMALIZED_METRICS, + 'dtype' => 'infoid', + 'text' => 'Summary metrics', + 'url' => '/warehouse/search/jobs/metrics', + 'documentation' => 'shows a table with the performance metrics collected during the job. These are typically average values over the job. The label for each row has a tooltip that describes the metric. The data are grouped into the following categories: @@ -169,178 +131,51 @@ class WarehouseControllerProvider extends BaseControllerProvider
  • Network I/O Statistics: information about the data transmitted and received over the network devices.
  • - ", - "type" => "metrics", - "leaf" => true - ), + ', + 'type' => 'metrics', + 'leaf' => true + ], \DataWarehouse\Query\RawQueryTypes::DETAILED_METRICS => - array( - "infoid" => \DataWarehouse\Query\RawQueryTypes::DETAILED_METRICS, - "dtype" => "infoid", - "text" => "Detailed metrics", - "url" => "/rest/v1.0/warehouse/search/jobs/detailedmetrics", - "documentation" => "shows the data generated by the job summarization software. Please + [ + 'infoid' => \DataWarehouse\Query\RawQueryTypes::DETAILED_METRICS, + 'dtype' => 'infoid', + 'text' => 'Detailed metrics', + 'url' => '/warehouse/search/jobs/detailedmetrics', + 'documentation' => 'shows the data generated by the job summarization software. Please consult the relevant job summarization software documentation for details - about these metrics.", - "type" => "detailedmetrics", - "leaf" => true - ), + about these metrics.', + 'type' => 'detailedmetrics', + 'leaf' => true + ], \DataWarehouse\Query\RawQueryTypes::ANALYTICS => - array( - "infoid" => \DataWarehouse\Query\RawQueryTypes::ANALYTICS, - "dtype" => "infoid", - "text" => "Job analytics", - "url" => "/rest/v1.0/warehouse/search/jobs/analytics", - "documentation" => "Click the help icon on each plot to show the description of the analytic", - "type" => "analytics", - "hidden" => true, - "leaf" => true - ), + [ + 'infoid' => \DataWarehouse\Query\RawQueryTypes::ANALYTICS, + 'dtype' => 'infoid', + 'text' => 'Job analytics', + 'url' => '/warehouse/search/jobs/analytics', + 'documentation' => 'Click the help icon on each plot to show the description of the analytic', + 'type' => 'analytics', + 'hidden' => true, + 'leaf' => true + ], \DataWarehouse\Query\RawQueryTypes::TIMESERIES_METRICS => - array( - "infoid" => \DataWarehouse\Query\RawQueryTypes::TIMESERIES_METRICS, - "dtype" => "infoid", - "text" => "Timeseries", - "leaf" => false - ), + [ + 'infoid' => \DataWarehouse\Query\RawQueryTypes::TIMESERIES_METRICS, + 'dtype' => 'infoid', + 'text' => 'Timeseries', + 'leaf' => false + ], \DataWarehouse\Query\RawQueryTypes::VM_INSTANCE => - array( - "infoid" => \DataWarehouse\Query\RawQueryTypes::VM_INSTANCE, - "dtype" => "infoid", - "text" => "VM State/Events", - "documentation" => "Show the lifecycle of a VM. Green signifies when a VM is active and red signifies when a VM is stopped.", - "url" => "/rest/v1.0/warehouse/search/cloud/vmstate", - "type" => "vmstate", - "leaf" => true - ) - ); - - /** - * 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"); - } + [ + 'infoid' => \DataWarehouse\Query\RawQueryTypes::VM_INSTANCE, + 'dtype' => 'infoid', + 'text' => 'VM State/Events', + 'documentation' => 'Show the lifecycle of a VM. Green signifies when a VM is active and red signifies when a VM is stopped.', + 'url' => '/warehouse/search/cloud/vmstate', + 'type' => 'vmstate', + 'leaf' => true + ] + ]; /** * Retrieves the Search History for the user making the request. @@ -362,16 +197,17 @@ public function setupRoutes(Application $app, ControllerCollection $controller) * total: ... number of records in 'data' ... * } * + * * @param Request $request - * @param Application $app - * @return array in the format array( boolean success, string message) + * @return Response * @throws AccessDeniedException * @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,57 +220,56 @@ 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 + * @param int $id + * @return Response * - * @param Request $request that will be used to complete the operation. - * @param Application $app that will be used to complete the operation. - * @param int $id of the Search History record to be retrieved. - * @return \Symfony\Component\HttpFoundation\JsonResponse - * @throws AccessDeniedException + * @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'; - $user = $this->authorize($request); - $realm = $this->getStringParam($request, 'realm', true); $searchHistory = $this->getUserStore($user, $realm); - $record = $searchHistory->getById($id); if (isset($record)) { foreach ($record['results'] as &$result) { @@ -449,17 +284,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,19 +304,17 @@ public function getHistoryByTitle(Request $request, Application $app, $realm, $t if (!isset($search['dtype'])) { $search['dtype'] = 'recordid'; } - return $app->json( - array( + return $this->json( + [ 'action' => $action, 'success' => true, 'data' => $search - ), - 200 + ] ); - break; } } - throw new NotFoundHttpException(); + throw new NotFoundHttpException(''); } /** @@ -488,19 +322,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. */ - 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']) ) { - throw new BadRequestHttpException( - 'Malformed request. Expected \'data.text\' to be present.' - ); + if ($decoded === null || !isset($decoded['text'])) { + throw new BadRequestHttpException('Malformed request. Expected \'data.text\' to be present.'); } $decoded['text'] = htmlspecialchars($decoded['text'], ENT_COMPAT | ENT_HTML5); @@ -510,39 +341,37 @@ 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 - * @throws BadRequestHttpException + * + * + * @param Request $request + * + * @return Response + * + * @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); + $recordId = $this->getIntParam($request, 'recordid'); $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 . ")" + 'Create request will exceed record storage restrictions ' . + '(record count limited to ' . self::MAX_RECORDS . ')' ); } @@ -551,35 +380,38 @@ public function createHistory(Request $request, Application $app) } - return $app->json( - array( + return $this->json( + [ 'success' => true, 'action' => $action, 'total' => count($created), 'results' => $created - ) + ] ); } /** * 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 - * @throws BadRequestHttpException - * @throws AccessDeniedException + * + * @return Response + * + * @throws BadRequestHttpException|AccessDeniedHttpException|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); $action = 'updateHistory'; + $user = $this->authorize($request); $data = $this->getSearchParams($request); - $realm = $this->getStringParam($request, 'realm', true); $history = $this->getUserStore($user, $realm); @@ -590,30 +422,30 @@ public function updateHistory(Request $request, Application $app, $id) $result['dtype'] = 'recordid'; } - $results = $app->json( - array( + return $this->json( + [ 'success' => true, 'action' => $action, + 'total' => 1, 'results' => $result - ), - 200 + ] ); - - return $results; } /** * 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,12 +455,12 @@ public function deleteHistory(Request $request, Application $app, $id) $history = $this->getUserStore($user, $realm); $deleted = $history->delById($id); - return $app->json( - array( + return $this->json( + [ 'success' => true, 'action' => $action, 'total' => $deleted - ) + ] ); } @@ -636,13 +468,17 @@ 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 - * @throws BadRequestHttpException - * @throws AccessDeniedException + * + * + * @param Request $request + * + * @return Response + * + * @throws BadRequestHttpException|AccessDeniedHttpException|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,11 +489,11 @@ public function deleteAllHistory(Request $request, Application $app) $history = $this->getUserStore($user, $realm); $history->del(); - return $app->json( - array( + return $this->json( + [ 'success' => true, 'action' => $action - ) + ] ); } @@ -665,12 +501,15 @@ 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); @@ -678,88 +517,43 @@ public function searchJobs(Request $request, Application $app) $params = $this->getStringParam($request, 'params', true); $params = json_decode($params, true); - - if($params === null) { - 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 - * @throws BadRequestHttpException - * @throws AccessDeniedException + * @return Response + * @throws BadRequestHttpException|AccessDeniedHttpException|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); + $actionName = 'searchJobsByAction'; - $name = '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); - - return $results; - - } - - /** - * Get the list of resources known to XDMoD and the metadata about them - * - * @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. - */ - public function getResources(Request $request, Application $app) - { - Tokens::authenticate($request); - - $config = \Configuration\XdmodConfiguration::assocArrayFactory('resource_metadata.json', CONFIG_DIR); - - $query_sql = $config['resource_query']; - $params = array(); - $wheres = array(); - - foreach ($config['where_conditions'] as $param => $wherecond) { - $value = $this->getStringParam($request, $param); - if ($value) { - $params[$param] = $value; - array_push($wheres, $wherecond); - } - } - - if (count($wheres) > 0) { - $query_sql .= " WHERE " . implode(" AND ", $wheres); - } - - $db = DB::factory('database'); - $stmt = $db->prepare($query_sql); - $stmt->execute($params); - - $resourceData = array(); - while ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { - $resourceData[$result['resource_name']] = $result; - } - return $app->json(array( - 'success' => true, - 'results' => $resourceData - )); + return $this->processJobSearchByAction($request, $user, $action, $realm, $jobId, $actionName); } /** @@ -767,36 +561,51 @@ 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 { - $user = $this->authorize($request); + /*TODO: verify that unauthorized users should be able to access this endpoint */ + $user = $this->getUser(); + if (null === $user) { + $user = XDUser::getPublicUser(); + } else { + $user = XDUser::getUserByUserName($user->getUserIdentifier()); + } - // Get the realms for the user's active role. + // Get the realms for the query group and 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. * - * @return json object + * + * @param Request $request The request used to make this call. + * + * @return Response + * + * @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 +629,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 +661,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,12 +671,12 @@ public function getAggregateData(Request $request, Application $app) unset($val[$config->group_by . '_short_name']); unset($val[$config->group_by . '_order_id']); } - return $app->json( - array( + return $this->json( + [ 'results' => $results, 'total' => $dataset->getTotalPossibleCount(), 'success' => true - ) + ] ); } @@ -876,29 +685,39 @@ 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); - // Get parameters. - $realmParam = $this->getStringParam($request, 'realm'); + $realm = $this->getStringParam($request, 'realm'); + + /*TODO: verify that this is what the expected exception is here.*/ // Get the dimensions for the query group, realm, and user's active role. - $groupBys = Acls::getQueryDescripters( - $user, - $realmParam - ); + try { + $groupBys = Acls::getQueryDescripters($user, $realm); + } catch (Exception $e) { + return $this->json([ + 'success' => false, + 'message' => $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + $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(), @@ -911,38 +730,42 @@ public function getDimensions(Request $request, Application $app) } } - // Return the dimensions found. - return $app->json(array( + return $this->json([ 'success' => true, - 'results' => $dimensionsToReturn, - )); + 'results' => $dimensionsToReturn + ]); } /** * Get the dimension values available for the user's active role. * - * 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'); - $realmParameter = $this->getStringParam($request, 'realm'); + $realm = $this->getStringParam($request, 'realm'); $realms = null; - if ($realmParameter !== null) { - $realms = preg_split('/,\s*/', trim($realmParameter), null, PREG_SPLIT_NO_EMPTY); + if (null !== $realm) { + $realms = preg_split('/,\s*/', trim($realm), -1, PREG_SPLIT_NO_EMPTY); } // Get the dimension values. @@ -963,33 +786,43 @@ public function getDimensionValues(Request $request, Application $app, $dimensio $dimensionValue['short_name'] = html_entity_decode($dimensionValue['short_name']); } - // Return the found dimension values. - return $app->json(array( + return $this->json([ '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 = getConfiguration('features', 'multiple_service_providers') === 'on'; + } catch (Exception $e) { $multipleProvidersSupported = false; } @@ -1059,80 +892,92 @@ public function getQuickFilters(Request $request, Application $app) } } - // Return the quick filters. - return $app->json(array( + return $this->json([ 'success' => true, - 'results' => array( + 'results' => [ '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); + /*TODO: verify that this endpoint is for authorized users only. */ + $user = $this->authorize($request); + $dimensionName = MetricExplorer::getDimensionName($user, $dimensionId); $success = !empty($dimensionName); $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 ); } /** - * Attempt to retrieve the the name for the provided dimensionId and - * valueId. + * 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); + $user = $this->authorize($request); $valueName = MetricExplorer::getDimensionValueName($user, $dimensionId, $valueId); $success = !empty($valueName); $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,23 +988,28 @@ 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([ 'success' => true, 'results' => array_keys($aggregation_units), - )); + ]); } /** @@ -1167,20 +1017,25 @@ 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 +1044,22 @@ 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 +1068,16 @@ 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 +1101,20 @@ 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 +1125,22 @@ 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 +1151,22 @@ 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 +1175,9 @@ 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,32 +1186,52 @@ 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); $limit = $this->getIntParam($request, 'limit', true); - $searchParameterStr = $this->getStringParam($request, 'params', true); - - $searchParams = json_decode($searchParameterStr, true); + $searchParams = json_decode( + $this->getStringParam($request, 'params', true), + true + ); if ($searchParams === null || !is_array($searchParams)) { - throw new BadRequestHttpException('The params parameter must be a json object'); + throw new BadRequestHttpException('params parameter must be valid JSON'); } $params = array_intersect_key($searchParams, $queryDescripters); @@ -1351,7 +1240,7 @@ public function processJobSearch(Request $request, Application $app, XDUser $use throw new BadRequestHttpException('Invalid search parameters specified in params object'); } else { $QueryClass = "\\DataWarehouse\\Query\\$realm\\RawData"; - $query = new $QueryClass($realm, "day", $startDate, $endDate, null, "", array()); + $query = new $QueryClass($realm, 'day', $startDate, $endDate, null, '', []); $allRoles = $user->getAllRoles(); $query->setMultipleRoleParameters($allRoles, $user); @@ -1363,25 +1252,25 @@ public function processJobSearch(Request $request, Application $app, XDUser $use $dataSet = new \DataWarehouse\Data\SimpleDataset($query); $raw = $dataSet->getResults($limit, $offset); - $data = array(); + $data = []; foreach ($raw as $row) { $resource = $row['resource']; $localJobId = $row['local_job_id']; $row['text'] = "$resource-$localJobId"; $row['dtype'] = 'jobid'; - array_push($data, $row); + $data[] = $row; } $total = $dataSet->getTotalPossibleCount(); - $results = $app->json( - array( + $results = $this->json( + [ 'success' => true, 'action' => $action, 'results' => $data, 'totalCount' => $total - ) + ] ); if ($total === 0) { @@ -1390,18 +1279,18 @@ public function processJobSearch(Request $request, Application $app, XDUser $use // need to rerun the query without the role params to see if any results come back. // note the data for the priviledged query is not returned to the user. - $privQuery = new $QueryClass("day", $startDate, $endDate, null, "", array()); + $privQuery = new $QueryClass('day', $startDate, $endDate, null, '', []); $privQuery->setRoleParameters($params); $privDataSet = new \DataWarehouse\Data\SimpleDataset($privQuery, 1, 0); $privResults = $privDataSet->getResults(); if (count($privResults) != 0) { - $results = $app->json( - array( + $results = $this->json( + [ 'success' => false, 'action' => $action, 'message' => 'Unable to complete the requested operation. Access Denied.' - ), + ], 401 ); } @@ -1413,53 +1302,75 @@ 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( - array( + $results = $this->json( + [ 'success' => false, 'action' => $actionName, 'message' => "Unable to process the requested operation. Unsupported action $action." - ), + ], 400 ); break; @@ -1469,18 +1380,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) + private function getJobPeers(XDUser $user, string $realm, $jobId, int $start, int $limit): Response { $jobdata = $this->getJobDataSet($user, $realm, $jobId, 'internal'); if (!$jobdata->hasResults()) { @@ -1516,14 +1425,14 @@ protected function getJobPeers(Application $app, XDUser $user, $realm, $jobId, $ 'ref' => array( 'realm' => $realm, 'jobid' => $jobId, - "text" => $thisjob['resource'] . '-' . $thisjob['local_job_id'] + 'text' => $thisjob['resource'] . '-' . $thisjob['local_job_id'] ) ) ); $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,34 +1448,26 @@ protected function getJobPeers(Application $app, XDUser $user, $realm, $jobId, $ } } - return $app->json(array( + return $this->json([ 'success' => true, - 'data' => array($result), + 'data' => [$result], 'total' => count($dataset->getResults()) - )); + ]); } /** - * @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( - array( - 'data' => $dataSet->export(), - 'success' => true - ), - 200 - ); + return $this->json(['data' => $dataSet->export(), 'success' => true]); } /** @@ -1574,13 +1475,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 AccessDeniedException(); } $QueryClass = "\\DataWarehouse\\Query\\$realm\\JobDataset"; @@ -1590,13 +1491,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 AccessDeniedException(); } } return $dataSet; @@ -1605,16 +1506,15 @@ 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,268 +1522,270 @@ 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) )); + return [['key' => '.', 'value' => '', 'expanded' => true, 'children' => $this->atosRecurse($values)]]; } - 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) ); + $result = []; + foreach ($values as $key => $value) { + if (is_array($value)) { + if (count($value) > 0) { + $result[] = [ + 'key' => "$key", + 'value' => '', + 'expanded' => true, + 'children' => $this->atosRecurse($value) + ]; } } else { - $result[] = array("key" => "$key", "value" => $value, "leaf" => true); + $result[] = ['key' => "$key", 'value' => $value, 'leaf' => true]; } } 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 - * @throws BadRequestHttpException - * @throws Exception + * @return Response + * @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"); } - $infoclass = "\\DataWarehouse\\Query\\$realm\\JobMetadata"; - $info = new $infoclass(); + $infoClass = "\\DataWarehouse\\Query\\$realm\\JobMetadata"; + $info = new $infoClass(); - $result = array(); + $result = []; foreach ($info->getJobTimeseriesMetricNodeMeta($user, $jobId, $tsId, $nodeId) as $cpu) { - $cpu['url'] = "/rest/v0.1/warehouse/search/jobs/timeseries"; - $cpu['type'] = "timeseries"; - $cpu['dtype'] = "cpuid"; + $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(['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"); } - $infoclass = "\\DataWarehouse\\Query\\$realm\\JobMetadata"; - $info = new $infoclass(); + $infoClass = "\\DataWarehouse\\Query\\$realm\\JobMetadata"; + $info = new $infoClass(); - $result = array(); + $result = []; foreach ($info->getJobTimeseriesMetricMeta($user, $jobId, $tsId) as $node) { - $node['url'] = "/rest/v0.1/warehouse/search/jobs/timeseries"; - $node['type'] = "timeseries"; - $node['dtype'] = "nodeid"; + $node['url'] = '/warehouse/search/jobs/timeseries'; + $node['type'] = 'timeseries'; + $node['dtype'] = 'node'; $result[] = $node; } - return $app->json(array("success" => true, "results" => $result)); + return $this->json(['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(); + case '' . \DataWarehouse\Query\RawQueryTypes::VM_INSTANCE: + $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['type'] = "timeseries"; - $tsid['dtype'] = "tsid"; + $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(['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['type'] = "timeseries"; - $tsid['dtype'] = "tsid"; + $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(['success' => true, 'results' => $result]); default: - throw new BadRequestHttpException("Node is a leaf"); + 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( - array( + return $this->json( + [ 'success' => true, 'action' => $action, 'results' => array_values($data) - ) + ] ); } /** - * @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(); - $results = array(); + + $results = []; foreach ($output as $item) { - $results[] = array( + $results[] = [ 'text' => $item['text'], 'dtype' => 'recordid', 'recordid' => $item['recordid'], 'searchterms' => $item['searchterms'] - ); + ]; } - return $app->json( - array( + return $this->json( + [ 'success' => true, 'action' => $action, 'results' => $results, 'total' => count($results) - ) + ] ); } /** - * @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(); + $results = []; - 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( + $results[] = [ 'dtype' => 'realm', - 'realm' => $realmconfig['name'], - 'text' => $realmconfig['display'] - ); + 'realm' => $realmConfig['name'], + 'text' => $realmConfig['display'] + ]; } } - return $app->json( - array( + return $this->json( + [ 'success' => true, 'action' => $action, 'results' => $results - ) + ] ); } - private function encodeFloatArray(array $in) + /** + * @param array $in + * @return array + */ + private function encodeFloatArray(array $in): array { - $out = array(); + $out = []; foreach ($in as $key => $value) { if (is_float($value) && is_nan($value)) { $out[$key] = 'NaN'; @@ -1894,36 +1796,60 @@ 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(); + $queryClass = "\\DataWarehouse\\Query\\$realm\\JobMetadata"; + $query = new $queryClass(); - $jobsummary = $query->getJobSummary($user, $jobId); + $jobSummary = $query->getJobSummary($user, $jobId); - $result = array(); + $result = []; // Really this should be a recursive function! - foreach ($jobsummary as $key => $val) { + foreach ($jobSummary as $key => $val) { $name = "$key"; if (is_array($val)) { if (array_key_exists('avg', $val) && !is_array($val['avg'])) { - $result[] = array_merge(array("name" => $name, "leaf" => true), $this->encodeFloatArray($val)); + $result[] = array_merge( + [ + 'name' => $name, + 'leaf' => true + ], + $this->encodeFloatArray($val) + ); } else { - $l1data = array("name" => $name, "avg" => "", "expanded" => "true", "children" => array()); - foreach ($val as $subkey => $subval) { + $l1data = ['name' => $name, 'avg' => '', 'expanded' => 'true', 'children' => []]; + foreach ($val as $subkey => $subVal) { $subName = "$subkey"; - if (is_array($subval)) { - if (array_key_exists('avg', $subval) && !is_array($subval['avg'])) { - $l1data['children'][] = array_merge(array("name" => $subName, "leaf" => true), $this->encodeFloatArray($subval)); + if (is_array($subVal)) { + if (array_key_exists('avg', $subVal) && !is_array($subVal['avg'])) { + $l1data['children'][] = array_merge( + [ + 'name' => $subName, + 'leaf' => true + ], + $this->encodeFloatArray($subVal) + ); } else { - $l2data = array("name" => $subName, "avg" => "", "expanded" => "true", "children" => array()); - - foreach ($subval as $subsubkey => $subsubval) { - $subSubName = "$subsubkey"; - if (is_array($subsubval)) { - if (array_key_exists('avg', $subsubval) && !is_array($subsubval['avg'])) { - $l2data['children'][] = array_merge(array("name" => $subSubName, "leaf" => true), $this->encodeFloatArray($subsubval)); + $l2data = ['name' => $subName, 'avg' => '', 'expanded' => 'true', 'children' => []]; + + foreach ($subVal as $subSubKey => $subSubVal) { + $subSubName = "$subSubKey"; + if (is_array($subSubVal)) { + if (array_key_exists('avg', $subSubVal) && !is_array($subSubVal['avg'])) { + $l2data['children'][] = array_merge( + [ + 'name' => $subSubName, + 'leaf' => true + ], + $this->encodeFloatArray($subSubVal) + ); } } } @@ -1941,41 +1867,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; + $columns = ['Time']; + $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++) { - $outline = array(); + for ($i = 0; $i < $numberOfDataPoints; $i++) { + $outline = []; 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 +1923,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,29 +1937,57 @@ 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( 'timezone' => $data['schema']['timezone'] ); - $chartImage = \xd_charting\exportChart($chartConfig, $settings['width'], $settings['height'], $settings['scale'], $type, $globalConfig, $settings['fileMetadata']); + $chartImage = \xd_charting\exportChart( + $chartConfig, + $settings['width'], + $settings['height'], + $settings['scale'], + $type, + $globalConfig, + $settings['fileMetadata'] + ); $chartFilename = $settings['fileMetadata']['title'] . '.' . $type; $mimeOverride = $type == 'svg' ? 'image/svg+xml' : null; 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(); - $results = $info->getJobTimeseriesData($user, $jobId, $tsId, $nodeId, $cpuId); + $infoClass = "\\DataWarehouse\\Query\\$realm\\JobMetadata"; + $info = new $infoClass(); + + $results = $info->getJobTimeseriesData($user, $jobId, $tsId, $nodeId, $cpuId, $this->logger); if (count($results) === 0) { throw new NotFoundHttpException('The requested resource does not exist'); @@ -2041,26 +1995,28 @@ private function getJobTimeSeriesData(Application $app, Request $request, \XDUse $format = $this->getStringParam($request, 'format', false, 'json'); - if (!in_array($format, array('json', 'png', 'svg', 'pdf', 'csv'))) { + if (!in_array($format, ['json', 'png', 'svg', 'pdf', 'csv'])) { throw new BadRequestHttpException('Unsupported format type.'); } + $subject = $results['schema']['source'] ?? ''; + $title = $results['schema']['description'] ?? ''; switch ($format) { case 'png': case 'pdf': case 'svg': - $exportConfig = array( + $exportConfig = [ 'width' => $this->getIntParam($request, 'width', false, 916), '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, - 'fileMetadata' => array( + 'show_title' => $this->getStringParam($request, 'show_title', false, 'y') === 'y', + 'fileMetadata' => [ 'author' => $user->getFormalName(), - 'subject' => 'Timeseries data for ' . $results['schema']['source'], - 'title' => $results['schema']['description'] - ) - ); + 'subject' => 'Timeseries data for ' . $subject, + 'title' => $title + ] + ]; $response = $this->chartImageResponse($results, $format, $exportConfig); break; case 'csv': @@ -2068,7 +2024,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(['success' => true, 'data' => [$results]]); break; } @@ -2081,132 +2037,160 @@ 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 + * @param array $searchParams + * @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; - } - - if (isset($searchparams['jobref']) && is_numeric($searchparams['jobref'])) { - $params = array( - 'primary_key' => $searchparams['jobref'] - ); - } elseif (isset($searchparams['resource_id']) && isset($searchparams['local_job_id'])) { - $params = array( - 'resource_id' => $searchparams['resource_id'], - 'job_identifier' => $searchparams['local_job_id'] - ); + throw new AccessDeniedException(); + } + + if (isset($searchParams['jobref']) && is_numeric($searchParams['jobref'])) { + $params = [ + 'primary_key' => $searchParams['jobref'] + ]; + } elseif (isset($searchParams['resource_id']) && isset($searchParams['local_job_id'])) { + $params = [ + 'resource_id' => $searchParams['resource_id'], + 'job_identifier' => $searchParams['local_job_id'] + ]; } else { throw new BadRequestHttpException('invalid search parameters'); } $QueryClass = "\\DataWarehouse\\Query\\$realm\\JobDataset"; - $query = new $QueryClass($params, "brief"); + $query = new $QueryClass($params, 'brief'); $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['text'] = $result['resource'] . '-' . $result['local_job_id']; $result['dtype'] = 'jobid'; - array_push($results, $result); + $results[] = $result; } if (!$dataSet->hasResults()) { - $privilegedQuery = new $QueryClass($params, "brief"); + $privilegedQuery = new $QueryClass($params, 'brief'); $privilegedResults = $privilegedQuery->execute(1); if ($privilegedResults['count'] != 0) { - throw new \DataWarehouse\Query\Exceptions\AccessDeniedException(); + throw new AccessDeniedHttpException(); } } - return $app->json( - array( + return $this->json( + [ '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([ + 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. - * - * 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. - * - * 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 - * indicating the size of the chunk data in octets, followed by `\\r\\n`, - * followed by the chunk data, followed by another `\\r\\n`. The first - * chunk contains an array that contains the `display` property of each - * obtained field. Each subsequent chunk contains an array that contains - * the obtained field values for the next row of raw data. The final chunk - * is of length zero to indicate the end of the stream. + * 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. + * + * 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 JSON text sequence. The first line + * will be an array containing the `display` property of each obtained + * field. Subsequent lines will be arrays containing the obtained field + * values for each record. + * + * * * @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 - * key is provided; if the end date is - * before the start date; or if the offset - * is negative. + * not included; if an invalid start date, + * end date, realm, field alias, or filter + * key is provided; if the end date is + * 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) { + $this->logger->error('Unable to authenticate user by token'); + return $this->json(buildError(new Exception('No token provided.')), 401, [ + 'WWW-Authenticate' => 'Bearer' + ]); + } + try { + $params = $this->validateRawDataParams($request, $user); + } catch (HttpException $e) { + $this->logger->error('Unable to validate parameters'); + return $this->json(buildError($e), $e->getStatusCode()); + } + $realmManager = new RealmManager(); $queryClass = $realmManager->getRawDataQueryClass($params['realm']); $logger = $this->getRawDataLogger(); + $this->logger->debug('Have everything, beginning to stream!'); $streamCallback = function () use ( $user, $params, $queryClass, $logger ) { + $logger->debug('Streaming Starting!'); $reachedOffset = false; $i = 1; $offset = $params['offset']; // Jobs realm has a performance improvement by querying one day at // a time. if ('Jobs' === $params['realm']) { + $logger->debug('Streaming Jobs realm Data'); $currentDate = $params['start_date']; while ($currentDate <= $params['end_date']) { - self::echoRawData( + $this->echoRawData( $queryClass, $currentDate, $currentDate, @@ -2225,9 +2209,10 @@ public function getRawData(Request $request, Application $app) ); } } else { + $logger->debug('Streaming other realms'); // 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 +2227,71 @@ 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()). + * Specifically for the Data Analytics Framework * * @param Request $request + * @return Response + */ + #[Route('/warehouse/resources', methods: ['GET'])] + #[Route('{prefix}/warehouse/resources', requirements: ['prefix' => '.*'], methods: ['GET'])] + public function getResources(Request $request): Response + { + $this->tokenHelper->authenticate($request); + + $config = \Configuration\XdmodConfiguration::assocArrayFactory('resource_metadata.json', CONFIG_DIR); + + $query_sql = $config['resource_query']; + $params = array(); + $wheres = array(); + + foreach ($config['where_conditions'] as $param => $wherecond) { + $value = $this->getStringParam($request, $param); + if ($value) { + $params[$param] = $value; + array_push($wheres, $wherecond); + } + } + + if (count($wheres) > 0) { + $query_sql .= " WHERE " . implode(" AND ", $wheres); + } + + $db = DB::factory('database'); + $stmt = $db->prepare($query_sql); + $stmt->execute($params); + + $resourceData = array(); + while ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $resourceData[$result['resource_name']] = $result; + } + return $this->json(array( + 'success' => true, + 'results' => $resourceData + )); + } + + /** + * Validate the parameters of the request from the given user to the raw + * 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)) { @@ -2282,6 +2308,7 @@ private function validateRawDataParams($request, $user) 'The requested realm is not configured to provide raw data.' ); } + $queryDescripters = Acls::getQueryDescripters($user, $params['realm']); if (empty($queryDescripters)) { throw new AccessDeniedException( @@ -2296,7 +2323,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 +2331,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 +2347,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 +2357,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 +2389,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 +2435,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 +2466,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 +2489,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 +2501,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 +2513,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); @@ -2505,20 +2532,20 @@ private function getRawDataFieldsArray($request) /** * Validate the optional `filters` parameter of the given request to the - * raw data endpoint (@see getRawData()), e.g., the parameter - * `filters[foo]=bar,baz` results in `['foo' => ['bar', 'baz']]`. - * - * @param Request $request + * raw data endpoint (@param Request $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 +2567,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 array $params containing a `filters` key whose value is an + * @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, @@ -2569,31 +2595,103 @@ private static function setRawDataQueryFilters($query, $params) /** * Validate a specific filter from the `filters` parameter of a request to - * the raw data endpoint (@see getRawData()), and return the parsed array - * 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 + * the raw data endpoint (@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 . '\'.' - ); + throw new BadRequestHttpException('Invalid filter key \'' . $filterKey . '\'.', null); + } + 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); } - $filterValuesArray = explode(',', $filterValuesStr); - return $filterValuesArray; + + $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(getConfiguration( + 'datawarehouse', + 'rest_raw_row_limit' + )); } } diff --git a/classes/Rest/Controllers/WarehouseExportControllerProvider.php b/src/Controller/WarehouseExportController.php similarity index 57% rename from classes/Rest/Controllers/WarehouseExportControllerProvider.php rename to src/Controller/WarehouseExportController.php index d93f2510ed..4c8e7a7cfd 100644 --- a/classes/Rest/Controllers/WarehouseExportControllerProvider.php +++ b/src/Controller/WarehouseExportController.php @@ -1,103 +1,76 @@ '.*'])] +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; /** - * @var LoggerInterface + * @var QueryHandler */ - private $logger; + private $queryHandler; - public function __construct(array $params = []) + /** + * @throws Exception if unable to instantiate the logger. + */ + public function __construct(LoggerInterface $logger, Environment $twig, Tokens $tokenHelper) { - parent::__construct($params); - $this->logger = Log::factory( - 'data-warehouse-export-rest', - [ - 'console' => false, - 'file' => false, - 'mail' => false - ] - ); + parent::__construct($logger, $twig, $tokenHelper); + $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 } @@ -106,7 +79,6 @@ public function getRealms(Request $request, Application $app) $user = $this->authorize($request); } - $config = RawStatisticsConfiguration::factory(); $realms = array_map( @@ -121,7 +93,7 @@ function ($realm) use ($config) { $this->realmManager->getRealmsForUser($user) ); - return $app->json( + return $this->json( [ 'success' => true, 'data' => array_values($realms), @@ -131,18 +103,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,17 +122,19 @@ 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 - * @throws BadRequestHttpException + * @return Response + * @throws Exception if user is not authorized to access this route. */ - public function createRequest(Request $request, Application $app) + #[Route('/request', methods: ['POST'])] + public function createRequest(Request $request): Response { + $this->logger->debug('Creating Request'); $user = $this->authorize($request); + + $this->logger->debug('User is Authenticated'); + $realm = $this->getStringParam($request, 'realm', true); $realms = array_map( @@ -172,50 +144,65 @@ function ($realm) { $this->realmManager->getRealmsForUser($user) ); if (!in_array($realm, $realms)) { + $this->logger->debug('Invalid Realm'); throw new BadRequestHttpException('Invalid realm'); } + $this->logger->debug('Realm is valid'); $startDate = $this->getDateFromISO8601Param($request, 'start_date', true); $endDate = $this->getDateFromISO8601Param($request, 'end_date', true); $now = new DateTime(); if ($startDate > $now) { + $this->logger->debug('Start Date is invalid'); throw new BadRequestHttpException('Start date cannot be in the future'); } + $this->logger->debug('Start Date is valid.'); + if ($endDate > $now) { + $this->logger->debug('End Date is invalid'); throw new BadRequestHttpException('End date cannot be in the future'); } + $this->logger->debug('End Date is valid'); + $interval = $startDate->diff($endDate); if ($interval === false) { + $this->logger->debug('Interval is Invalid'); throw new BadRequestHttpException('Failed to calculate date interval'); } + $this->logger->debug('Interval is valid'); if ($interval->invert === 1) { + $this->logger->debug('Interval is invalid'); throw new BadRequestHttpException('Start date must be before end date'); } $format = strtoupper($this->getStringParam($request, 'format', true)); if (!in_array($format, ['CSV', 'JSON'])) { + $this->logger->debug('Format is invalid'); throw new BadRequestHttpException('format must be CSV or JSON'); } try { + $this->logger->debug('Creating Export Request'); $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()); + $this->logger->debug('Failed to create export request'); + throw new BadRequestHttpException('Failed to create export request'); } - return $app->json([ + $this->logger->debug('Created Export Request'); + return $this->json([ 'success' => true, 'message' => 'Created export request', 'data' => [['id' => $id]], @@ -224,23 +211,26 @@ 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 + * @return Response + * @throws Exception if the user is not authorized for this route. + * @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 AccessDeniedHttpException if the file for the request identified by the provided id is not readable. */ - public function getExportedDataFile(Request $request, Application $app, $id) + #[Route('/download/{id}', requirements: ["id" => "\d+"], methods: ['GET'])] + public function getExportedDataFile(Request $request, int $id): Response { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $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; } @@ -269,22 +259,12 @@ function ($request) use ($id) { throw new AccessDeniedHttpException('Exported data is not readable'); } - $this->logger->info( - '', - [ - 'module' => self::LOG_MODULE, - 'message' => 'Sending data warehouse export file', - 'event' => 'DOWNLOAD', - 'id' => $id, - 'Users.id' => $user->getUserId() - ] - ); + $this->logger->info('Sending data warehouse export file'); if ($request['downloaded_datetime'] === null) { $this->queryHandler->updateDownloadedDatetime($request['id']); } - - return $app->sendFile( + return new BinaryFileResponse( $file, 200, [ @@ -298,60 +278,30 @@ 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 - * @throws NotFoundHttpException - */ - public function deleteRequest(Request $request, Application $app, $id) - { - $user = $this->authorize($request); - $count = $this->queryHandler->deleteRequest($id, $user->getUserId()); - - if ($count === 0) { - throw new NotFoundHttpException('Export request not found'); - } - - $this->logger->info('', [ - 'module' => self::LOG_MODULE, - 'message' => 'Deleted data warehouse export request', - 'event' => 'DELETE_BY_USER', - 'id' => $id, - 'Users.id' => $user->getUserId() - ]); - - return $app->json([ - 'success' => true, - 'message' => 'Deleted export request', - 'data' => [['id' => $id]], - 'total' => 1 - ]); - } - - /** - * Delete multiple requests. - * - * The request body content must be a JSON encoded array of request IDs. + * @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. * - * @param Request $request - * @param Application $app - * @return \Symfony\Component\HttpFoundation\JsonResponse - * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException - * @throws NotFoundHttpException */ - public function deleteRequests(Request $request, Application $app) + #[Route('/requests', methods: ['DELETE'])] + public function deleteRequests(Request $request): Response { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + $user = $this->authorize($request); $requestIds = []; try { - $requestIds = @json_decode($request->getContent()); - + $this->logger->debug(var_export($request->request->all(), true)); + $requestIds = json_decode($request->get('ids')); + $this->logger->debug(sprintf('Request ids: %s', var_export($requestIds, true))); if ($requestIds === null) { throw new Exception('Failed to decode JSON'); } @@ -360,15 +310,19 @@ public function deleteRequests(Request $request, Application $app) throw new Exception('Export request IDs must be in an array'); } - foreach ($requestIds as $id) { - if (!is_int($id)) { - throw new Exception('Export request IDs must integers'); - } + try { + $requestIds = array_map( + function ($value) { + return is_int($value) ? $value : intval($value); + }, + $requestIds + ); + } catch (Exception $e) { + throw new Exception('Export request IDs must integers'); } + } catch (Exception $e) { - throw new BadRequestHttpException( - 'Malformed HTTP request content: ' . $e->getMessage() - ); + return $this->json(buildError('Malformed HTTP request content: ' . $e->getMessage())); } try { @@ -380,16 +334,8 @@ public function deleteRequests(Request $request, Application $app) if ($count === 0) { throw new NotFoundHttpException('Export request not found'); } - $this->logger->info( - '', - [ - 'module' => self::LOG_MODULE, - 'message' => 'Deleted data warehouse export request', - 'event' => 'DELETE_BY_USER', - 'id' => $id, - 'Users.id' => $user->getUserId() - ] - ); + + $this->logger->info('Deleted data warehouse export request'); } $dbh->commit(); @@ -398,10 +344,10 @@ public function deleteRequests(Request $request, Application $app) throw $e; } catch (Exception $e) { $dbh->rollBack(); - throw new BadRequestHttpException('Failed to delete export requests'); + throw new HttpException(500, 'Failed to delete export requests'); } - return $app->json([ + return $this->json([ 'success' => true, 'message' => 'Deleted export requests', 'data' => array_map( @@ -413,4 +359,33 @@ function ($id) { 'total' => count($requestIds) ]); } + + /** + * + * @param Request $request + * @param string $id + * @return Response + * @throws Exception + */ + #[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()); + + if ($count === 0) { + throw new NotFoundHttpException('Export request not found'); + } + + $this->logger->info('Deleted data warehouse export request'); + + return $this->json([ + 'success' => true, + 'message' => 'Deleted export request', + 'data' => [['id' => $id]], + 'total' => 1 + ]); + } + } 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/Errors/ErrorController.php b/src/Errors/ErrorController.php new file mode 100644 index 0000000000..c905f6a253 --- /dev/null +++ b/src/Errors/ErrorController.php @@ -0,0 +1,51 @@ +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('Error Controller Erroring!'); + $headers = []; + if (method_exists($exception, 'getHeaders')) { + $headers = $exception->getHeaders(); + } + + $message = $exception->getMessage(); + $userPos = strpos($message, 'User'); + $alreadyExistsPos = strpos($message, 'already exists'); + if ($userPos && $alreadyExistsPos) { + return new RedirectResponse('/'); + } + + return new JsonResponse(buildError($exception), 200, $headers); + } + +} 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/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..9209ce89fc --- /dev/null +++ b/src/Security/Authenticators/SimpleSamlPhpAuthenticator.php @@ -0,0 +1,174 @@ +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); + } + } + } + + + public function supports(Request $request): ?bool + { + $referer = $request->headers->get('referer'); + $this->logger->info('Checking if Authenticator supports request', [$referer]); + return $referer === $this->parameters->get('sso')['login_link']; + } + + 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 + ) + ); + } + } + + 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]); + } + + 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/env.template b/templates/env.template new file mode 100644 index 0000000000..1551b4f615 --- /dev/null +++ b/templates/env.template @@ -0,0 +1,9 @@ +# XDMoD related env variables # +XDMOD_LOG_DIR=/var/log/xdmod + +# Symfony related env variables # +DATABASE_URL= +GOOGLE_RECAPTCHA_SITE_KEY= +GOOGLE_RECAPTCHA_SECRET= +APP_SECRET=[:app_secret:] +APP_ENV=prod 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..741292c19e --- /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 }}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 \ No newline at end of file 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..2bd7508532 --- /dev/null +++ b/templates/twig/index.html.twig @@ -0,0 +1,397 @@ + + + + + {{ 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 %} + {# From gaq/xdmod.php #} + + {% 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..5cceaced62 --- /dev/null +++ b/templates/twig/internal_dashboard.html.twig @@ -0,0 +1,200 @@ + + + + + XDMoD Internal Dashboard + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% 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..f2c02792d0 --- /dev/null +++ b/templates/twig/internal_dashboard_login.html.twig @@ -0,0 +1,135 @@ + + + + + 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..c64d1c8fce --- /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/samlSetup.sh b/tests/ci/samlSetup.sh index 08e6bcc7fc..0080e82dc3 100755 --- a/tests/ci/samlSetup.sh +++ b/tests/ci/samlSetup.sh @@ -9,229 +9,317 @@ 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 <% %" /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 + + 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 < "$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', - ), - ), -); + /** + * SAML Attribute Metadata + */ + var metadata = [{ + id: "orgId", + optional: 'true', + displayName: 'Organization', + description: 'Organization the user Belongs to', + multiValue: false + }, { + id: "fieldOfScience", + optional:'true', + displayName: 'Field Of Science', + description: 'Field of Science the user works in', + multiValue: false + }, { + id: "itname", + optional:'false', + displayName: 'Organization Username', + description: 'The system user', + multiValue: false + }, { + id: 'system_username', + optional: 'true', + displayName: 'System Username', + description: 'Username of account used to run jobs.', + multiValue: false + }, { + id: "firstName", + optional: false, + displayName: 'First Name', + description: 'The given name of the user', + multiValue: false + }, { + id: "lastName", + optional: false, + displayName: 'Last Name', + description: 'The surname of the user', + multiValue: false + }, { + id: "displayName", + optional: true, + displayName: 'Display Name', + description: 'The display name of the user', + multiValue: false + }, { + id: "email", + optional: false, + displayName: 'E-Mail Address', + description: 'The e-mail address of the user', + multiValue: false + },{ + id: "mobilePhone", + optional: true, + displayName: 'Mobile Phone', + description: 'The mobile phone of the user', + multiValue: false + }, { + id: "groups", + optional: true, + displayName: 'Groups', + description: 'Group memberships of the user', + multiValue: true + }, { + id: "userType", + optional: true, + displayName: 'User Type', + description: 'The type of user', + options: ['Admin', 'Editor', 'Commenter'] + }]; + + module.exports = { + user: profile, + metadata: metadata + } EOF + log "setup" "Configuring SimpleSamlPHP" + # Configure SimplesamlPHP, stops / starts httpd as appropriate + configureSimplesamlPHP; + + AUD_URL=https://$HOSTNAME/xdmod-sp + + # The ACS url is the only one that needs the port specified. + if [ -n "$PORT" ]; then + HOSTNAME="${HOSTNAME}:${PORT}" + fi + + log "setup" "Spinning up for $HOSTNAME" + ACS_URL=https://$HOSTNAME/simplesaml/module.php/saml/sp/saml2-acs.php/xdmod-sp + + + log "setup" "Starting local IDP" + node app.js --acs "$ACS_URL" --aud "$AUD_URL" --httpsPrivateKey idp-private-key.pem --httpsCert idp-public-cert.pem --https true > /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/ci/scripts/xdmod-setup-start.tcl b/tests/ci/scripts/xdmod-setup-start.tcl index a7a7dda4c9..8bcfdba4e4 100644 --- a/tests/ci/scripts/xdmod-setup-start.tcl +++ b/tests/ci/scripts/xdmod-setup-start.tcl @@ -22,6 +22,8 @@ provideInput {Center Logo Path:} {} provideInput {Enable Dashboard Tab*} {off} confirmFileWrite yes enterToContinue +confirmFileWrite yes +enterToContinue selectMenuOption 2 answerQuestion {DB Hostname or IP} localhost 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/integration/lib/BaseTest.php b/tests/integration/lib/BaseTest.php index 2258ecbe98..a7420c1067 100644 --- a/tests/integration/lib/BaseTest.php +++ b/tests/integration/lib/BaseTest.php @@ -891,4 +891,14 @@ private static function truncateStr($str, $numChars) : $str ); } + + protected function log($message) + { + if (getenv('TEST_VERBOSE') === '1') { + echo "\n*****************************\n"; + echo "$message\n"; + } + } + + } diff --git a/tests/integration/lib/Controllers/BaseUserAdminTest.php b/tests/integration/lib/Controllers/BaseUserAdminTest.php index a669ce335a..cdb6b86f70 100644 --- a/tests/integration/lib/Controllers/BaseUserAdminTest.php +++ b/tests/integration/lib/Controllers/BaseUserAdminTest.php @@ -51,7 +51,7 @@ abstract class BaseUserAdminTest extends BaseTest */ protected $peopleHelper; - protected function setup(): void + protected function setUp(): void { $this->helper = new XdmodTestHelper(); $this->peopleHelper = new PeopleHelper(); @@ -90,7 +90,7 @@ protected static function removeUser($userId, $username) { $helper = new XdmodTestHelper(); - $helper->authenticateDashboard('mgr'); + $helper->authenticate('mgr'); $data = array( 'operation' => 'delete_user', 'uid' => $userId @@ -158,7 +158,7 @@ protected static function removeUser($userId, $username) } } - $helper->logoutDashboard(); + $helper->logout(); } /** @@ -173,7 +173,7 @@ protected static function removeUser($userId, $username) **/ protected function createUser(array $options) { - $this->helper->authenticateDashboard('mgr'); + $this->helper->authenticate('mgr'); // retrieve required arguments $username = isset($options['username']) ? $options['username'] : null; @@ -200,26 +200,27 @@ protected function createUser(array $options) $userType = isset($options['user_type']) ? $options['user_type'] : self::DEFAULT_USER_TYPE; $output = isset($options['output']) ? $options['output'] : 'test.create.user'; $expectedSuccess = isset($options['expected_success']) ? $options['expected_success'] : true; + $expectedHttpCode = !$expectedSuccess ? 400 : 200; // construct form params for post request to create new user. $data = array( - 'operation' => 'create_user', + 'operation' => 'create_user', 'account_request_id' => '', - 'first_name' => $firstName, - 'last_name' => $lastName, - 'email_address' => $emailAddress, - 'username' => $username, - 'acls' => json_encode( + 'first_name' => $firstName, + 'last_name' => $lastName, + 'email_address' => $emailAddress, + 'username' => $username, + 'acls' => json_encode( $acls ), - 'assignment' => $person, - 'institution' => $institution, - 'user_type' => $userType + 'assignment' => $person, + 'institution' => $institution, + 'user_type' => $userType ); $response = $this->helper->post('controllers/user_admin.php', null, $data); - $this->validateResponse($response); + $this->validateResponse($response, $expectedHttpCode); // retrieve the expected results of submitting the 'create_user' request // with the supplied arguments. @@ -236,12 +237,12 @@ protected function createUser(array $options) // expected have keys in common. $substitutions = array( '$emailAddress' => $emailAddress, - '$username' => $username, - '$userType' => $userType, - '$firstName' => $firstName, - '$lastName' => $lastName, - '$assignment' => $person, - '$institution' => $institution + '$username' => $username, + '$userType' => $userType, + '$firstName' => $firstName, + '$lastName' => $lastName, + '$assignment' => $person, + '$institution' => $institution ); // retrieve the keys that the actual / expected have in common. @@ -262,27 +263,31 @@ protected function createUser(array $options) $userId = $this->retrieveUserId($username); self::$newUsers[$username] = $userId; } - + $this->log("Logging out of mgr session"); // make sure to logout of the current 'mgr' session. - $this->helper->logoutDashboard(); + $this->helper->logout(); return $userId; } - protected function updateCurrentUser($userId, $password = null, $firstName = null, $lastName = null, $emailAddress = null) + protected function updateCurrentUser($username, $password, $firstName = null, $lastName = null, $emailAddress = null) { $helper = new XdmodTestHelper(); - $helper->authenticateDashboard('mgr'); - $loginAsParams = array( - 'uid' => $userId - ); + $this->log("Logging in as Manager!"); + $helper->authenticate('mgr'); // perform the pseudo-login - $helper->get('internal_dashboard/controllers/pseudo_login.php', $loginAsParams); + $this->log("Attempting to Switch Users"); + $switchResult = $helper->get("?_switch_user=$username"); + if ($switchResult[1]['http_code'] !== 200) { + $this->fail("Unable to switch to $username"); + } // build the update user params - $updateUserData = array(); + $updateUserData = [ + '_user_switch' => $username + ]; if (isset($password)) { $updateUserData['password'] = $password; @@ -315,9 +320,15 @@ protected function updateCurrentUser($userId, $password = null, $firstName = nul $this->assertEquals( $expected, $updateUserResponse[0], - "Unable to validate update user response. Expected: " . json_encode($expected). " Received: " . json_encode($updateUserResponse[0]) + "Unable to validate update user response. Expected: " . json_encode($expected) . " Received: " . json_encode($updateUserResponse[0]) ); + $switchBackResult = $helper->get('', ['_switch_user' => '_exit']); + $this->assertEquals( + 200, + $switchBackResult[1]['http_code'], + 'Switch Back Request unexpectedly failed' + ); $helper->logout(); } @@ -326,29 +337,30 @@ protected function updateCurrentUser($userId, $password = null, $firstName = nul * arguments. Note that this utilizes the user_admin/update_user operation to do the updating * as opposed to the `updateCurrentUser` function that utilizes the `users/current` rest path. * - * @param int $userId + * @param int $userId * @param string $emailAddress - * @param array $acls - * @param int $assignedPerson - * @param int $institution - * @param int $user_type + * @param array $acls + * @param int $assignedPerson + * @param int $institution + * @param int $user_type * @throws Exception */ protected function updateUser($userId, $emailAddress, $acls, $assignedPerson, $institution, $user_type, $sticky = false) { $data = array( - 'operation' => 'update_user', - 'uid' => $userId, + 'operation' => 'update_user', + 'uid' => $userId, 'email_address' => $emailAddress, - 'acls' => json_encode( + 'acls' => json_encode( $acls ), 'assigned_user' => $assignedPerson, - 'institution' => $institution, - 'user_type' => $user_type, - 'sticky' => $sticky + 'institution' => $institution, + 'user_type' => $user_type, + 'sticky' => $sticky ); - $this->helper->authenticateDashboard('mgr'); + + $this->helper->authenticate('mgr'); $response = $this->helper->post('controllers/user_admin.php', null, $data); @@ -365,7 +377,7 @@ protected function updateUser($userId, $emailAddress, $acls, $assignedPerson, $i $this->assertTrue($data['success'], "Expected the 'success' property to be: true Received: " . $data['success']); - $this->helper->logoutDashboard(); + $this->helper->logout(); } /** @@ -380,14 +392,14 @@ protected function updateUser($userId, $emailAddress, $acls, $assignedPerson, $i */ protected function retrieveUserId($userName, $userGroup = 3) { - $this->helper->authenticateDashboard('mgr'); + $this->helper->authenticate('mgr'); $listUsersResponse = $this->helper->post( 'controllers/user_admin.php', null, array( 'operation' => 'list_users', - 'group' => $userGroup + 'group' => $userGroup ) ); @@ -404,13 +416,13 @@ protected function retrieveUserId($userName, $userGroup = 3) $this->assertNotNull($userId, "Unable to find user: $userName in user group: $userGroup"); - $this->helper->logoutDashboard(); + $this->helper->logout(); return $userId; } /** - * @param string $userId the `id` of the user whose properties we are retrieving. + * @param string $userId the `id` of the user whose properties we are retrieving. * @param array $properties the set of properties that we want to retrieve from the user. * @return mixed|array An empty array if none of the requested properties are found. If * only one property is requested / found then return the properties @@ -420,14 +432,14 @@ protected function retrieveUserId($userName, $userGroup = 3) */ protected function retrieveUserProperties($userId, array $properties) { - $this->helper->authenticateDashboard('mgr'); + $this->helper->authenticate('mgr'); $response = $this->helper->post( 'controllers/user_admin.php', null, array( 'operation' => 'get_user_details', - 'uid' => $userId + 'uid' => $userId ) ); @@ -436,10 +448,9 @@ protected function retrieveUserProperties($userId, array $properties) $user = $response[0]['user_information']; $keys = array_intersect($properties, array_keys($user)); $results = array_intersect_key($user, array_flip($keys)); + $this->helper->logout(); - $this->helper->logoutDashboard(); - - return count($results) === 1 && count($properties) === 1 ? array_pop($results) : $results; + return count($results) === 1 && count($properties) === 1 ? array_pop($results) : $results; } /** @@ -447,8 +458,8 @@ protected function retrieveUserProperties($userId, array $properties) * response. In particular, it asserts that the http-code and content-type * match the provided arguments. * - * @param mixed $response to be validated. - * @param int $expectedHttpCode the http-code that the response is + * @param mixed $response to be validated. + * @param int $expectedHttpCode the http-code that the response is * expected to have. * @param string $expectedContentType the content-type that the response is * expected to have. diff --git a/tests/integration/lib/Controllers/ControllerTest.php b/tests/integration/lib/Controllers/ControllerTest.php index 86dffd076f..04be089426 100644 --- a/tests/integration/lib/Controllers/ControllerTest.php +++ b/tests/integration/lib/Controllers/ControllerTest.php @@ -22,7 +22,7 @@ protected function setup(): void public function testEnumExistingUsers() { - $this->helper->authenticateDashboard('mgr'); + $this->helper->authenticate('mgr'); $params = array( 'operation' => 'enum_existing_users', @@ -65,7 +65,7 @@ public function testEnumExistingUsers() $this->assertTrue($allFound, "There were other differences besides the expected 'last_logged_in' | " . json_encode($actualUsers)); - $this->helper->logoutDashboard(); + $this->helper->logout(); } public function testEnumUserTypes() @@ -74,7 +74,7 @@ public function testEnumUserTypes() parent::getTestFiles()->getFile('controllers', 'enum_user_types-8.0.0') ); - $this->helper->authenticateDashboard('mgr'); + $this->helper->authenticate('mgr'); $data = array( 'operation' => 'enum_user_types' @@ -97,7 +97,7 @@ public function testEnumUserTypes() $this->assertEquals($expected, $actual, "Expected the actual results to match the expected results"); - $this->helper->logoutDashboard(); + $this->helper->logout(); } public function testEnumRoles() @@ -106,7 +106,7 @@ public function testEnumRoles() parent::getTestFiles()->getFile('controllers', 'enum_roles-add_default_center') ); - $this->helper->authenticateDashboard('mgr'); + $this->helper->authenticate('mgr'); $data = array( 'operation' => 'enum_roles' @@ -158,7 +158,7 @@ function ($value, $index, $properties) use (&$success) { $this->assertTrue($allFound, "There were other differences besides the expected 'last_logged_in'"); - $this->helper->logoutDashboard(); + $this->helper->logout(); } @@ -174,7 +174,7 @@ public function testListUsers(array $options) $group = $options['user_group']; $outputFile = $options['output']; - $this->helper->authenticateDashboard('mgr'); + $this->helper->authenticate('mgr'); $data = array( 'operation' => 'list_users', @@ -198,7 +198,7 @@ public function testListUsers(array $options) $this->assertTrue($allFound, "There were other differences besides the expected 'last_logged_in'"); - $this->helper->logoutDashboard(); + $this->helper->logout(); } public function listUsersGroupProvider() @@ -214,7 +214,7 @@ public function testEnumUserTypesAndRoles() parent::getTestFiles()->getFile('controllers', 'enum_user_types_and_roles-update_enum_user_types_and_roles') ); - $this->helper->authenticateDashboard('mgr'); + $this->helper->authenticate('mgr'); $data = array( 'operation' => 'enum_user_types_and_roles' @@ -237,14 +237,14 @@ public function testEnumUserTypesAndRoles() $this->assertEquals($expected, $actual, "Expected the actual results to equal the expected."); - $this->helper->logoutDashboard(); + $this->helper->logout(); } public function testSabUserEnumTgUsers() { - $this->helper->authenticateDashboard('mgr'); + $this->helper->authenticate('mgr'); $data = array( 'start' => 0, @@ -300,12 +300,12 @@ function ($value) use ($expectedUser) { } } $this->assertEmpty($notFound, "There were expected users missing in actual (person_id is not actually checked and may be different).\nExpected: " . json_encode($notFound) . "\nActual: " . json_encode($actualUsers)); - $this->helper->logoutDashboard(); + $this->helper->logout(); } public function testCreateUser() { - $this->helper->authenticateDashboard('mgr'); + $this->helper->authenticate('mgr'); $data = array( 'operation' => 'create_user', @@ -355,7 +355,7 @@ public function testCreateUser() $this->assertEquals($expectedMessage, $data['message'], "Expected the 'message' property to be: $expectedMessage Received: " . $data['message']); } - $this->helper->logoutDashboard(); + $this->helper->logout(); } /** @@ -363,7 +363,7 @@ public function testCreateUser() */ public function testModifyUser() { - $this->helper->authenticateDashboard('mgr'); + $this->helper->authenticate('mgr'); $users = $this->listUsers(); @@ -413,7 +413,7 @@ function ($item) { $this->assertEquals($expectedStatus, $data['status'], "Expected the 'status' property to be: $expectedStatus Received: " . $data['status']); $this->assertEquals('bsmith', $data['username'], "Expected the 'username' property to be: $expectedUsername Received: " . $data['username']); $this->assertEquals($expectedUserType, $data['user_type'], "Expected the 'user_type' property to be $expectedUserType Received: " . $data['user_type']); - $this->helper->logoutDashboard(); + $this->helper->logout(); } /** @@ -421,7 +421,7 @@ function ($item) { */ public function testDeleteUser() { - $this->helper->authenticateDashboard('mgr'); + $this->helper->authenticate('mgr'); $users = $this->listUsers(); $user = array_values( @@ -455,7 +455,7 @@ function ($item) { $this->assertTrue($data['success'], "Expected the 'success' property to be: true Received: " . $data['success']); $this->assertEquals($expectedMessage, $data['message'], "Expected the 'message' property to be: $expectedMessage received: " . $data['message']); - $this->helper->logoutDashboard(); + $this->helper->logout(); } /** @@ -471,7 +471,7 @@ public function testEnumTargetAddresses(array $options) $expectedFile = $expected['file']; $expectedFileName = parent::getTestFiles()->getFile('controllers', $expectedFile); - $expectedContentType = array_key_exists('content_type', $expected) ? $expected['content_type'] : 'text/html; charset=UTF-8'; + $expectedContentType = array_key_exists('content_type', $expected) ? $expected['content_type'] : 'application/json'; $expectedHttpCode = array_key_exists('http_code', $expected) ? $expected['http_code'] : 200; $data = array_merge( @@ -507,7 +507,7 @@ public function testEnumTargetAddresses(array $options) $this->assertEquals($expected, $actual); if (isset($options['last'])) { - $helper->logoutDashboard(); + $helper->logout(); } } @@ -520,7 +520,7 @@ public function provideEnumTargetAddresses() $data = JSON::loadFile(parent::getTestFiles()->getFile('controllers', 'enum_target_addresses-update_enum_user_types_and_roles', 'input')); $helper = new XdmodTestHelper(); - $helper->authenticateDashboard('mgr'); + $helper->authenticate('mgr'); foreach($data as $key => $test) { foreach($test[0]['data'] as $dataKey => $value) { diff --git a/tests/integration/lib/Controllers/MetricExplorerTest.php b/tests/integration/lib/Controllers/MetricExplorerTest.php index c2ea7fd6e3..e0349ef364 100644 --- a/tests/integration/lib/Controllers/MetricExplorerTest.php +++ b/tests/integration/lib/Controllers/MetricExplorerTest.php @@ -136,12 +136,12 @@ public function testInvalidChartRequests() { unset($params['font_size']); - $response = $this->helper->post('/controllers/metric_explorer.php', null, $params); + $response = $this->helper->post('controllers/metric_explorer.php', null, $params); $output = json_decode($response[0]); $this->assertEquals($output->data[0]->layout->annotations[0]->font->size, "19"); $params['data_series'] = '[object Object]'; - $response = $this->helper->post('/controllers/metric_explorer.php', null, $params); + $response = $this->helper->post('controllers/metric_explorer.php', null, $params); $this->assertEquals(400, $response[1]['http_code']); } @@ -190,19 +190,19 @@ public function testInvalidRawDataRequests() { $this->helper->authenticate('cd'); unset($params['start_date']); - $response = $this->helper->post('/controllers/metric_explorer.php', null, $params); + $response = $this->helper->post('controllers/metric_explorer.php', null, $params); $this->assertFalse($response[0]['success']); - $this->assertEquals('missing required start_date parameter', $response[0]['message']); + $this->assertEquals('start_date is a required parameter.', $response[0]['message']); $params['start_date'] = '2016-12-29'; unset($params['end_date']); - $response = $this->helper->post('/controllers/metric_explorer.php', null, $params); + $response = $this->helper->post('controllers/metric_explorer.php', null, $params); $this->assertFalse($response[0]['success']); - $this->assertEquals('missing required end_date parameter', $response[0]['message']); + $this->assertEquals('end_date is a required parameter.', $response[0]['message']); $params['end_date'] = '2016-12-29'; $params['data_series'] = '[object Object]'; - $response = $this->helper->post('/controllers/metric_explorer.php', null, $params); + $response = $this->helper->post('controllers/metric_explorer.php', null, $params); $this->assertFalse($response[0]['success']); $this->assertEquals('Invalid data_series specified', $response[0]['message']); } @@ -452,7 +452,7 @@ public function testGetRawData($params, $limit, $shouldHaveTotalAvail) $this->helper->authenticate('cd'); - $response = $this->helper->post('/controllers/metric_explorer.php', null, $params); + $response = $this->helper->post('controllers/metric_explorer.php', null, $params); $this->assertArrayHasKey('data', $response[0]); $this->assertCount($limit, $response[0]['data']); diff --git a/tests/integration/lib/Controllers/ReportBuilderTest.php b/tests/integration/lib/Controllers/ReportBuilderTest.php index 664be88379..5b6fcab07a 100644 --- a/tests/integration/lib/Controllers/ReportBuilderTest.php +++ b/tests/integration/lib/Controllers/ReportBuilderTest.php @@ -58,7 +58,7 @@ protected function setup(): void if (!isset($this->verbose)) { $this->verbose = false; } - $this->helper = new XdmodTestHelper(__DIR__ . '/../../../'); + $this->helper = new XdmodTestHelper(); } public function provideDlReportInputValidation() @@ -72,7 +72,7 @@ public function provideDlReportInputValidation() ); $response = array( 'success' => false, - 'message' => 'Invalid filename' + 'message' => 'Invalid report_loc' ); $tests[] = array($params, $response); @@ -82,7 +82,10 @@ public function provideDlReportInputValidation() 'report_loc' => '3-1614908275-PVe1U', 'format' => 'rar' ); - $response = 'Invalid format specified'; + $response = [ + 'success' => false, + 'message' => 'Invalid format' + ]; $tests[] = array($params, $response); @@ -91,7 +94,7 @@ public function provideDlReportInputValidation() ); $response = array( 'success' => false, - 'message' => '\'report_loc\' not specified.' + 'message' => 'report_loc is a required parameter.' ); $tests[] = array($params, $response); @@ -102,7 +105,7 @@ public function provideDlReportInputValidation() ); $response = array( 'success' => false, - 'message' => '\'format\' not specified.' + 'message' => 'format is a required parameter.' ); $tests[] = array($params, $response); @@ -118,9 +121,9 @@ public function provideDlReportInputValidation() public function testDownloadReportInputValidation($params, $expected) { $this->helper->authenticate('usr'); - $data = $this->helper->get('/controllers/report_builder.php', $params); + $data = $this->helper->get('controllers/report_builder.php', $params); - $response = $this->helper->get('/controllers/report_builder.php', $params); + $response = $this->helper->get('controllers/report_builder.php', $params); $data = $response[0]; $curlinfo = $response[1]; @@ -166,7 +169,7 @@ public function testEnumAvailableCharts(array $options) 'operation' => $operation ); - $response = $this->helper->post("/controllers/report_builder.php", null, $params); + $response = $this->helper->post("controllers/report_builder.php", null, $params); $this->assertEquals($expected['content_type'], $response[1]['content_type']); $this->assertEquals($expected['http_code'], $response[1]['http_code']); @@ -227,7 +230,7 @@ public function testEnumReports(array $options) 'operation' => $operation ); - $response = $this->helper->post("/controllers/report_builder.php", null, $params); + $response = $this->helper->post("controllers/report_builder.php", null, $params); $this->assertEquals($expected['content_type'], $response[1]['content_type']); $this->assertEquals($expected['http_code'], $response[1]['http_code']); @@ -283,10 +286,10 @@ public function testCreateReport(array $options) $this->log("Logged in as $user"); $chartParams = array(); - + $i = 0; foreach ($charts as $chart) { $chartParams = array(); - $this->log("Creating Chart..."); + $this->log("Creating Chart $i..."); // create the chart... $success = $this->createChart($chart); @@ -315,6 +318,7 @@ public function testCreateReport(array $options) $paramString = substr($thumbnailLink, strpos($thumbnailLink, '?') + 1, strlen($thumbnailLink) - strpos($thumbnailLink, '?')); $params = explode('&', $paramString); + $this->log(sprintf("Params:\n %s", var_export($params, true))); $results = array(); foreach ($params as $param) { list($key, $value) = explode('=', $param); @@ -333,25 +337,33 @@ public function testCreateReport(array $options) 'start_date' => $startDate, 'end_date' => $endDate ); - + $this->log('Rendering Report Image'); + $this->log(sprintf("New Params:\n %s", var_export($results, true))); // render the chart image so that a temp file is created on the backend. $this->reportImageRenderer($results); } - + $i += 1; } + $this->log('Rendering Chart Params...'); // render the charts as volatile foreach ($chartParams as $chartData) { + $params = $chartData['params']; $params['type'] = 'volatile'; + $this->log(var_export($params, true)); $this->reportImageRenderer($params); } + $this->log('Done Rendering Chart Params!'); + $this->log('Get new report Name'); // Retrieve the next available report name for this user. $reportName = $this->getNewReportName(); $data['report_name'] = $reportName; + $this->log('Creating Report...'); + $this->log(var_export($data, true)); // Attempt to create the report. $reportId = $this->createReport($data); @@ -457,7 +469,7 @@ public function testEnumTemplates(array $options) } $response = $this->helper->post( - '/controllers/report_builder.php', + 'controllers/report_builder.php', null, array('operation' => 'enum_templates') ); @@ -568,11 +580,17 @@ private function processChartAction(array $data, array $expected) $this->log("Processing Chart Action: $expectedAction"); - $response = $this->helper->post('/controllers/chart_pool.php', null, $data); + $response = $this->helper->post('controllers/chart_pool.php', null, $data); + $this->log('Expected Content-Type: [' . $expectedContentType . ']'); $this->log("Response Content-Type: [" . $response[1]['content_type'] . "]"); + $this->log('Expected HTTP-Code : [' . $expectedHttpCode . ']'); $this->log("Response HTTP-Code : [" . $response[1]['http_code'] . "]"); + if (($expectedContentType !== $response[1]['content_type']) || + ($expectedHttpCode !== $response[1]['http_code'])) { + echo var_export($response, true) . "\n"; + } $this->assertEquals($expectedContentType, $response[1]['content_type']); $this->assertEquals($expectedHttpCode, $response[1]['http_code']); @@ -581,7 +599,7 @@ private function processChartAction(array $data, array $expected) $this->log("\tResponse: " . json_encode($json)); $this->assertEquals($expectedResponse, $json); - + $this->log(sprintf('Done Processing %s Chart Action!', $expectedAction)); return $json['success']; } @@ -594,7 +612,7 @@ private function processChartAction(array $data, array $expected) private function createReport(array $data) { $this->log("Creating Report"); - $response = $this->helper->post('/controllers/report_builder.php', null, $data); + $response = $this->helper->post('controllers/report_builder.php', null, $data); $this->log("Response Content-Type: [" . $response[1]['content_type'] . "]"); $this->log("Response HTTP-Code : [" . $response[1]['http_code'] . "]"); @@ -633,7 +651,7 @@ private function removeReportById($reportId) 'selected_report' => $reportId ); - $response = $this->helper->post('/controllers/report_builder.php', null, $data); + $response = $this->helper->post('controllers/report_builder.php', null, $data); $this->log("Response Content-Type: [" . $response[1]['content_type'] . "]"); $this->log("Response HTTP-Code : [" . $response[1]['http_code'] . "]"); @@ -665,7 +683,7 @@ private function getNewReportName() 'operation' => 'get_new_report_name' ); - $response = $this->helper->post('/controllers/report_builder.php', null, $data); + $response = $this->helper->post('controllers/report_builder.php', null, $data); $this->log("Response Content-Type: [" . $response[1]['content_type'] . "]"); $this->log("Response HTTP-Code : [" . $response[1]['http_code'] . "]"); @@ -707,10 +725,11 @@ private function getNewReportName() */ private function enumAvailableCharts() { + $this->log('Enum Available Charts'); $data = array( 'operation' => 'enum_available_charts' ); - $response = $this->helper->post('/controllers/report_builder.php', null, $data); + $response = $this->helper->post('controllers/report_builder.php', null, $data); $this->log("Response Content-Type: [" . $response[1]['content_type'] . "]"); $this->log("Response HTTP-Code : [" . $response[1]['http_code'] . "]"); @@ -732,19 +751,13 @@ private function enumAvailableCharts() */ private function reportImageRenderer(array $params) { - $response = $this->helper->get('/report_image_renderer.php', $params); + $response = $this->helper->get('reports/builder/image', $params); + print_r($response); $this->log("Response Content-Type: [" . $response[1]['content_type'] . "]"); $this->log("Response HTTP-Code : [" . $response[1]['http_code'] . "]"); $this->assertEquals('image/png', $response[1]['content_type']); $this->assertEquals(200, $response[1]['http_code']); } - - private function log($msg) - { - if ($this->verbose) { - echo "$msg\n"; - } - } } diff --git a/tests/integration/lib/Controllers/RoleDelegationTest.php b/tests/integration/lib/Controllers/RoleDelegationTest.php index e2d0fe0257..289f8345ff 100644 --- a/tests/integration/lib/Controllers/RoleDelegationTest.php +++ b/tests/integration/lib/Controllers/RoleDelegationTest.php @@ -56,13 +56,13 @@ public function testSuccessfulRoleDelegation(array $options) $this->helper->authenticate($user); $response = $this->helper->post('controllers/role_manager.php', null, $data); - $this->validateResponse($response, 200, 'text/html; charset=UTF-8'); + $this->validateResponse($response, 200, 'application/json'); - $this->assertTrue(is_string($response[0]), "Response data not as expected. Received: " . json_encode($response[0])); - $content = json_decode($response[0], true); $expected = JSON::loadFile($this->getTestFiles()->getFile('role_delegation', $expectedFileName)); - - $this->assertEquals($expected, $content); + if ($response[0] !== $expected) { + echo var_export($response, true); + } + $this->assertEquals($expected, $response[0]); $this->helper->logout(); } @@ -107,7 +107,7 @@ public function testInvalidRoleDelegation(array $options) $this->helper->authenticate($user); $response = $this->helper->post('controllers/role_manager.php', null, $data); - $this->validateResponse($response, 200, 'application/json'); + $this->validateResponse($response); $content = $response[0]; $expected = JSON::loadFile($this->getTestFiles()->getFile('role_delegation', $expectedFileName)); diff --git a/tests/integration/lib/Controllers/SSOLoginTest.php b/tests/integration/lib/Controllers/SSOLoginTest.php index 3033c105f5..6f3534d52e 100644 --- a/tests/integration/lib/Controllers/SSOLoginTest.php +++ b/tests/integration/lib/Controllers/SSOLoginTest.php @@ -1051,8 +1051,8 @@ public function loginsProvider() public function createSystemAccount($personLongName, $resourceId, $username) { $query = <<markTestSkipped('Needs realm integration.'); } - $response = $this->helper->post('/controllers/user_interface.php', null, $input); + $response = $this->helper->post('controllers/user_interface.php', null, $input); $this->assertEquals('application/json', $response[1]['content_type']); $this->assertEquals(400, $response[1]['http_code']); @@ -119,7 +119,7 @@ public function testGetTabs() $this->markTestSkipped('Needs realm integration.'); } - $response = $this->helper->post('/controllers/user_interface.php', null, array('operation' => 'get_tabs', 'public_user' => 'true')); + $response = $this->helper->post('controllers/user_interface.php', null, array('operation' => 'get_tabs', 'public_user' => 'true')); $this->assertEquals($response[1]['content_type'], 'application/json'); $this->assertEquals($response[1]['http_code'], 200); @@ -152,7 +152,7 @@ public function testSystemUsernameAccess() $this->markTestSkipped('Needs realm integration.'); } self::$publicView['group_by'] = "username"; - $response = $this->helper->post('/controllers/user_interface.php', null, self::$publicView); + $response = $this->helper->post('controllers/user_interface.php', null, self::$publicView); $expectedErrorMessage = <<helper->post('/controllers/user_interface.php', null, $input); + $response = $this->helper->post('controllers/user_interface.php', null, $input); $got = json_decode($response[0], true); @@ -383,9 +383,9 @@ public function testAggregateViewValidData($view, $expected) $this->markTestSkipped('Needs realm integration.'); } - $response = $this->helper->post('/controllers/user_interface.php', null, $view); + $response = $this->helper->post('controllers/user_interface.php', null, $view); - $this->assertNotFalse(strpos($response[1]['content_type'], 'text/plain')); + $this->assertNotFalse(strpos($response[1]['content_type'], 'text/html; charset=UTF-8')); $this->assertEquals($response[1]['http_code'], 200); $plotdata = json_decode($response[0], true); @@ -414,9 +414,9 @@ public function testErrorBars($input, $expected) if (!in_array("jobs", self::$XDMOD_REALMS)) { $this->markTestSkipped('Needs realm integration.'); } - $response = $this->helper->post('/controllers/user_interface.php', null, $input); + $response = $this->helper->post('controllers/user_interface.php', null, $input); - $this->assertNotFalse(strpos($response[1]['content_type'], 'text/plain')); + $this->assertNotFalse(strpos($response[1]['content_type'], 'text/html; charset=UTF-8')); $this->assertEquals($response[1]['http_code'], 200); $plotdata = json_decode(UsageExplorerHelper::demanglePlotData($response[0]), true); @@ -493,10 +493,9 @@ public function testExport($chartConfig, $expectedMimeType, $expectedFinfo) $this->markTestSkipped('Needs realm integration.'); } - $response = $this->helper->post('/controllers/user_interface.php', null, $chartConfig); + $response = $this->helper->post('controllers/user_interface.php', null, $chartConfig); $this->assertEquals($response[1]['http_code'], 200); - $actualContentType = $response[1]['content_type']; $this->assertEquals($expectedMimeType, $actualContentType); @@ -626,7 +625,7 @@ public function testPublicUserGetMenus() } EOF; - $response = $this->helper->post('/controllers/user_interface.php', null, json_decode($data, true)); + $response = $this->helper->post('controllers/user_interface.php', null, json_decode($data, true)); $this->assertEquals($response[1]['content_type'], 'application/json'); $this->assertEquals($response[1]['http_code'], 200); @@ -670,7 +669,7 @@ public function testDataFiltering($user, $chartSettings, $expectedNames) $this->helper->authenticate($user); - $response = $this->helper->post('/controllers/user_interface.php', null, $chartSettings); + $response = $this->helper->post('controllers/user_interface.php', null, $chartSettings); $this->assertEquals($response[1]['http_code'], 200); @@ -1144,7 +1143,7 @@ public function testGetTimeseriesDataCsv( $expectedParameterLine = ''; } $response = $this->helper->post( - '/controllers/user_interface.php', + 'controllers/user_interface.php', null, $data ); diff --git a/tests/integration/lib/Controllers/UserAdminTest.php b/tests/integration/lib/Controllers/UserAdminTest.php index 3ba31abf08..2e26bc2b87 100644 --- a/tests/integration/lib/Controllers/UserAdminTest.php +++ b/tests/integration/lib/Controllers/UserAdminTest.php @@ -19,17 +19,18 @@ class UserAdminTest extends BaseUserAdminTest */ public function testCreateUserFails(array $params, array $expected) { - $this->helper->authenticateDashboard('mgr'); + $this->helper->authenticate('mgr'); $response = $this->helper->post('controllers/user_admin.php', null, $params); + $this->assertTrue(strpos($response[1]['content_type'], 'application/json') >= 0); - $this->assertEquals(200, $response[1]['http_code']); + $this->assertEquals(400, $response[1]['http_code']); $actual = $response[0]; $this->assertEquals($expected, $actual); - $this->helper->logoutDashboard(); + $this->helper->logout(); } /** @@ -163,7 +164,7 @@ public function testCreateUsersSuccess(array $user) // users password so that it can be used to login in future tests. if ($userId !== null) { $username = array_search($userId, self::$newUsers); - $this->updateCurrentUser($userId, $username); + $this->updateCurrentUser($username, $username); } } @@ -575,7 +576,7 @@ function ($key, $value) use ($expectedStat, $expectedDifferences, $actualDiffere } if (isset($options['last'])) { - $helper->logoutDashboard(); + $helper->logout(); } } @@ -736,7 +737,7 @@ function ($key, $expectedRow) use ($actualRow, $ignoredColumns) { } if (isset($options['last'])) { - $helper->logoutDashboard(); + $helper->logout(); } } @@ -751,7 +752,7 @@ public function provideGetUserVisits() ); $helper = new XdmodTestHelper(); - $helper->authenticateDashboard('mgr'); + $helper->authenticate('mgr'); foreach($data as &$datum) { $datum[0]['helper'] = $helper; @@ -831,7 +832,7 @@ protected function getUserVisits(array $options) $expectedData = $options['expected']; $expectedContentType = $expectedData['content_type']; - $this->helper->authenticateDashboard('mgr'); + $this->helper->authenticate('mgr'); $data = array_merge( array( @@ -861,7 +862,7 @@ protected function getUserVisits(array $options) } } - $this->helper->logoutDashboard(); + $this->helper->logout(); return (int)$results; } diff --git a/tests/integration/lib/Logging/CCRDBHandlerTest.php b/tests/integration/lib/Logging/CCRDBHandlerTest.php index 418e5a7bb6..d090a36713 100644 --- a/tests/integration/lib/Logging/CCRDBHandlerTest.php +++ b/tests/integration/lib/Logging/CCRDBHandlerTest.php @@ -2,6 +2,9 @@ namespace IntegrationTests\Logging; +use CCR\Log; +use PHPSQLParser\Test\Creator\whereTest; + class CCRDBHandlerTest extends \PHPUnit\Framework\TestCase { @@ -19,16 +22,23 @@ public function testHandlerWritesCorrectly() 'file' => false, 'console' => false, 'mail' => false, - 'dbLogLevel' => \CCR\Log::DEBUG + 'dbLogLevel' => Log::DEBUG ) ); + // We should be able to just log strings $logger->debug("Testing DB Write Handler: $now"); + // We should be able to log string messages w/ additional context. + $logger->debug("Testing DB Write Handler w/ Context", ['timestamp' => "$now"]); + + // we should be able to log w/ no messages and only a context array. + $logger->debug('', ['message' => 'Testing 123', 'timestamp' => "$now"]); + $results = $db->query("SELECT * FROM $schema.$table WHERE message LIKE '%$now%' "); $actual = count($results); - $this->assertEquals(1, $actual, sprintf("Expected 1 log record to be written, but received: %s", $actual)); + $this->assertEquals(3, $actual, sprintf("Expected 2 log record to be written, but received: %s", $actual)); $this->assertTrue(is_numeric($results[0]['id']), sprintf("Expected the id value to be numeric, received: %s", $results[0]['id'])); // Check that the result has the required column @@ -45,21 +55,34 @@ public function testHandlerWritesCorrectly() // Check that the data contained in the required column is formatted correctly. $message = $result['message']; $json = null; + $isActuallyJson = str_contains($message, '{'); try { $json = json_decode($message); } catch (\Exception $e) { $this->fail("Expected the `message` property to be json de-codable. Received: $message"); } - $this->assertNotNull($json); - $this->assertObjectHasProperty( - 'message', - $json, - sprintf( - "Expected decoded message to be an object with a `message` property. Received: %s", - print_r($json, true) - ) - ); + // json_decode does things differently in php 8.2 vs. php 7.4. In 7.4, if you pass a string that does not + // contain json ( ex. "This is a test" ) to json_decode, it will return "This is a test". In 8.2 it will return + // null, which makes sense to be fair since "This is a test" is not valid json. But it's still annoying. + if (is_null($json) && $isActuallyJson) { + echo "\n". var_export($result, true) . "\n"; + } + // If it's null & not json then cool, if json is not null, then we expect $isActuallyJson to also be true. + $valid = (is_null($json) && !$isActuallyJson) || (!is_null($json) && $isActuallyJson); + $this->assertTrue($valid); + + // If we get valid json back, then make sure it has the `message` property. + if (!is_null($json)) { + $this->assertObjectHasProperty( + 'message', + $json, + sprintf( + "Expected decoded message to be an object with a `message` property. Received: %s", + print_r($json, true) + ) + ); + } } } diff --git a/tests/integration/lib/Rest/JobViewerTest.php b/tests/integration/lib/Rest/JobViewerTest.php index 28513e9207..fef7de5c51 100644 --- a/tests/integration/lib/Rest/JobViewerTest.php +++ b/tests/integration/lib/Rest/JobViewerTest.php @@ -18,7 +18,7 @@ class JobViewerTest extends BaseTest public function setup(): void { - $xdmodConfig = array( 'decodetextasjson' => true ); + $xdmodConfig = array( 'decodetextasjson' => true); $this->xdmodhelper = new XdmodTestHelper($xdmodConfig); } diff --git a/tests/integration/lib/Rest/WarehouseControllerProviderTest.php b/tests/integration/lib/Rest/WarehouseControllerProviderTest.php index a88cc8bddf..08bff89d6d 100644 --- a/tests/integration/lib/Rest/WarehouseControllerProviderTest.php +++ b/tests/integration/lib/Rest/WarehouseControllerProviderTest.php @@ -561,7 +561,7 @@ public function testGetAggregateDataMalformedRequests( $output ) { parent::authenticateRequestAndValidateJson( - self::$helper, + new XdmodTestHelper(), $role, $input, $output diff --git a/tests/integration/lib/Rest/WarehouseExportControllerProviderTest.php b/tests/integration/lib/Rest/WarehouseExportControllerProviderTest.php index 9c6d9455ee..14f446a92e 100644 --- a/tests/integration/lib/Rest/WarehouseExportControllerProviderTest.php +++ b/tests/integration/lib/Rest/WarehouseExportControllerProviderTest.php @@ -191,6 +191,16 @@ function ($error) { * @dataProvider provideTokenAuthTestData */ public function testGetRealmsTokenAuth($role, $tokenType) { + $find_index_by_id = function($value, $array) { + $i = 0; + foreach ($array as $item) { + if (array_key_exists('id', $item) && $item['id'] === $value) { + return $i; + } + $i += 1; + } + return false; + }; parent::runTokenAuthTest( $role, $tokenType, @@ -202,10 +212,12 @@ public function testGetRealmsTokenAuth($role, $tokenType) { 'endpoint_type' => 'rest', 'authentication_type' => 'token_optional' ], - parent::validateSuccessResponse(function ($body, $assertMessage) { + parent::validateSuccessResponse(function ($body, $assertMessage) use($find_index_by_id) { $this->assertSame(3, $body['total'], $assertMessage); - $index = 0; - foreach (['Jobs', 'Cloud', 'ResourceSpecifications'] as $realmName) { + foreach (['Jobs' => 31, 'Cloud' => 19, 'ResourceSpecifications' =>16] as $realmName => $fieldCount) { + // We can't assume that the data returned from export/realms will always be in the same order so we + // start by finding the index of the realm we're currently testing. + $index = $find_index_by_id($realmName, $body['data']); $realm = $body['data'][$index]; foreach (['id', 'name'] as $property) { $this->assertSame( @@ -231,14 +243,9 @@ public function testGetRealmsTokenAuth($role, $tokenType) { $assertMessage ); } - $index++; - } - - $counts = [31, 19, 16]; - for ($i = 0; $i < count($counts); $i++) { $this->assertCount( - $counts[$i], - $body['data'][$i]['fields'], + $fieldCount, + $body['data'][$index]['fields'], $assertMessage ); } @@ -442,10 +449,9 @@ public function testDeleteRequests($role, $httpCode, $schema) $datum['id'] = (int)$datum['id']; $ids[] = $datum['id']; } - $data = json_encode($ids); - + $data = ['ids' => json_encode($ids)]; // Delete all existing requests. - list($content, $info, $headers) = self::$helpers[$role]->delete('rest/warehouse/export/requests', null, $data); + list($content, $info, $headers) = self::$helpers[$role]->delete('rest/warehouse/export/requests', $data, $data); $this->assertMatchesRegularExpression('#\bapplication/json\b#', $headers['Content-Type'], 'Content type header'); $this->assertEquals($httpCode, $info['http_code'], 'HTTP response code'); $this->validateAgainstSchema($content, $schema); diff --git a/tests/integration/lib/TestHarness/XdmodTestHelper.php b/tests/integration/lib/TestHarness/XdmodTestHelper.php index 3608805a0d..1b71160f39 100644 --- a/tests/integration/lib/TestHarness/XdmodTestHelper.php +++ b/tests/integration/lib/TestHarness/XdmodTestHelper.php @@ -226,52 +226,12 @@ public function authenticateSSO($parameters, $includeDefault = true) } } - /** - * Attempt to authenticate using the provided $userrole against XDMoD's - * internal dashboard. - * - * @param string $userrole the role you wish to authenticate as with the - * internal dashboard. - * @throws \Exception if the specified $userrole is not present in testing.json - */ - public function authenticateDashboard($userrole) - { - if (! isset($this->config['role'][$userrole])) { - throw new \Exception("User role $userrole not defined in testing.json file"); - } - $this->userrole = $userrole; - $this->setauthvariables(null); - $data = array( - 'xdmod_username' => $this->config['role'][$userrole]['username'], - 'xdmod_password' => $this->config['role'][$userrole]['password'] - ); - $authresult = $this->post("internal_dashboard/user_check.php", null, $data); - $cookie = isset($authresult[2]['Set-Cookie']) ? $authresult[2]['Set-Cookie'] : null; - $this->setauthvariables('', $cookie); - } - public function logout() { $this->post("rest/auth/logout", null, null); $this->setauthvariables(null); } - /** - * Attempt to execute the internal dashboard's logout action for the current - * session. - */ - public function logoutDashboard() - { - $this->post( - 'internal_dashboard/controllers/controller.php', - null, - array( - 'operation' => 'logout' - ) - ); - $this->setauthvariables(null); - } - private function docurl() { $this->responseHeaders = array(); diff --git a/tests/integration/lib/TokenAuthTest.php b/tests/integration/lib/TokenAuthTest.php index 81d2b82bb6..49ecedfef7 100644 --- a/tests/integration/lib/TokenAuthTest.php +++ b/tests/integration/lib/TokenAuthTest.php @@ -4,7 +4,7 @@ use CCR\DB; use Exception; -use Models\Services\Tokens; +use CCR\Security\Helpers\Tokens; use IntegrationTests\TestHarness\XdmodTestHelper; /** diff --git a/tests/post/lib/Controllers/MetricExplorerControllerTest.php b/tests/post/lib/Controllers/MetricExplorerControllerTest.php index fe83bcb39e..773f19992a 100644 --- a/tests/post/lib/Controllers/MetricExplorerControllerTest.php +++ b/tests/post/lib/Controllers/MetricExplorerControllerTest.php @@ -25,7 +25,7 @@ public function testFilterEncoding() 'search_text' =>'fa' ); - $response = $helper->post('/controllers/metric_explorer.php', null, $params); + $response = $helper->post('controllers/metric_explorer.php', null, $params); $this->assertEquals('application/json', $response[1]['content_type']); $this->assertEquals(200, $response[1]['http_code']); @@ -85,7 +85,7 @@ public function testRawDataEncoding() } EOF; - $response = $helper->post('/controllers/metric_explorer.php', null, json_decode($config)); + $response = $helper->post('controllers/metric_explorer.php', null, json_decode($config)); $this->assertEquals('application/json', $response[1]['content_type']); $this->assertEquals(200, $response[1]['http_code']); diff --git a/tests/post/lib/Rest/JobViewerTest.php b/tests/post/lib/Rest/JobViewerTest.php index 533f6f894e..dc02b3e213 100644 --- a/tests/post/lib/Rest/JobViewerTest.php +++ b/tests/post/lib/Rest/JobViewerTest.php @@ -9,6 +9,8 @@ class JobViewerTest extends BaseTest { const ENDPOINT = 'rest/v0.1/warehouse/'; + private $xdmodhelper; + public function setup(): void { $xdmodConfig = array( 'decodetextasjson' => true ); diff --git a/tests/regression/lib/Controllers/MetricExplorerChartsTest.php b/tests/regression/lib/Controllers/MetricExplorerChartsTest.php index 320852b90a..d3f01b76fb 100644 --- a/tests/regression/lib/Controllers/MetricExplorerChartsTest.php +++ b/tests/regression/lib/Controllers/MetricExplorerChartsTest.php @@ -108,7 +108,7 @@ private function getFiltersByValue($helper, $realm, $dimension, $values) return $filters; } - private function getDimensionValues($helper, $realm, $dimension) + private static function getDimensionValues($helper, $realm, $dimension) { $params = array( 'operation' => 'get_dimension', @@ -120,12 +120,12 @@ private function getDimensionValues($helper, $realm, $dimension) 'selectedFilterIds' => '' ); - $response = $helper->post('/controllers/metric_explorer.php', null, $params); + $response = $helper->post('controllers/metric_explorer.php', null, $params); - $this->assertEquals('application/json', $response[1]['content_type']); - $this->assertEquals(200, $response[1]['http_code']); + self::assertEquals('application/json', $response[1]['content_type']); + self::assertEquals(200, $response[1]['http_code']); - $this->assertEquals($response[0]['totalCount'], count($response[0]['data'])); + self::assertEquals($response[0]['totalCount'], count($response[0]['data'])); return $response[0]['data']; } @@ -421,7 +421,7 @@ public function remainderChartProvider() * and is intended to be run against a known working XDMoD to generate a baseline * set of values for regression testing. */ - private function generateFilterTests() + private static function generateFilterTests() { // Generate test scenario for filter tests. $baseConfig = array( @@ -441,7 +441,7 @@ private function generateFilterTests() foreach ($response[0]['results'] as $dimConfig) { $dimension = $dimConfig['id']; - $dimensionValues = $this->getDimensionValues($helper, $config['realm'], $dimension); + $dimensionValues = self::getDimensionValues($helper, $config['realm'], $dimension); $testConfig = array( 'settings' => array( @@ -475,7 +475,7 @@ private function generateFilterTests() return $output; } - public function filterTestsProvider() + public static function filterTestsProvider() { $data_file = realpath(__DIR__ . '/../../../artifacts/xdmod/regression/chartFilterTests.json'); if (file_exists($data_file)) { @@ -484,7 +484,7 @@ public function filterTestsProvider() // Generate test permutations. The expected values for the data points are not set. // this causes the test function to record the values and they are then written // to a file in the tearDownAfterClass function. - $inputs = $this->generateFilterTests(); + $inputs = self::generateFilterTests(); } $helper = new XdmodTestHelper(); diff --git a/tests/regression/lib/Controllers/UsageChartsTest.php b/tests/regression/lib/Controllers/UsageChartsTest.php index cbdff166ab..e0d041b34c 100644 --- a/tests/regression/lib/Controllers/UsageChartsTest.php +++ b/tests/regression/lib/Controllers/UsageChartsTest.php @@ -139,7 +139,7 @@ private function phash($type, $imageData) public function testChartSettings($testName, $input, $expectedHash) { $postvars = null; - $response = self::$helper->post('/controllers/user_interface.php', $postvars, $input); + $response = self::$helper->post('controllers/user_interface.php', $postvars, $input); $imageData = $response[0]; $actualHash = $this->phash($input['format'], $imageData); @@ -174,7 +174,7 @@ private function genoutput($reference, $settings, $expectedHashes) $testName = ''; foreach ($settings as $key => $value) { $reference[$key] = $value; - $testName .= "${key}=${value}/"; + $testName .= "{$key}={$value}/"; } $hash = false; diff --git a/tests/regression/lib/Controllers/UsageExplorerJobsTest.php b/tests/regression/lib/Controllers/UsageExplorerJobsTest.php index 5392441e50..52fd4fc160 100644 --- a/tests/regression/lib/Controllers/UsageExplorerJobsTest.php +++ b/tests/regression/lib/Controllers/UsageExplorerJobsTest.php @@ -10,7 +10,7 @@ class UsageExplorerJobsTest extends \PHPUnit\Framework\TestCase { /** - * @var \RegressionTestHelper + * @var RegressionTestHelper */ private static $helper; diff --git a/tests/regression/lib/TestHarness/RegressionTestHelper.php b/tests/regression/lib/TestHarness/RegressionTestHelper.php index 34dc04490b..fc706756a5 100644 --- a/tests/regression/lib/TestHarness/RegressionTestHelper.php +++ b/tests/regression/lib/TestHarness/RegressionTestHelper.php @@ -360,7 +360,7 @@ public function checkCsvExport($testName, $input, $expectedFile, $userRole) throw new SkippedTestError($fullTestName . ' intentionally skipped'); } - list($csvdata, $curldata) = self::post('/controllers/user_interface.php', null, $input); + list($csvdata, $curldata) = self::post('controllers/user_interface.php', null, $input); if (!empty(self::$timingOutputDir)) { $time_data = $fullTestName . "," . $curldata['total_time'] . "," . $curldata['starttransfer_time'] . "\n"; $outputCSV = self::$timingOutputDir . "timings.csv"; diff --git a/tests/unit/lib/DataWarehouse/VisualizationTest.php b/tests/unit/lib/DataWarehouse/VisualizationTest.php index 4cd686d7a0..8b70e9dec3 100644 --- a/tests/unit/lib/DataWarehouse/VisualizationTest.php +++ b/tests/unit/lib/DataWarehouse/VisualizationTest.php @@ -25,7 +25,8 @@ public function setup(): void 0x999900, 0xCC3300, 0x669999, 0x993333, 0x339966, 0xC42525, 0xA6C96A, 0x111111); } - public function tearDown(): void { + public function tearDown(): void + { } @@ -35,7 +36,7 @@ public function testGetLotsOfColours() $v = \DataWarehouse\Visualization::getColors($count); - $this->assertEquals(count($v), 65); + $this->assertEquals(65, count($v)); } public function testGetFewColours() @@ -61,14 +62,13 @@ public function testNoWhite() $ncolours = 10; $v = \DataWarehouse\Visualization::getColors($ncolours, 0, false); - $this->assertEquals($v, array_slice($this->expected, 1) ); + $this->assertEquals($v, array_slice($this->expected, 1)); $this->assertGreaterThanOrEqual($ncolours, count($v)); } public function testArraySizes() { - for($i = 0; $i < 300; $i++) - { + for ($i = 0; $i < 300; $i++) { $v = \DataWarehouse\Visualization::getColors($i, 0, false); $this->assertGreaterThanOrEqual($i, count($v)); diff --git a/tests/unit/lib/ETL/Configuration/JsonReferenceWithFallbackTest.php b/tests/unit/lib/ETL/Configuration/JsonReferenceWithFallbackTest.php index d8c9a2d710..6a87aeffdc 100644 --- a/tests/unit/lib/ETL/Configuration/JsonReferenceWithFallbackTest.php +++ b/tests/unit/lib/ETL/Configuration/JsonReferenceWithFallbackTest.php @@ -21,7 +21,7 @@ class JsonReferenceWithFallbackTest extends TestCase public static function setupBeforeClass(): void { - // Set up a logger so we can get warnings and error messages from the ETL infrastructure + // Set up a logger so we can get warnings and error messages from the ETL infrastructure $conf = array( 'file' => false, 'db' => false, @@ -80,7 +80,7 @@ public function provideInvalidValue() */ public function testLastFileDNE($value) { - $this->expectExceptionMessageMatches("/Failed to open file '[^']+file_does_not_exist.txt': file_get_contents\([^)]+\): failed to open stream: No such file or directory/"); + $this->expectExceptionMessageMatches("/Failed to open file '[^']+file_does_not_exist.txt': file_get_contents\([^)]+\): Failed to open stream: No such file or directory/"); $this->expectException(Exception::class); $this->runTransformTest($value); } diff --git a/tests/unit/lib/ETL/DataEndpoint/WebServerLogFileTest.php b/tests/unit/lib/ETL/DataEndpoint/WebServerLogFileTest.php index 4855003b8e..ffa2d5ea8d 100644 --- a/tests/unit/lib/ETL/DataEndpoint/WebServerLogFileTest.php +++ b/tests/unit/lib/ETL/DataEndpoint/WebServerLogFileTest.php @@ -53,7 +53,7 @@ public function testWebServerLogFile($filename, $logFormat, $expected) $endpoint->connect(); $numIterations = 0; foreach ($endpoint as $record) { - $this->assertSame($expected[$numIterations], $record); + $this->assertEqualsCanonicalizing($expected[$numIterations], $record); $numIterations++; } $this->assertSame( diff --git a/tests/unit/lib/LogTest.php b/tests/unit/lib/LogTest.php index dab58803ae..f636ec27a0 100644 --- a/tests/unit/lib/LogTest.php +++ b/tests/unit/lib/LogTest.php @@ -39,7 +39,7 @@ public function testLogLevels($logLevel, $expectedLines) // Log messages at each level and compare the number of actual lines output to the number // expected based on the log level mask. - $logger->emerg('Emergency'); + $logger->emergency('Emergency'); $logger->alert('Alert'); $logger->critical('Critical'); $logger->error('Error'); diff --git a/tests/unit/lib/NewRest/Controllers/BaseControllerTest.php b/tests/unit/lib/NewRest/Controllers/BaseControllerTest.php deleted file mode 100644 index aa81352500..0000000000 --- a/tests/unit/lib/NewRest/Controllers/BaseControllerTest.php +++ /dev/null @@ -1,197 +0,0 @@ -getAttributes($user); - $request = $this->getRequest($attributes); - - $baseController = $this->getMockForAbstractClass('Rest\Controllers\BaseControllerProvider'); - $exception = null; - - try { - if ($requestedAcl !== null) { - $authorized = $baseController->authorize($request, array($requestedAcl)); - } else { - $authorized = $baseController->authorize($request); - } - - $this->assertEquals($authorized, $user); - } catch (\Exception $e) { - $exception = $e; - } - - $exceptionClass = $exception !== null ? get_class($exception) : null; - $message = $exception !== null ? $exception->getMessage() : null; - - $this->assertEquals($exceptionClass, $expectedException); - $this->assertEquals($message, $expectedMessage); - - } - - /** - * @param $attributes - * @return \PHPUnit\Framework\MockObject\MockObject - */ - public function getRequest($attributes) - { - $builder = $this->createMock('Symfony\Component\HttpFoundation\Request'); - $builder->attributes = $attributes; - return $builder; - } - - - /** - * @param $user - * @return \PHPUnit\Framework\MockObject\MockObject - */ - public function getAttributes($user) - { - $builder = $this->getMockBuilder('\Symfony\Component\HttpFoundation\ParameterBag'); - $builder->onlyMethods(array('get')); - $mock = $builder->getMock(); - $mock->method('get') - ->with($this->equalTo(BaseControllerProvider::_USER)) - ->willReturn($user); - return $mock; - } - - - /** - * The Data Provider for the before / after tests. - * - * @return array in the form of: - * array( - * array( - * mockUser, - * array('requested', 'acls') - * ), - * ... - * ) - */ - public function generateUserDataSet() - { - $mgr = $this->createUser(array('mgr', 'usr')); - $cd = $this->createUser(array('cd', 'usr')); - $pi = $this->createUser(array('pi', 'usr')); - $usr = $this->createUser(array('usr'), 'usr'); - $sab = $this->createUser(array('usr', 'sab')); - $pub = $this->createUser(array('pub')); - - $accessDeniedException = 'Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException'; - $unauthorizedException = 'Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException'; - - $notAuthorized = BaseControllerProvider::EXCEPTION_MESSAGE; - - $tests = array( - array($mgr, null, null, null), - array($mgr, ROLE_ID_MANAGER, null, null), - - array($cd, null, null, null), - array($cd, ROLE_ID_CENTER_DIRECTOR, null, null), - array($cd, ROLE_ID_MANAGER, $accessDeniedException, $notAuthorized), - - array($pi, null, null, null), - array($pi, ROLE_ID_PRINCIPAL_INVESTIGATOR, null, null), - array($pi, ROLE_ID_USER, null, null), - array($pi, ROLE_ID_MANAGER, $accessDeniedException, $notAuthorized), - array($pi, ROLE_ID_CENTER_DIRECTOR, $accessDeniedException, $notAuthorized), - - array($usr, null, null, null), - array($usr, ROLE_ID_USER, null, null), - array($usr, ROLE_ID_MANAGER, $accessDeniedException, $notAuthorized), - array($usr, ROLE_ID_CENTER_DIRECTOR, $accessDeniedException, $notAuthorized), - array($usr, ROLE_ID_PRINCIPAL_INVESTIGATOR, $accessDeniedException, $notAuthorized), - - array($sab, null, null, null), - array($sab, 'sab', null, null), - array($sab, ROLE_ID_USER, null, null), - array($sab, ROLE_ID_MANAGER, $accessDeniedException, $notAuthorized), - array($sab, ROLE_ID_CENTER_DIRECTOR, $accessDeniedException, $notAuthorized), - array($sab, ROLE_ID_PRINCIPAL_INVESTIGATOR, $accessDeniedException, $notAuthorized), - - array($pub, null, $unauthorizedException, $notAuthorized), - array($pub, ROLE_ID_PUBLIC, null, null), - array($pub, ROLE_ID_USER, $unauthorizedException, $notAuthorized), - array($pub, ROLE_ID_CENTER_DIRECTOR, $unauthorizedException, $notAuthorized), - array($pub, ROLE_ID_MANAGER, $unauthorizedException, $notAuthorized), - array($pub, ROLE_ID_PRINCIPAL_INVESTIGATOR, $unauthorizedException, $notAuthorized) - - ); - return $tests; - } - - /** - * Used to create a mock XDUser object suitable for use in both versions of - * the isAuthorized functions. - * - * @param array $roles an array of strings representing the - * roles / acls this user is assigned - * @return \PHPUnit_Framework_MockObject_MockObject - */ - protected function createUser(array $roles) - { - $builder = $this->getMockBuilder('\XDUser') - ->disableOriginalConstructor() - ->onlyMethods( - array( - 'getRoles', - 'isManager', - 'isPublicUser', - 'hasAcl', - 'hasAcls', - '__toString' - ) - ); - $stub = $builder->getMock(); - $stub->method('getRoles')->willReturn($roles); - $stub->method('isManager')->willReturnCallback(function () use ($roles) { - return in_array(ROLE_ID_MANAGER, $roles); - }); - $stub->method('isPublicUser')->willReturnCallback(function () use ($roles) { - return in_array(ROLE_ID_PUBLIC, $roles); - }); - $stub->method('hasAcl')->willReturnCallback(function () use ($roles) { - $args = func_get_args(); - if (count($args) >= 1) { - $arg = $args[0]; - return in_array($arg, $roles); - } - return false; - }); - $stub->method('__toString')->willReturn(json_encode(array( - 'roles' => $roles, - 'is_manager' => in_array(ROLE_ID_MANAGER, $roles), - 'is_public_user' => in_array(ROLE_ID_PUBLIC, $roles) - ))); - $stub->method('hasAcls')->willreturnCallback( - function () use ($roles) { - $args = func_get_args(); - if (count($args) >= 1 && is_array($args[0])) { - $requested = $args[0]; - $total = 0; - foreach ($requested as $value) { - $found = in_array($value, $roles); - $total += $found === true - ? 1 - : 0; - } - return $total === count($requested); - } - return false; - } - ); - return $stub; - } -}