diff --git a/.github/workflows/build-6.0.0-image.yml b/.github/workflows/build-6.0.0-image.yml new file mode 100644 index 000000000..89ca269e9 --- /dev/null +++ b/.github/workflows/build-6.0.0-image.yml @@ -0,0 +1,45 @@ +name: Build Docker 6.0.0 image + +on: + push: + branches: [ ui ] + +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Check out the repo + uses: actions/checkout@v6 + with: + ref: 'ui' + + - name: Retrieve Repomanager version + run: | + echo "VERSION=$(cat ${GITHUB_WORKSPACE}/www/version)" >> $GITHUB_ENV + echo "Version: ${{ env.VERSION }}" + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the container registry + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_TOKEN }} + + # Build the images + - name: Build and push docker + uses: docker/build-push-action@v6 + with: + file: Dockerfile + push: true + tags: lbr38/repomanager:6.0.0-beta + platforms: linux/amd64,linux/arm64,linux/arm/v7 + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/test-database-update.yml b/.github/workflows/test-database-update.yml index 0b187807a..527cda5a5 100644 --- a/.github/workflows/test-database-update.yml +++ b/.github/workflows/test-database-update.yml @@ -1,4 +1,6 @@ -name: Database update tests +# TODO debug +# Crash en boucle - à corriger une fois 6.0.0 mergé dans devel +# name: Database update tests on: push: @@ -91,7 +93,9 @@ jobs: cd ${GITHUB_WORKSPACE} - sed -i 's/branch:.*/branch: devel/g' docker-compose.yml + # TODO debug + # sed -i 's/branch:.*/branch: devel/g' docker-compose.yml + sed -i 's/branch:.*/branch: ui/g' docker-compose.yml sed -i 's/FQDN:.*/FQDN: repomanager.test.com/g' docker-compose.yml docker-compose -f docker-compose.yml up -d diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..dfa4cd630 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,7 @@ +## Approach +- Read existing files before writing. Don't re-read unless changed. +- Thorough in reasoning, concise in output. +- Skip files over 100KB unless required. +- No sycophantic openers or closing fluff. +- No emojis or em-dashes. +- Do not guess APIs, versions, flags, commit SHAs, or package names. Verify by reading code or docs before asserting. \ No newline at end of file diff --git a/docker-compose-dev-6.0.0.yml b/docker-compose-dev-6.0.0.yml new file mode 100644 index 000000000..64165c77e --- /dev/null +++ b/docker-compose-dev-6.0.0.yml @@ -0,0 +1,27 @@ +--- +# TODO debug +# For my dev environment only +version: "3" +services: + repomanager: + container_name: repomanager-dev-600 + image: repomanager + network_mode: host + environment: + FQDN: repomanager-dev-600.localhost + MAX_UPLOAD_SIZE: 64M + NGINX_LISTEN_PORT: 7777 + PUID: 1000 + PGID: 1000 + build: + context: . + dockerfile: Dockerfile + args: + branch: ui + restart: unless-stopped + volumes: + - /etc/localtime:/etc/localtime:ro + - /var/lib/docker/volumes/repomanager-dev-600-logs:/var/log/nginx + - /var/lib/docker/volumes/repomanager-dev-600-www:/var/www/repomanager + - /var/lib/docker/volumes/repomanager-dev-600-data:/var/lib/repomanager + - /var/lib/docker/volumes/repomanager-dev-600-repo:/home/repo diff --git a/docker/config/nginx/repomanager.conf b/docker/config/nginx/repomanager.conf index 9e910ce86..268db58f9 100644 --- a/docker/config/nginx/repomanager.conf +++ b/docker/config/nginx/repomanager.conf @@ -121,8 +121,12 @@ server { access_log off; } - location /repo { - alias $REPOS_DIR; + # Matches /repo/ and /repo/xxx but not /repository + location ^~ /repo/ { + # Final slash is important, otherwise it could break the download of packages + alias $REPOS_DIR/; + + # Uncomment the following lines to enable directory listing (autoindex) for repositories # autoindex on; # autoindex_exact_size off; # autoindex_localtime on; diff --git a/www/bin/repomanager b/www/bin/repomanager index cc7778301..91fbf4a16 100755 --- a/www/bin/repomanager +++ b/www/bin/repomanager @@ -92,9 +92,10 @@ echo ' | | \| ___/| |_> ) (_) ) Y Y \/ __ \| | \/ __ \/ /_/ > ___/| | \/ |__| \___ > __/ \____/|__|_| (____ /___| (____ |___ / \___ >__| \/|__| \/ \/ \/ \/_____/ \/ - ' +echo -e "Version $(cat $WWW_DIR/version)\n" + while [ $# -ge 1 ];do case "$1" in --help|-help|-h) diff --git a/www/config/tasks/create.php b/www/config/tasks/create.php index c541837c7..cc0894345 100644 --- a/www/config/tasks/create.php +++ b/www/config/tasks/create.php @@ -18,6 +18,7 @@ 'env', 'group', 'description', + 'tags', 'advanced-params' // Advanced params include package include/exclude and metadata custom fields, it is optional ], diff --git a/www/config/tasks/duplicate.php b/www/config/tasks/duplicate.php index 7e0bb3690..76dca44c5 100644 --- a/www/config/tasks/duplicate.php +++ b/www/config/tasks/duplicate.php @@ -19,6 +19,7 @@ 'env', 'group', 'description', + 'tags' ], // Conditional params must be compared with form values diff --git a/www/config/tasks/env.php b/www/config/tasks/env.php index 7f0fdce96..61ac71f5f 100644 --- a/www/config/tasks/env.php +++ b/www/config/tasks/env.php @@ -24,5 +24,10 @@ // Form configuration $formConfig = [ // Allowed schedule types for this task - 'allowed-schedule-types' => ['unique'] + // Recurring is only allowed when the task targets a dynamic set of repositories, + // as pointing an environment on a fixed snapshot repeatedly makes no sense + 'allowed-schedule-types' => ['unique'], + + // This task can target a dynamic set of repositories (all latest snapshots matching filters) + 'allow-dynamic-target' => true ]; diff --git a/www/config/tasks/rebuild.php b/www/config/tasks/rebuild.php index 2859ca75c..ddeb035ec 100644 --- a/www/config/tasks/rebuild.php +++ b/www/config/tasks/rebuild.php @@ -16,5 +16,8 @@ // Form configuration $formConfig = [ // Allowed schedule types for this task - 'allowed-schedule-types' => ['unique', 'recurring'] + 'allowed-schedule-types' => ['unique', 'recurring'], + + // This task can target a dynamic set of repositories (all latest snapshots matching filters) + 'allow-dynamic-target' => true ]; diff --git a/www/config/tasks/rename.php b/www/config/tasks/rename.php index d09a29e90..121368a7a 100644 --- a/www/config/tasks/rename.php +++ b/www/config/tasks/rename.php @@ -3,14 +3,13 @@ $taskConfig = [ 'description' => 'Rename repository', - // Retrieve repository info from snap id - 'retrieve-repo-from-snap-id' => true, + // Retrieve repository info from repo id + 'retrieve-repo-from-repo-id' => true, // Required params 'required-params' => [ 'name', - 'snap-id', - 'gpg-sign' // Required to make sure deb metadata is regenerated correctly + 'repo-id' ], // Conditional params must be compared with form values diff --git a/www/config/tasks/update.php b/www/config/tasks/update.php index 9f7018162..c1bda04f2 100644 --- a/www/config/tasks/update.php +++ b/www/config/tasks/update.php @@ -46,5 +46,8 @@ // Form configuration $formConfig = [ // Allowed schedule types for this task - 'allowed-schedule-types' => ['unique', 'recurring'] + 'allowed-schedule-types' => ['unique', 'recurring'], + + // This task can target a dynamic set of repositories (all latest snapshots matching filters) + 'allow-dynamic-target' => true ]; diff --git a/www/controllers/App/Config/Env.php b/www/controllers/App/Config/Env.php index f58cec2ab..c997e8fad 100644 --- a/www/controllers/App/Config/Env.php +++ b/www/controllers/App/Config/Env.php @@ -2,33 +2,25 @@ namespace Controllers\App\Config; -use Exception; +use Controllers\Environment; class Env { /** * Load environments */ - public static function get() + public static function get(): void { - $myenv = new \Controllers\Environment(); + $envController = new Environment(); if (!defined('ENVS')) { - define('ENVS', $myenv->listAll()); - } - if (!defined('ENVS_TOTAL')) { - define('ENVS_TOTAL', $myenv->total()); + define('ENVS', $envController->listAll()); } if (!defined('DEFAULT_ENV')) { - define('DEFAULT_ENV', $myenv->default()); - } - if (!defined('LAST_ENV')) { - define('LAST_ENV', $myenv->last()); + define('DEFAULT_ENV', $envController->default()); } - /** - * If there is no environment configured then __LOAD_ERROR_EMPTY_ENVS = 1 - */ + // If there is no environment configured then __LOAD_ERROR_EMPTY_ENVS = 1 if (empty(ENVS)) { if (!defined('__LOAD_ERROR_EMPTY_ENVS')) { define('__LOAD_ERROR_EMPTY_ENVS', 1); @@ -39,6 +31,6 @@ public static function get() } } - unset($myenv); + unset($envController); } } diff --git a/www/controllers/App/Main.php b/www/controllers/App/Main.php index 7a4aa99ca..a950fd8e9 100644 --- a/www/controllers/App/Main.php +++ b/www/controllers/App/Main.php @@ -89,7 +89,7 @@ public function __construct(string $level = 'all') // Errors related to the loading of the environments if (defined('__LOAD_ERROR_EMPTY_ENVS') && __LOAD_ERROR_EMPTY_ENVS > 0) { - $__LOAD_ERROR_MESSAGES[] = 'You must at least configure 1 environment.'; + $__LOAD_ERROR_MESSAGES[] = 'You should at least configure one environment.'; $__LOAD_GENERAL_ERROR++; } diff --git a/www/controllers/App/Permissions.php b/www/controllers/App/Permissions.php index d1f7e1a4e..9b754cd5c 100644 --- a/www/controllers/App/Permissions.php +++ b/www/controllers/App/Permissions.php @@ -12,7 +12,7 @@ class Permissions public static function load() { $userController = new \Controllers\User\User(); - $userPermissionController = new \Controllers\User\Permission(); + $userPermissionController = new \Controllers\User\Permission\Permission(); $isAdmin = false; $isSuperAdmin = false; diff --git a/www/controllers/Environment.php b/www/controllers/Environment.php index 43da91a80..a2ec2513d 100644 --- a/www/controllers/Environment.php +++ b/www/controllers/Environment.php @@ -59,7 +59,7 @@ public function add(string $name, string $color) : void * Check if environment already exists */ if ($this->exists($name)) { - throw new Exception('Environment ' . $name . ' already exists'); + throw new Exception('Environment ' . $name . ' already exists'); } /** @@ -72,7 +72,7 @@ public function add(string $name, string $color) : void /** * Add env to database */ - $this->model->add($name, $color); + $this->model->add($name, $color, 'false'); } /** @@ -106,17 +106,14 @@ public function edit(array $envs) : void throw new Exception('You are not allowed to perform this action'); } - /** - * Delete all envs from database before inserting the new ones - */ + // Delete all envs from database before inserting the new ones $this->model->deleteAll(); - /** - * Check if all specified envs are valid then add them to $envsToInsert array - */ + // Check that all specified env values are valid foreach ($envs as $env) { $name = Validate::string($env['name']); $color = Validate::string($env['color']); + $protected = Validate::string($env['protected']); if (empty($name)) { throw new Exception('Environment name is empty'); @@ -126,30 +123,54 @@ public function edit(array $envs) : void throw new Exception('Environment color is empty'); } + if (!in_array($protected, ['true', 'false'])) { + throw new Exception('Environment protected value is invalid'); + } + if (!Validate::alphaNumericHyphen($name)) { - throw new Exception('Environment ' . $name . ' contains invalid characters'); + throw new Exception('Environment ' . $name . ' contains invalid characters'); } if ($this->exists($name)) { - throw new Exception('Environment ' . $name . ' already exists'); + throw new Exception('Environment ' . $name . ' already exists'); } - $this->model->add($name, $color); + $this->model->add($name, $color, $protected); } } /** * Return all environments list */ - public function listAll() + public function listAll(): array { return $this->model->listAll(); } + /** + * Return the list of protected environments + */ + public function getProtected(): array + { + return $this->model->getProtected(); + } + + /** + * Return true if the environment is protected + */ + public function isProtected(string $name): bool + { + if (in_array($name, $this->getProtected())) { + return true; + } + + return false; + } + /** * Return default environment */ - public function default() + public function default(): string { return $this->model->default(); } diff --git a/www/controllers/Group/Group.php b/www/controllers/Group/Group.php index 212ae4752..643b68d83 100644 --- a/www/controllers/Group/Group.php +++ b/www/controllers/Group/Group.php @@ -206,6 +206,14 @@ public function getReposMembers(int $id): array return $this->model->getReposMembers($id); } + /** + * Return the list of group Ids the specified repo is member of + */ + public function getRepoGroupsIds(int $repoId): array + { + return $this->model->getRepoGroupsIds($repoId); + } + /** * Return the list of repos not in any group */ diff --git a/www/controllers/Host/Host.php b/www/controllers/Host/Host.php index 1c51ceef9..42c2dbc5f 100644 --- a/www/controllers/Host/Host.php +++ b/www/controllers/Host/Host.php @@ -89,13 +89,14 @@ public function getSettings(): array /** * Edit the display settings on the hosts page */ - public function setSettings(int $complianceThresholdCount, int $complianceThresholdDays, int $complianceRebootRequired): void + public function setSettings(int $complianceThresholdCount, int $complianceThresholdDays, int $complianceSecurityUpdate, int $complianceRebootRequired): void { if (!HostPermission::allowedAction('edit-settings')) { throw new Exception('You are not allowed to perform this action'); } - if (!is_numeric($complianceThresholdCount) or !is_numeric($complianceThresholdDays) or !is_numeric($complianceRebootRequired)) { + // Settings must be numeric + if (!is_numeric($complianceThresholdCount) or !is_numeric($complianceThresholdDays) or !is_numeric($complianceSecurityUpdate) or !is_numeric($complianceRebootRequired)) { throw new Exception('Setting must be numeric'); } @@ -104,11 +105,15 @@ public function setSettings(int $complianceThresholdCount, int $complianceThresh throw new Exception('Value must be greater than 0'); } + if ($complianceSecurityUpdate !== 0 and $complianceSecurityUpdate !== 1) { + throw new Exception('Invalid value for compliance security update setting'); + } + if ($complianceRebootRequired !== 0 and $complianceRebootRequired !== 1) { throw new Exception('Invalid value for compliance reboot required setting'); } - $this->model->setSettings($complianceThresholdCount, $complianceThresholdDays, $complianceRebootRequired); + $this->model->setSettings($complianceThresholdCount, $complianceThresholdDays, $complianceSecurityUpdate, $complianceRebootRequired); } /** @@ -233,11 +238,21 @@ public function getCompliance(int $hostId): array $host = $this->get($hostId); $settings = $this->getSettings(); + $securityUpdate = false; $compliant = true; $reason = ''; // Retrieve the total number of available packages - $available = count($hostPackageController->getAvailable()); + $available = $hostPackageController->getAvailable(); + $availableCount = count($available); + + // Check if there is at least one security update available in the list of available packages ('security' field is set to true) + foreach ($available as $package) { + if (!empty($package['Security']) and $package['Security'] == 'true') { + $securityUpdate = true; + break; + } + } // Retrieve the date of the last package upgrade event $latestUpdate = $hostPackageEventController->getLastPackageUpgradeEvent()['Date'] ?? null; @@ -246,11 +261,12 @@ public function getCompliance(int $hostId): array $thresholdDate = strtotime('-' . $settings['compliance_threshold_days'] . ' days'); // The host is not compliant if the available updates count is >= threshold - if ($available >= $settings['compliance_threshold_count']) { + if ($availableCount >= $settings['compliance_threshold_count']) { $compliant = false; - $reason = 'Pending updates count (' . $available . ') is greater than or equal to the threshold (' . $settings['compliance_threshold_count'] . ')'; + $reason = 'Pending updates count (' . $availableCount . ') is greater than or equal to the threshold (' . $settings['compliance_threshold_count'] . ')'; } + // The host is not compliant if no package update has been performed yet if (!$latestUpdate) { $compliant = false; $reason = 'No package update has been performed yet'; @@ -262,6 +278,12 @@ public function getCompliance(int $hostId): array $reason = 'Latest update date (' . $latestUpdate . ') is older than the compliance threshold (' . $settings['compliance_threshold_days'] . ' days)'; } + // Optional rule: host is not compliant when there is a security update available + if ((int) $settings['compliance_security_update'] === 1 and $securityUpdate) { + $compliant = false; + $reason = 'Host has a security update available'; + } + // Optional rule: host is not compliant when a reboot is required $rebootRequired = $host['Reboot_required'] ?? 'false'; if ((int) $settings['compliance_reboot_required'] === 1 and ($rebootRequired === 'true')) { @@ -272,8 +294,9 @@ public function getCompliance(int $hostId): array return [ 'compliant' => $compliant, 'reason' => $reason, - 'available_updates_count' => $available, + 'available_updates_count' => $availableCount, 'latest_update' => $latestUpdate, + 'security_update' => $securityUpdate, 'reboot_required' => $rebootRequired ]; } diff --git a/www/controllers/Host/Package/Package.php b/www/controllers/Host/Package/Package.php index 975420120..826d58e50 100644 --- a/www/controllers/Host/Package/Package.php +++ b/www/controllers/Host/Package/Package.php @@ -50,10 +50,19 @@ private function getPackageState(string $packageName, string|null $packageVersio /** * Return the list of inventoried packages on the host + * Supports optional pagination and search filter */ - public function getInventory() : array + public function getInventory(string $search = '', bool $withOffset = false, int $offset = 0) : array { - return $this->model->getInventory(); + return $this->model->getInventory($search, $withOffset, $offset); + } + + /** + * Return the total count of inventoried packages, optionally filtered + */ + public function countInventory(string $search = ''): int + { + return $this->model->countInventory($search); } /** @@ -143,17 +152,17 @@ private function addPackage(string $name, string $version, string $state, string /** * Add a package in the table available packages list */ - private function addPackageAvailable(string $name, string $version, string $repository) : void + private function addPackageAvailable(string $name, string $version, string $security, string $repository) : void { - $this->model->addPackageAvailable($name, $version, $repository); + $this->model->addPackageAvailable($name, $version, $security, $repository); } /** * Update a package in the available packages list */ - private function updatePackageAvailable(string $name, string $version, string $repository) : void + private function updatePackageAvailable(string $name, string $version, string $security, string $repository) : void { - $this->model->updatePackageAvailable($name, $version, $repository); + $this->model->updatePackageAvailable($name, $version, $security, $repository); } /** @@ -341,14 +350,14 @@ public function setPackagesAvailable(array $packages) : void // If the package does not exist in available packages list, add it if (!$this->packageAvailableExists($package['name'])) { - $this->addPackageAvailable($package['name'], $package['version'], $package['repository']); + $this->addPackageAvailable($package['name'], $package['version'], $package['security'], $package['repository']); continue; } // If it exists in the database, also check the version present in the database // If the version in the database is different then we update the package in the database, otherwise we do nothing if ($this->packageVersionAvailableExists($package['name'], $package['version'])) { - $this->updatePackageAvailable($package['name'], $package['version'], $package['repository']); + $this->updatePackageAvailable($package['name'], $package['version'], $package['security'], $package['repository']); } } } diff --git a/www/controllers/Layout/Chart/vars/repo-accesses-chart.vars.inc.php b/www/controllers/Layout/Chart/vars/repo-accesses-chart.vars.inc.php index 778fea038..060397059 100644 --- a/www/controllers/Layout/Chart/vars/repo-accesses-chart.vars.inc.php +++ b/www/controllers/Layout/Chart/vars/repo-accesses-chart.vars.inc.php @@ -12,12 +12,12 @@ $envData = []; // Check that repository Id is specified -if (empty(__ACTUAL_URI__[3])) { +if (empty(__ACTUAL_URI__[2])) { throw new Exception('no repository ID specified'); } // Check that repository Id is valid -if (!is_numeric(__ACTUAL_URI__[3])) { +if (!is_numeric(__ACTUAL_URI__[2])) { throw new Exception('invalid repository ID'); } @@ -34,7 +34,7 @@ // } // Retrieve repo infos from DB -$repoController->getAllById(__ACTUAL_URI__[3], '', ''); +$repoController->getAllById(__ACTUAL_URI__[2], '', ''); // Get statistics directly from database for all environments $results = []; diff --git a/www/controllers/Layout/Chart/vars/repo-snapshots-chart.vars.inc.php b/www/controllers/Layout/Chart/vars/repo-snapshots-chart.vars.inc.php index 3a041c8dc..622d5377d 100644 --- a/www/controllers/Layout/Chart/vars/repo-snapshots-chart.vars.inc.php +++ b/www/controllers/Layout/Chart/vars/repo-snapshots-chart.vars.inc.php @@ -6,20 +6,18 @@ $labels = []; $options = []; -if (__ACTUAL_URI__[2] != 'repo') { - throw new Exception('Error: invalid URI specified.'); +// Check that repository Id is specified +if (empty(__ACTUAL_URI__[2])) { + throw new Exception('no repository ID specified'); } -if (empty(__ACTUAL_URI__[3])) { - throw new Exception('Error: missing repository ID.'); -} - -if (!is_numeric(__ACTUAL_URI__[3])) { - throw new Exception('Error: invalid repository ID specified.'); +// Check that repository Id is valid +if (!is_numeric(__ACTUAL_URI__[2])) { + throw new Exception('invalid repository ID'); } // Get repository info -$repoController->getAllById(__ACTUAL_URI__[3]); +$repoController->getAllById(__ACTUAL_URI__[2]); // Get all snapshots of the repository $snapshots = $repoController->getSnapshots($repoController->getRepoId()); @@ -92,7 +90,7 @@ // Configure click callback to redirect to snapshot page 'clickCallback' => [ 'enabled' => true, - 'url' => '/browse/{value}', + 'url' => '/snapshot/{value}', 'newTab' => false // '_self' for same tab, '_blank' for new tab ] ]; diff --git a/www/controllers/Layout/Chart/vars/repo-snapshots-size-chart.vars.inc.php b/www/controllers/Layout/Chart/vars/repo-snapshots-size-chart.vars.inc.php index 4b2377dd1..296d92cf3 100644 --- a/www/controllers/Layout/Chart/vars/repo-snapshots-size-chart.vars.inc.php +++ b/www/controllers/Layout/Chart/vars/repo-snapshots-size-chart.vars.inc.php @@ -5,23 +5,21 @@ $labels = []; $options = []; -if (__ACTUAL_URI__[2] != 'repo') { - throw new Exception('Error: invalid URI specified.'); +// Check that repository Id is specified +if (empty(__ACTUAL_URI__[2])) { + throw new Exception('no repository ID specified'); } -if (empty(__ACTUAL_URI__[3])) { - throw new Exception('Error: missing repository ID.'); -} - -if (!is_numeric(__ACTUAL_URI__[3])) { - throw new Exception('Error: invalid repository ID specified.'); +// Check that repository Id is valid +if (!is_numeric(__ACTUAL_URI__[2])) { + throw new Exception('invalid repository ID'); } // Get repository info -$repoController->getAllById(__ACTUAL_URI__[3]); +$repoController->getAllById(__ACTUAL_URI__[2]); // Get all statistics for the specified repo ID -$stats = $repoStatsController->getByRepoId(__ACTUAL_URI__[3]); +$stats = $repoStatsController->getByRepoId(__ACTUAL_URI__[2]); $snapData = []; diff --git a/www/controllers/Layout/Chart/vars/repo-storage-chart.vars.inc.php b/www/controllers/Layout/Chart/vars/repo-storage-chart.vars.inc.php deleted file mode 100644 index 8d08c2b30..000000000 --- a/www/controllers/Layout/Chart/vars/repo-storage-chart.vars.inc.php +++ /dev/null @@ -1,44 +0,0 @@ - 0 && $diskUsedSpacePercent <= 30) { - $datasets[0]['colors'][] = '#15bf7f'; - $datasets[0]['colors'][] = 'rgba(21, 191, 127, 0.40)'; -} -if ($diskUsedSpacePercent > 30 && $diskUsedSpacePercent <= 50) { - $datasets[0]['colors'][] = '#ffb536'; - $datasets[0]['colors'][] = 'rgba(255, 181, 54, 0.40)'; -} -if ($diskUsedSpacePercent > 50 && $diskUsedSpacePercent <= 70) { - $datasets[0]['colors'][] = '#ff7c49'; - $datasets[0]['colors'][] = 'rgba(255, 124, 73, 0.40)'; -} -if ($diskUsedSpacePercent > 70 && $diskUsedSpacePercent <= 100) { - $datasets[0]['colors'][] = '#F32F63'; - $datasets[0]['colors'][] = 'rgba(243, 47, 99, 0.40)'; -} - -$options['innerRadius'] = '65%'; -$options['outerRadius'] = '99%'; -$options['toolbox']['show'] = false; -$options['legend']['show'] = false; -$options['tooltip']['show'] = false; -$options['emphasis']['disabled'] = true; - -unset($diskTotalSpace, $diskFreeSpace, $diskUsedSpace); diff --git a/www/controllers/Layout/Container/vars/browse/actions.vars.inc.php b/www/controllers/Layout/Container/vars/browse/actions.vars.inc.php deleted file mode 100644 index a5430edce..000000000 --- a/www/controllers/Layout/Container/vars/browse/actions.vars.inc.php +++ /dev/null @@ -1,68 +0,0 @@ -getAllById('', $snapId, ''); - -/** - * Define snapshot path - */ -if ($repoController->getPackageType() == 'rpm') { - $snapshotPath = REPOS_DIR . '/rpm/' . $repoController->getName() . '/' . $repoController->getReleasever() . '/' . $repoController->getDate(); -} - -if ($repoController->getPackageType() == 'deb') { - $snapshotPath = REPOS_DIR . '/deb/' . $repoController->getName() . '/' . $repoController->getDist() . '/' . $repoController->getSection() . '/' . $repoController->getDate(); -} - -/** - * If the path does not exist on the server then we quit - * browse/list container should have already throwed an error - */ -if (!is_dir($snapshotPath)) { - throw new Exception(); -} - -/** - * Retrieve repo rebuild status - */ -$rebuild = $repoController->getRebuild(); - -/** - * Upload packages - */ -if (!empty($_POST['action']) and $_POST['action'] == 'uploadPackage' and !empty($_POST['snapId']) and is_numeric($_POST['snapId']) and !empty($_FILES['packages'])) { - try { - if (isset($_POST['overwrite']) && is_null($overwrite = Convert::toBool($_POST['overwrite']))) { - throw new Exception('Invalid overwrite value'); - } - - $repoPackageController->upload(Sort::byPostFiles($_FILES['packages']), $overwrite ?? false); - $uploadSuccessMessage = 'Packages uploaded successfully'; - } catch (AppException $e) { - $uploadErrorDetails = $e->getDetails(); - } catch (Exception $e) { - $uploadErrorMessage = $e->getMessage(); - } - - unset($repoPackageController); -} diff --git a/www/controllers/Layout/Container/vars/browse/list.vars.inc.php b/www/controllers/Layout/Container/vars/browse/list.vars.inc.php deleted file mode 100644 index 5c63fc9b0..000000000 --- a/www/controllers/Layout/Container/vars/browse/list.vars.inc.php +++ /dev/null @@ -1,60 +0,0 @@ -exists($snapId)) { - throw new Exception('Error: repository snapshot #' . $snapId . ' does not exist.'); -} - -/** - * Retrieve repo infos from DB - */ -$myrepo->getAllById('', $snapId, ''); - -/** - * Define snapshot path - */ -if ($myrepo->getPackageType() == 'rpm') { - $snapshotPath = REPOS_DIR . '/rpm/' . $myrepo->getName() . '/' . $myrepo->getReleasever() . '/' . $myrepo->getDate(); -} - -if ($myrepo->getPackageType() == 'deb') { - $snapshotPath = REPOS_DIR . '/deb/' . $myrepo->getName() . '/' . $myrepo->getDist() . '/' . $myrepo->getSection() . '/' . $myrepo->getDate(); -} - -/** - * If the path does not exist on the server then we quit - */ -if (!is_dir($snapshotPath)) { - throw new Exception('Error: repo directory ' . $snapshotPath . ' does not exist.'); -} - -/** - * Retrieve repo size and packages count - */ -if ($myrepo->getPackageType() == 'rpm') { - $packagesCount = count(\Controllers\Filesystem\File::findRecursive($snapshotPath, [], ['rpm'])); -} -if ($myrepo->getPackageType() == 'deb') { - $packagesCount = count(\Controllers\Filesystem\File::findRecursive($snapshotPath, [], ['deb'])); -} - -/** - * Get the size of the snapshot - */ -$repoSize = $repoSnapshotController->getSize($snapId, true) ?? 'Unknown'; - -unset($repoSnapshotController); diff --git a/www/controllers/Layout/Container/vars/header/menu.vars.inc.php b/www/controllers/Layout/Container/vars/header/menu.vars.inc.php index 3bc13241c..ab7111e8c 100644 --- a/www/controllers/Layout/Container/vars/header/menu.vars.inc.php +++ b/www/controllers/Layout/Container/vars/header/menu.vars.inc.php @@ -1,13 +1,9 @@ getRunning(); -/** - * Count running tasks - */ +// Count running tasks $totalRunningTasks = count($tasksRunning); diff --git a/www/controllers/Layout/Container/vars/host/packages.vars.inc.php b/www/controllers/Layout/Container/vars/host/packages.vars.inc.php index 2e6747577..b2f8c0ffb 100644 --- a/www/controllers/Layout/Container/vars/host/packages.vars.inc.php +++ b/www/controllers/Layout/Container/vars/host/packages.vars.inc.php @@ -13,7 +13,7 @@ $complianceThresholdDays = $settings['compliance_threshold_days']; // Get installed packages and its total -$packagesInventored = $hostPackageController->getInventory(); +$packagesInventoredTotal = $hostPackageController->countInventory(); $packagesInstalledCount = count($hostPackageController->getInstalled()); // Get available packages and its total diff --git a/www/controllers/Layout/Container/vars/hosts/overview.vars.inc.php b/www/controllers/Layout/Container/vars/hosts/overview.vars.inc.php index 6780808f8..ff1e6c37c 100644 --- a/www/controllers/Layout/Container/vars/hosts/overview.vars.inc.php +++ b/www/controllers/Layout/Container/vars/hosts/overview.vars.inc.php @@ -1,26 +1,18 @@ get()); -/** - * Getting a list of all hosts kernel - */ +// Getting a list of all hosts kernel $kernels = $hostListingController->getKernel(); array_multisort(array_column($kernels, 'Count'), SORT_DESC, $kernels); -/** - * Getting a list of all hosts profiles - */ +// Getting a list of all hosts profiles $profiles = $hostListingController->getProfile(); array_multisort(array_column($profiles, 'Count'), SORT_DESC, $profiles); -/** - * Getting a list of all hosts requiring a reboot - */ +// Getting a list of all hosts requiring a reboot $rebootRequiredList = $hostListingController->getRebootRequired(); $rebootRequiredCount = count($rebootRequiredList); diff --git a/www/controllers/Layout/Container/vars/stats/repo.vars.inc.php b/www/controllers/Layout/Container/vars/repo/kpi.vars.inc.php similarity index 78% rename from www/controllers/Layout/Container/vars/stats/repo.vars.inc.php rename to www/controllers/Layout/Container/vars/repo/kpi.vars.inc.php index d916a92a4..c4575451d 100644 --- a/www/controllers/Layout/Container/vars/stats/repo.vars.inc.php +++ b/www/controllers/Layout/Container/vars/repo/kpi.vars.inc.php @@ -3,16 +3,16 @@ $debRepoStatController = new \Controllers\Repo\Statistic\Deb(); $rpmRepoStatController = new \Controllers\Repo\Statistic\Rpm(); -if (empty(__ACTUAL_URI__[3])) { - throw new Exception('Error: missing repository ID.'); +if (empty(__ACTUAL_URI__[2])) { + throw new Exception('no repository ID specified.'); } -if (!is_numeric(__ACTUAL_URI__[3])) { - throw new Exception('Error: invalid repository ID specified.'); +if (!is_numeric(__ACTUAL_URI__[2])) { + throw new Exception('invalid repository ID specified.'); } // Get repository info -$repoController->getAllById(__ACTUAL_URI__[3]); +$repoController->getAllById(__ACTUAL_URI__[2]); // Get all snapshots of the repository $snapshots = $repoController->getSnapshots($repoController->getRepoId()); diff --git a/www/controllers/Layout/Container/vars/repos/kpi.vars.inc.php b/www/controllers/Layout/Container/vars/repos/kpi.vars.inc.php new file mode 100644 index 000000000..0e25c0ac7 --- /dev/null +++ b/www/controllers/Layout/Container/vars/repos/kpi.vars.inc.php @@ -0,0 +1,45 @@ +count(); + +// Get used and free disk space in bytes, and also in percent +$diskTotalSpace = disk_total_space(REPOS_DIR); +$diskFreeSpace = disk_free_space(REPOS_DIR); +$diskUsedSpace = $diskTotalSpace - $diskFreeSpace; +$diskFreeSpaceHuman = Convert::sizeToHuman($diskFreeSpace); +$diskUsedSpaceHuman = Convert::sizeToHuman($diskUsedSpace); +$diskUsedSpacePercent = round(($diskUsedSpace / $diskTotalSpace) * 100); +$diskFreeSpacePercent = round(($diskFreeSpace / $diskTotalSpace) * 100); + +// Get last and next scheduled tasks +$lastScheduledTask = $taskController->getLastScheduledTask(); +$nextScheduledTasks = $taskController->getNextScheduledTask(); +$lastScheduledTaskSummary = []; + +// If the last scheduled task dispatched sub-tasks, its real status is a summary of their statuses +if (!empty($lastScheduledTask['Id'])) { + $lastScheduledTaskSummary = (new \Controllers\Task\Listing())->getSubTasksSummary($lastScheduledTask['Id']); +} + +if (!empty($nextScheduledTasks)) { + $data = []; + + foreach ($nextScheduledTasks as $task) { + $data[] = $taskController->getDayTimeLeft($task['Id']); + } + + $nextScheduledTasks = $data; + + // Sort tasks by date and time + array_multisort(array_column($nextScheduledTasks, 'date'), SORT_ASC, array_column($nextScheduledTasks, 'time'), SORT_ASC, $nextScheduledTasks); + + // Remove duplicates (when multiple tasks are scheduled for the same date and time) + $nextScheduledTasks = array_unique($nextScheduledTasks, SORT_REGULAR); +} + +unset($repoController, $taskController, $diskTotalSpace, $diskFreeSpace, $diskUsedSpace, $data); diff --git a/www/controllers/Layout/Container/vars/repos/list.vars.inc.php b/www/controllers/Layout/Container/vars/repos/list.vars.inc.php index 6c618f2ce..bbaf95c89 100644 --- a/www/controllers/Layout/Container/vars/repos/list.vars.inc.php +++ b/www/controllers/Layout/Container/vars/repos/list.vars.inc.php @@ -1,43 +1,64 @@ get($_SESSION['id']); + +// Get protected environments +$protectedEnvs = $envController->getProtected(); // Retrieve all group names $groupsList = $groupController->listAll(true); -// Get total repos count -$totalRepos = $repoController->count(); +// Define if the user can view all repositories or only specific groups and repositories +$viewAll = IS_ADMIN || in_array('all', USER_PERMISSIONS['repositories']['view']); +$allowedGroups = $viewAll ? [] : USER_PERMISSIONS['repositories']['view']['groups']; +$allowedRepos = $viewAll ? [] : USER_PERMISSIONS['repositories']['view']['repos']; -// Get used and free disk space in bytes, and also in percent -$diskTotalSpace = disk_total_space(REPOS_DIR); -$diskFreeSpace = disk_free_space(REPOS_DIR); -$diskUsedSpace = $diskTotalSpace - $diskFreeSpace; -$diskFreeSpaceHuman = \Controllers\Utils\Convert::sizeToHuman($diskFreeSpace); -$diskUsedSpaceHuman = \Controllers\Utils\Convert::sizeToHuman($diskUsedSpace); -$diskUsedSpacePercent = round(($diskUsedSpace / $diskTotalSpace) * 100); -$diskFreeSpacePercent = round(($diskFreeSpace / $diskTotalSpace) * 100); +// Loop through groups to get repos for each group and add them to the $groups array +foreach ($groupsList as $group) { + // Group is allowed if the user can view all repositories or if the group is in the allowed groups list + $groupAllowed = $viewAll || in_array($group['Id'], $allowedGroups); + $groupRepos = []; -// Get last and next scheduled tasks -$lastScheduledTask = $taskController->getLastScheduledTask(); -$nextScheduledTasks = $taskController->getNextScheduledTask(); + foreach ($repoListingController->listByGroup($group['Name']) as $repo) { + // A repository granted individually makes its group visible, without revealing the other repositories of that group + if (!$groupAllowed and !in_array($repo['repoId'], $allowedRepos)) { + continue; + } -if (!empty($nextScheduledTasks)) { - $data = []; + // Add the whole repo to the repos array + $repos[] = $repo; - foreach ($nextScheduledTasks as $task) { - $data[] = $taskController->getDayTimeLeft($task['Id']); + // Add the repoId to the group if not already in the group repos (to avoid duplicate repos in the same group) + if (!in_array($repo['repoId'], $groupRepos)) { + $groupRepos[] = $repo['repoId']; + } } - $nextScheduledTasks = $data; + // If the group is allowed or if it has at least one repository that the user can view, add it to the visible groups array + if ($groupAllowed || !empty($groupRepos)) { + // Do not display the 'Default' group if it is empty, unless it is the only group + if ($group['Name'] == 'Default' and count($groupRepos) == 0 and count($groupsList) > 1) { + continue; + } - // Sort tasks by date and time - array_multisort(array_column($nextScheduledTasks, 'date'), SORT_ASC, array_column($nextScheduledTasks, 'time'), SORT_ASC, $nextScheduledTasks); - - // Remove duplicates (when multiple tasks are scheduled for the same date and time) - $nextScheduledTasks = array_unique($nextScheduledTasks, SORT_REGULAR); + // Add the group to the visible groups array with its name, repositories, and count of repositories + $groups[$group['Id']] = [ + 'name' => $group['Name'], + 'repos' => $groupRepos, + 'count' => count($groupRepos) + ]; + } } -unset($groupController, $repoController, $taskController, $diskTotalSpace, $diskFreeSpace, $diskUsedSpace, $data); +unset($userPreferenceController, $groupController, $repoController, $envController); diff --git a/www/controllers/Layout/Container/vars/snapshot/kpi.vars.inc.php b/www/controllers/Layout/Container/vars/snapshot/kpi.vars.inc.php new file mode 100644 index 000000000..947ea750c --- /dev/null +++ b/www/controllers/Layout/Container/vars/snapshot/kpi.vars.inc.php @@ -0,0 +1,59 @@ +exists($snapId)) { + throw new Exception('repository snapshot #' . $snapId . ' does not exist.'); +} + +// Retrieve repository infos from DB +$repoController->getAllById(null, $snapId, null); + +// Define repository name +if ($repoController->getPackageType() == 'rpm') { + $repository = $repoController->getName() . ' ❯ ' . $repoController->getReleasever(); +} +if ($repoController->getPackageType() == 'deb') { + $repository = $repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection(); +} + +// Define snapshot path +if ($repoController->getPackageType() == 'rpm') { + $snapshotPath = REPOS_DIR . '/rpm/' . $repoController->getName() . '/' . $repoController->getReleasever() . '/' . $repoController->getDate(); +} +if ($repoController->getPackageType() == 'deb') { + $snapshotPath = REPOS_DIR . '/deb/' . $repoController->getName() . '/' . $repoController->getDist() . '/' . $repoController->getSection() . '/' . $repoController->getDate(); +} + +// Retrieve repo size and packages count +if ($repoController->getPackageType() == 'rpm') { + $repoSize = Directory::getSize($snapshotPath); + $packagesCount = count(File::findRecursive($snapshotPath, [], ['rpm'])); +} +if ($repoController->getPackageType() == 'deb') { + $repoSize = Directory::getSize($snapshotPath); + $packagesCount = count(File::findRecursive($snapshotPath, [], ['deb'])); +} + +// Convert repo size in the most suitable byte format +$repoSize = Convert::sizeToHuman($repoSize); + +// Retrieve snapshot rebuild status +$rebuild = $repoController->getRebuild(); + +unset($repoSnapshotController); diff --git a/www/controllers/Layout/Container/vars/snapshot/list.vars.inc.php b/www/controllers/Layout/Container/vars/snapshot/list.vars.inc.php new file mode 100644 index 000000000..bc3366d1d --- /dev/null +++ b/www/controllers/Layout/Container/vars/snapshot/list.vars.inc.php @@ -0,0 +1,60 @@ +exists($snapId)) { + throw new Exception('repository snapshot #' . $snapId . ' does not exist.'); +} + +// Check if the snapshot has a protected environment +$protectedEnv = $repoSnapshotController->hasProtectedEnv($snapId); + +// Check if a task is running for this snapshot +$taskRunning = $repoSnapshotController->taskRunning($snapId); + +// Retrieve repo infos from DB +$repoController->getAllById('', $snapId, ''); + +// Define snapshot path +if ($repoController->getPackageType() == 'rpm') { + $snapshotPath = REPOS_DIR . '/rpm/' . $repoController->getName() . '/' . $repoController->getReleasever() . '/' . $repoController->getDate(); +} + +if ($repoController->getPackageType() == 'deb') { + $snapshotPath = REPOS_DIR . '/deb/' . $repoController->getName() . '/' . $repoController->getDist() . '/' . $repoController->getSection() . '/' . $repoController->getDate(); +} + +// If the path does not exist on the server then we quit +if (!is_dir($snapshotPath)) { + throw new Exception('snapshot directory ' . $snapshotPath . ' does not exist.'); +} + +// Retrieve repo size and packages count +if ($repoController->getPackageType() == 'rpm') { + $repoSize = Directory::getSize($snapshotPath); + $packagesCount = count(File::findRecursive($snapshotPath, [], ['rpm'])); +} +if ($repoController->getPackageType() == 'deb') { + $repoSize = Directory::getSize($snapshotPath); + $packagesCount = count(File::findRecursive($snapshotPath, [], ['deb'])); +} + +// Convert repo size in the most suitable byte format +$repoSize = Convert::sizeToHuman($repoSize); + +unset($repoController, $repoSnapshotController); diff --git a/www/controllers/Layout/Container/vars/tasks/log.vars.inc.php b/www/controllers/Layout/Container/vars/tasks/log.vars.inc.php index 0ae7ab669..d2b0ddcf5 100644 --- a/www/controllers/Layout/Container/vars/tasks/log.vars.inc.php +++ b/www/controllers/Layout/Container/vars/tasks/log.vars.inc.php @@ -1,12 +1,14 @@ exists($taskId)) { - throw new Exception('Task #' . $taskId . ' not found.'); + throw new Exception('Task #' . $taskId . ' not found'); } // Get task info @@ -51,106 +53,115 @@ $repoId = null; $snapId = null; - $envId = null; // Get log content $content = $taskLogController->getContent(); // If log content is empty if (empty($content)) { - throw new Exception('No log found for this task.'); - } - - if (empty($rawParams['action'])) { - throw new Exception('No action found in the task.'); - } - - /** - * Get repository info - */ - - // If the action is create, the repository info is in the raw params - if ($rawParams['action'] == 'create') { - $repoController->setType($rawParams['repo-type']); + /** + * The task itself may have no log if it only dispatched sub-tasks (e.g. a scheduled + * task targeting a group of repositories). In that case, display the list of + * sub-tasks it launched instead of failing. + */ + $childTasks = $taskListingController->getByParentId($taskId); + + if (empty($childTasks)) { + // A scheduled task has no log until it has been executed at least once + if (in_array($taskInfo['Status'], ['scheduled', 'disabled'])) { + throw new Exception('This task has not been executed yet'); + } - if (!empty($rawParams['source'])) { - $repoController->setSource($rawParams['source']); - } - if (!empty($rawParams['alias'])) { - $repoController->setName($rawParams['alias']); - } else { - $repoController->setName($rawParams['source']); - } - if (!empty($rawParams['dist'])) { - $repoController->setDist($rawParams['dist']); - } - if (!empty($rawParams['section'])) { - $repoController->setSection($rawParams['section']); - } - if (!empty($rawParams['releasever'])) { - $repoController->setReleasever($rawParams['releasever']); - } - if (!empty($rawParams['arch'])) { - $repoController->setArch($rawParams['arch']); - } - if (!empty($rawParams['package-type'])) { - $repoController->setPackageType($rawParams['package-type']); - } - if (!empty($rawParams['gpg-check'])) { - $repoController->setGpgCheck($rawParams['gpg-check']); - } - if (!empty($rawParams['gpg-sign'])) { - $repoController->setGpgSign($rawParams['gpg-sign']); - } - if (!empty($rawParams['description'])) { - $repoController->setDescription($rawParams['description']); - } - if (!empty($rawParams['group'])) { - $repoController->setGroup($rawParams['group']); + throw new Exception('No log found for this task'); } - // Otherwise, we get the repository info from the database + // Used to display the real status of the parent task, based on its sub-tasks status + $subTasksSummary = $taskListingController->getSubTasksSummary($taskId); + + ob_start(); + TableRender::render('tasks/list-children'); + $output .= ob_get_clean(); } else { - if (!empty($rawParams['repo-id'])) { - $repoId = $rawParams['repo-id']; + if (empty($rawParams['action'])) { + throw new Exception('No action found in the task'); } - if (!empty($rawParams['snap-id'])) { - $snapId = $rawParams['snap-id']; - } - // If the action is removeEnv, the environment has been removed, so it - if ($rawParams['action'] != 'removeEnv') { - if (!empty($rawParams['env-id'])) { - $envId = $rawParams['env-id']; + + // If the action is create, the repository info is in the raw params + if ($rawParams['action'] == 'create') { + $repoController->setType($rawParams['repo-type']); + + if (!empty($rawParams['source'])) { + $repoController->setSource($rawParams['source']); + } + if (!empty($rawParams['alias'])) { + $repoController->setName($rawParams['alias']); + } else { + $repoController->setName($rawParams['source']); + } + if (!empty($rawParams['dist'])) { + $repoController->setDist($rawParams['dist']); + } + if (!empty($rawParams['section'])) { + $repoController->setSection($rawParams['section']); + } + if (!empty($rawParams['releasever'])) { + $repoController->setReleasever($rawParams['releasever']); + } + if (!empty($rawParams['arch'])) { + $repoController->setArch($rawParams['arch']); + } + if (!empty($rawParams['package-type'])) { + $repoController->setPackageType($rawParams['package-type']); + } + if (!empty($rawParams['gpg-check'])) { + $repoController->setGpgCheck($rawParams['gpg-check']); + } + if (!empty($rawParams['gpg-sign'])) { + $repoController->setGpgSign($rawParams['gpg-sign']); + } + if (!empty($rawParams['description'])) { + $repoController->setDescription($rawParams['description']); + } + if (!empty($rawParams['tags'])) { + $repoController->setTags($rawParams['tags']); + } + if (!empty($rawParams['group'])) { + $repoController->setGroup($rawParams['group']); } - } - $repoController->getAllById($repoId, $snapId, $envId); - } + // Otherwise, we get the repository info from the database + } else { + if (!empty($rawParams['repo-id'])) { + $repoId = $rawParams['repo-id']; + } + if (!empty($rawParams['snap-id'])) { + $snapId = $rawParams['snap-id']; + } + + $repoController->getAllById($repoId, $snapId); + } - /** - * Include table template for the task - */ - ob_start(); - include_once(ROOT . '/views/templates/tasks/' . $rawParams['action'] . '.inc.php'); - $output .= ob_get_clean(); - - /** - * The container which will contain all the steps - */ - $output .= '
'; - - /** - * Include steps - */ - foreach ($content['steps'] as $stepIdentifier => $step) { + // Include table template for the task ob_start(); - include(ROOT . '/views/includes/containers/tasks/log/step.inc.php'); - include(ROOT . '/views/includes/containers/tasks/log/step-content.inc.php'); + include_once(ROOT . '/views/templates/tasks/' . $rawParams['action'] . '.inc.php'); $output .= ob_get_clean(); - } - $output .= '
'; + // The container which will contain all the steps + $output .= '
'; + + // Include steps + foreach ($content['steps'] as $stepIdentifier => $step) { + ob_start(); + include(ROOT . '/views/includes/containers/tasks/log/step.inc.php'); + include(ROOT . '/views/includes/containers/tasks/log/step-content.inc.php'); + $output .= ob_get_clean(); + } + + $output .= '
'; + } } } catch (Exception $e) { - $output = '

' . $e->getMessage() . '

'; + $taskFound = false; + $taskErrorMessage = $e->getMessage(); + $output = '

' . $taskErrorMessage . '

'; } diff --git a/www/controllers/Layout/Container/vars/tasks/tasks.vars.inc.php b/www/controllers/Layout/Container/vars/tasks/tasks.vars.inc.php new file mode 100644 index 000000000..9c6f3507f --- /dev/null +++ b/www/controllers/Layout/Container/vars/tasks/tasks.vars.inc.php @@ -0,0 +1,13 @@ +get()); + +// Get running tasks +$runningCount = count($taskListingController->getRunning()); + +// Get scheduled tasks +$scheduledCount = count($taskListingController->getScheduled()); + +unset($taskListingController); diff --git a/www/controllers/Layout/ContainerReload.php b/www/controllers/Layout/ContainerReload.php index 20069a77e..6dfe1385e 100644 --- a/www/controllers/Layout/ContainerReload.php +++ b/www/controllers/Layout/ContainerReload.php @@ -42,6 +42,14 @@ public function exists(string $name) return $this->model->exists($name); } + /** + * Delete the specified containers entries + */ + public function delete(array $names): void + { + $this->model->delete($names); + } + /** * Clean all containers entries */ diff --git a/www/controllers/Layout/Panel/vars/hosts/settings.vars.inc.php b/www/controllers/Layout/Panel/vars/hosts/settings.vars.inc.php index d33366754..e910c750d 100644 --- a/www/controllers/Layout/Panel/vars/hosts/settings.vars.inc.php +++ b/www/controllers/Layout/Panel/vars/hosts/settings.vars.inc.php @@ -17,6 +17,9 @@ // Get compliance threshold days $complianceThresholdDays = $settings['compliance_threshold_days']; +// Get compliance security update +$complianceSecurityUpdate = $settings['compliance_security_update']; + // Get compliance reboot required $complianceRebootRequired = $settings['compliance_reboot_required']; diff --git a/www/controllers/Layout/Panel/vars/repos/edit.vars.inc.php b/www/controllers/Layout/Panel/vars/repos/edit.vars.inc.php index a429abbd0..b09ef5662 100644 --- a/www/controllers/Layout/Panel/vars/repos/edit.vars.inc.php +++ b/www/controllers/Layout/Panel/vars/repos/edit.vars.inc.php @@ -14,7 +14,7 @@ * Check that action and repos params have been sent */ if (empty($item['repos'])) { - throw new Exception('Task repositories required'); + throw new Exception('Repositories required'); } $slidePanelTitle = 'EDIT REPOSITORY & SNAPSHOT PROPERTIES'; diff --git a/www/controllers/Layout/Panel/vars/repos/install.vars.inc.php b/www/controllers/Layout/Panel/vars/repos/install.vars.inc.php index 4ade1b5bc..235325404 100644 --- a/www/controllers/Layout/Panel/vars/repos/install.vars.inc.php +++ b/www/controllers/Layout/Panel/vars/repos/install.vars.inc.php @@ -45,8 +45,8 @@ $packageType = $repoController->getPackageType(); $packagesTypes[] = $packageType; ?> -
REPOSITORY
-
+
+
-

getDateFormatted() ?>

+

getDateFormatted() ?>

- -
INSTALLATION
@@ -95,11 +93,10 @@ if ($packageType == 'rpm') : ?>

         
 
-    if ($repo !== end($repos)) {
-        echo '
'; - } +
+ listAll('rpm'); -$newRepoDebSourcesList = $mysource->listAll('deb'); -$newRepoFormGroupList = $mygroup->listAll(); +// New repo form variables +$rpmSourcesList = $sourceController->listAll('rpm'); +$debSourcesList = $sourceController->listAll('deb'); +// Retrieve the groups, the tags and the recipients to build the target selectors +$groups = $groupController->listAll(); +$tags = $repoListingController->listTags(); $usersEmail = $userController->getEmails(); -unset($userController); +unset($targetController, $groupController, $repoListingController, $userController, $sourceController); diff --git a/www/controllers/Layout/Panel/vars/repos/rename.vars.inc.php b/www/controllers/Layout/Panel/vars/repos/rename.vars.inc.php new file mode 100644 index 000000000..646c0499a --- /dev/null +++ b/www/controllers/Layout/Panel/vars/repos/rename.vars.inc.php @@ -0,0 +1,24 @@ +get('rename', json_decode($item['repos'], true)); +} catch (JsonException $e) { + throw new Exception('Error while retrieving the form content: ' . $e->getMessage()); +} diff --git a/www/controllers/Layout/Panel/vars/repos/scheduled-task.vars.inc.php b/www/controllers/Layout/Panel/vars/repos/scheduled-task.vars.inc.php new file mode 100644 index 000000000..ac624f00a --- /dev/null +++ b/www/controllers/Layout/Panel/vars/repos/scheduled-task.vars.inc.php @@ -0,0 +1,38 @@ +getValidActions() as $action) { + if (RepoPermission::allowedAction($action)) { + $allowedActions[] = $action; + } +} + +if (empty($allowedActions)) { + throw new Exception('You are not allowed to access this panel'); +} + +// Human readable title for each action +$actionsTitles = [ + 'update' => 'Update repositories', + 'env' => 'Point an environment', + 'rebuild' => 'Rebuild repositories metadata' +]; + +// Retrieve the groups, the tags and the recipients to build the target selectors +$groups = $groupController->listAll(true); +$tags = $repoListingController->listTags(); +$usersEmail = $userController->getEmails(); + +unset($targetController, $groupController, $repoListingController, $userController); diff --git a/www/controllers/Layout/Panel/vars/repos/settings.vars.inc.php b/www/controllers/Layout/Panel/vars/repos/settings.vars.inc.php new file mode 100644 index 000000000..67dbb542d --- /dev/null +++ b/www/controllers/Layout/Panel/vars/repos/settings.vars.inc.php @@ -0,0 +1,12 @@ +get($_SESSION['id']); + +unset($userPreferenceController); diff --git a/www/controllers/Layout/Panel/vars/settings/user/permissions.vars.inc.php b/www/controllers/Layout/Panel/vars/settings/user/permissions.vars.inc.php index 7eeacd0d5..49c9e4c2c 100644 --- a/www/controllers/Layout/Panel/vars/settings/user/permissions.vars.inc.php +++ b/www/controllers/Layout/Panel/vars/settings/user/permissions.vars.inc.php @@ -1,6 +1,7 @@ get($userId); -/** - * Get repositories groups - */ +// Get repositories groups $groupsList = $groupController->listAll(true); + +// Get repositories, deduplicated by repository Id as the listing returns one row per snapshot and environment +$reposList = []; + +foreach ($repoListingController->list() as $repo) { + if (isset($reposList[$repo['repoId']])) { + continue; + } + + $label = $repo['Name']; + + if ($repo['Package_type'] == 'deb') { + $label .= ' ❯ ' . $repo['Dist'] . ' ❯ ' . $repo['Section']; + } + + if ($repo['Package_type'] == 'rpm' and !empty($repo['Releasever'])) { + $label .= ' ❯ ' . $repo['Releasever']; + } + + $reposList[$repo['repoId']] = $label; +} diff --git a/www/controllers/Layout/Panel/vars/snapshot/rebuild.vars.inc.php b/www/controllers/Layout/Panel/vars/snapshot/rebuild.vars.inc.php new file mode 100644 index 000000000..0792cb0ad --- /dev/null +++ b/www/controllers/Layout/Panel/vars/snapshot/rebuild.vars.inc.php @@ -0,0 +1,38 @@ +getAllById(null, $snapId); + +// Retrieve snapshot rebuild status +$rebuild = $repoController->getRebuild(); + +if (!empty($rebuild) and $rebuild == 'running') { + throw new Exception('Snapshot metadata rebuild is currently running. You cannot rebuild metadata at this time.'); +} + +// Define if the GPG signature checkbox should be checked by default +if ($repoController->getPackageType() == 'rpm' && RPM_SIGN_PACKAGES == 'true') { + $gpgSignChecked = 'checked'; +} +if ($repoController->getPackageType() == 'deb' && DEB_SIGN_REPO == 'true') { + $gpgSignChecked = 'checked'; +} diff --git a/www/controllers/Layout/Panel/vars/snapshot/upload.vars.inc.php b/www/controllers/Layout/Panel/vars/snapshot/upload.vars.inc.php new file mode 100644 index 000000000..e657e6992 --- /dev/null +++ b/www/controllers/Layout/Panel/vars/snapshot/upload.vars.inc.php @@ -0,0 +1,47 @@ +getAllById(null, $snapId); + +// Retrieve snapshot rebuild status +$rebuild = $repoController->getRebuild(); + +// Define snapshot path +if ($repoController->getPackageType() == 'rpm') { + $snapshotPath = REPOS_DIR . '/rpm/' . $repoController->getName() . '/' . $repoController->getReleasever() . '/' . $repoController->getDate(); +} + +if ($repoController->getPackageType() == 'deb') { + $snapshotPath = REPOS_DIR . '/deb/' . $repoController->getName() . '/' . $repoController->getDist() . '/' . $repoController->getSection() . '/' . $repoController->getDate(); +} + +// Check if the snapshot path exists +if (!is_dir($snapshotPath)) { + throw new Exception('Snapshot path does not exist on the server'); +} + +// If a task is already running on this repo then print a message +if (!empty($rebuild) and $rebuild == 'running') { + throw new Exception('A task is running on this repository snapshot. You cannot upload packages at this time.'); +} diff --git a/www/controllers/Layout/Panel/vars/tasks/edit.vars.inc.php b/www/controllers/Layout/Panel/vars/tasks/edit.vars.inc.php index d1a221997..f5f6cb637 100644 --- a/www/controllers/Layout/Panel/vars/tasks/edit.vars.inc.php +++ b/www/controllers/Layout/Panel/vars/tasks/edit.vars.inc.php @@ -1,9 +1,11 @@ 1) { - $output .= ''; + $output .= ''; } // First page button (n°1) @@ -108,7 +108,7 @@ public static function paginationBtn($currentPage, $totalPages): void // Next button if ($currentPage < $totalPages) { - $output .= ''; + $output .= ''; } echo $output; diff --git a/www/controllers/Layout/Table/vars/host/installed-packages.vars.inc.php b/www/controllers/Layout/Table/vars/host/installed-packages.vars.inc.php new file mode 100644 index 000000000..be881cb92 --- /dev/null +++ b/www/controllers/Layout/Table/vars/host/installed-packages.vars.inc.php @@ -0,0 +1,34 @@ +countInventory($search); + +// If offset is out of range, reset it to 0 +if ($reloadableTableOffset >= $reloadableTableTotalItems) { + $reloadableTableOffset = 0; +} + +// Get list of inventoried packages, with offset and optional search filter +$reloadableTableContent = $hostPackageController->getInventory($search, true, $reloadableTableOffset); + +// Count total pages for the pagination +$reloadableTableTotalPages = (int) ceil($reloadableTableTotalItems / 10); + +// Calculate current page number +$reloadableTableCurrentPage = (int) ceil($reloadableTableOffset / 10) + 1; + +unset($hostPackageController); diff --git a/www/controllers/Layout/Table/vars/stats/access.vars.inc.php b/www/controllers/Layout/Table/vars/stats/access.vars.inc.php index 419945087..f6ca42fc7 100644 --- a/www/controllers/Layout/Table/vars/stats/access.vars.inc.php +++ b/www/controllers/Layout/Table/vars/stats/access.vars.inc.php @@ -7,20 +7,18 @@ $timeEnd = strtotime(DATE_YMD . ' 23:59:59'); $envs = []; -if (__ACTUAL_URI__[2] != 'repo') { - throw new Exception('Error: invalid URI specified.'); +// Check that repository Id is specified +if (empty(__ACTUAL_URI__[2])) { + throw new Exception('no repository ID specified'); } -if (empty(__ACTUAL_URI__[3])) { - throw new Exception('Error: missing repository ID.'); -} - -if (!is_numeric(__ACTUAL_URI__[3])) { - throw new Exception('Error: invalid repository ID specified.'); +// Check that repository Id is valid +if (!is_numeric(__ACTUAL_URI__[2])) { + throw new Exception('invalid repository ID'); } // Get repository info -$repoController->getAllById(__ACTUAL_URI__[3]); +$repoController->getAllById(__ACTUAL_URI__[2]); // Retrieve period from cookie if exists if (!empty($_COOKIE['tables/stats/access/period'])) { diff --git a/www/controllers/Layout/Table/vars/stats/ip-access.vars.inc.php b/www/controllers/Layout/Table/vars/stats/ip-access.vars.inc.php index 6fd256bf4..269ac6f2c 100644 --- a/www/controllers/Layout/Table/vars/stats/ip-access.vars.inc.php +++ b/www/controllers/Layout/Table/vars/stats/ip-access.vars.inc.php @@ -7,16 +7,18 @@ $timeEnd = strtotime(DATE_YMD . ' 23:59:59'); $envs = []; -if (empty(__ACTUAL_URI__[3])) { - throw new Exception('Error: missing repository ID.'); +// Check that repository Id is specified +if (empty(__ACTUAL_URI__[2])) { + throw new Exception('no repository ID specified'); } -if (!is_numeric(__ACTUAL_URI__[3])) { - throw new Exception('Error: invalid repository ID specified.'); +// Check that repository Id is valid +if (!is_numeric(__ACTUAL_URI__[2])) { + throw new Exception('invalid repository ID'); } // Get repository info -$repoController->getAllById(__ACTUAL_URI__[3]); +$repoController->getAllById(__ACTUAL_URI__[2]); // Retrieve period from cookie if exists if (!empty($_COOKIE['tables/stats/ip-access/period'])) { diff --git a/www/controllers/Layout/Table/vars/tasks/list-children.vars.inc.php b/www/controllers/Layout/Table/vars/tasks/list-children.vars.inc.php new file mode 100644 index 000000000..a330d4b0d --- /dev/null +++ b/www/controllers/Layout/Table/vars/tasks/list-children.vars.inc.php @@ -0,0 +1,24 @@ +getByParentId($parentTaskId, true, $reloadableTableOffset); + +// Get list of ALL sub-tasks, without offset, for the total count +$reloadableTableTotalItems = count($taskListingController->getByParentId($parentTaskId)); + +// Count total pages for the pagination +$reloadableTableTotalPages = ceil($reloadableTableTotalItems / 10); + +// Calculate current page number +$reloadableTableCurrentPage = ceil($reloadableTableOffset / 10) + 1; diff --git a/www/controllers/Layout/Table/vars/tasks/list-done.vars.inc.php b/www/controllers/Layout/Table/vars/tasks/list-done.vars.inc.php index c6635dc6c..e88d4c393 100644 --- a/www/controllers/Layout/Table/vars/tasks/list-done.vars.inc.php +++ b/www/controllers/Layout/Table/vars/tasks/list-done.vars.inc.php @@ -1,31 +1,21 @@ getDone('', true, $reloadableTableOffset); -/** - * Get list of ALL done tasks, without offset, for the total count - */ +// Get list of ALL done tasks, without offset, for the total count $reloadableTableTotalItems = count($taskListingController->getDone()); -/** - * Count total pages for the pagination - */ +// Count total pages for the pagination $reloadableTableTotalPages = ceil($reloadableTableTotalItems / 10); -/** - * Calculate current page number - */ +// Calculate current page number $reloadableTableCurrentPage = ceil($reloadableTableOffset / 10) + 1; diff --git a/www/controllers/Layout/Table/vars/tasks/list-queued.vars.inc.php b/www/controllers/Layout/Table/vars/tasks/list-queued.vars.inc.php index 99bb55784..8e6bce488 100644 --- a/www/controllers/Layout/Table/vars/tasks/list-queued.vars.inc.php +++ b/www/controllers/Layout/Table/vars/tasks/list-queued.vars.inc.php @@ -1,31 +1,21 @@ getQueued('', true, $reloadableTableOffset); -/** - * Get list of ALL queued tasks, without offset, for the total count - */ +// Get list of ALL queued tasks, without offset, for the total count $reloadableTableTotalItems = count($taskListingController->getQueued()); -/** - * Count total pages for the pagination - */ +// Count total pages for the pagination $reloadableTableTotalPages = ceil($reloadableTableTotalItems / 10); -/** - * Calculate current page number - */ +// Calculate current page number $reloadableTableCurrentPage = ceil($reloadableTableOffset / 10) + 1; diff --git a/www/controllers/Layout/Table/vars/tasks/list-running.vars.inc.php b/www/controllers/Layout/Table/vars/tasks/list-running.vars.inc.php index 65f9a5728..065290e3d 100644 --- a/www/controllers/Layout/Table/vars/tasks/list-running.vars.inc.php +++ b/www/controllers/Layout/Table/vars/tasks/list-running.vars.inc.php @@ -1,31 +1,21 @@ getRunning('', true, $reloadableTableOffset); -/** - * Get list of ALL running tasks, without offset, for the total count - */ +// Get list of ALL running tasks, without offset, for the total count $reloadableTableTotalItems = count($taskListingController->getRunning()); -/** - * Count total pages for the pagination - */ +// Count total pages for the pagination $reloadableTableTotalPages = ceil($reloadableTableTotalItems / 10); -/** - * Calculate current page number - */ +// Calculate current page number $reloadableTableCurrentPage = ceil($reloadableTableOffset / 10) + 1; diff --git a/www/controllers/Layout/Table/vars/tasks/list-scheduled.vars.inc.php b/www/controllers/Layout/Table/vars/tasks/list-scheduled.vars.inc.php index f08221dcb..181b928a8 100644 --- a/www/controllers/Layout/Table/vars/tasks/list-scheduled.vars.inc.php +++ b/www/controllers/Layout/Table/vars/tasks/list-scheduled.vars.inc.php @@ -1,31 +1,21 @@ getScheduled(true, $reloadableTableOffset); -/** - * Get list of ALL scheduled tasks, without offset, for the total count - */ +// Get list of ALL scheduled tasks, without offset, for the total count $reloadableTableTotalItems = count($taskListingController->getScheduled()); -/** - * Count total pages for the pagination - */ +// Count total pages for the pagination $reloadableTableTotalPages = ceil($reloadableTableTotalItems / 10); -/** - * Calculate current page number - */ +// Calculate current page number $reloadableTableCurrentPage = ceil($reloadableTableOffset / 10) + 1; diff --git a/www/controllers/Repo/Browse.php b/www/controllers/Repo/Browse.php index b52f280f9..214c1390b 100644 --- a/www/controllers/Repo/Browse.php +++ b/www/controllers/Repo/Browse.php @@ -209,7 +209,7 @@ private static function printSearchResultFiles(array $results): void
  • -
    +

    @@ -265,7 +265,7 @@ private static function printFiles(array $files): void
  • -
    +

    @@ -286,10 +286,10 @@ private static function printFiles(array $files): void */ private static function printSubDir(string $dir, string $path): void { - $fullPath = $path . '/' . $dir; - ?> + $fullPath = $path . '/' . $dir; ?> +
  • -
    +

    diff --git a/www/controllers/Repo/Deb.php b/www/controllers/Repo/Deb.php index f16885657..78f58c4b4 100644 --- a/www/controllers/Repo/Deb.php +++ b/www/controllers/Repo/Deb.php @@ -2,8 +2,6 @@ namespace Controllers\Repo; -use Exception; - class Deb extends \Controllers\Repo\Repo { public function __construct() @@ -20,14 +18,6 @@ public function getIdByNameDistComponent(string $name, string $distribution, str return $this->model->getIdByNameDistComponent($name, $distribution, $component); } - /** - * Return repository environment description - */ - public function getDescriptionByName(string $name, string $dist, string $component, string $env) : string|null - { - return $this->model->getDescriptionByName($name, $dist, $component, $env); - } - /** * Return environment Id from repo name */ diff --git a/www/controllers/Repo/Edit/Form.php b/www/controllers/Repo/Edit/Form.php index f43bca092..c59a4d3bb 100644 --- a/www/controllers/Repo/Edit/Form.php +++ b/www/controllers/Repo/Edit/Form.php @@ -64,7 +64,7 @@ public function get(array $repos) : string */ ob_start(); - echo '
    '; + echo '
    '; /** * Include form template @@ -73,8 +73,6 @@ public function get(array $repos) : string echo '
    '; - echo '

    '; - $content .= ob_get_clean(); } @@ -152,13 +150,6 @@ public function edit(array $params) : void $repoController->updateSource($param['repo-id'], $param['source']); } - /** - * Update snapshot description - * TODO: no editable description for now - */ - // $repoController->updateDescription($param['snap-id'], $description); - // $snapshotController->updateDescription($param['snap-id'], $description); - /** * Add history */ diff --git a/www/controllers/Repo/Environment.php b/www/controllers/Repo/Environment.php index f99a3f937..8a95806fa 100644 --- a/www/controllers/Repo/Environment.php +++ b/www/controllers/Repo/Environment.php @@ -2,8 +2,7 @@ namespace Controllers\Repo; -use Exception; -use Controllers\Utils\Validate; +use \Controllers\Environment as GlobalEnvironment; class Environment { @@ -15,19 +14,19 @@ public function __construct() } /** - * Return all environments associated to a repository ID + * Return all environments associated to a snapshot Id */ - public function getByRepoId(int $repoId): array + public function getBySnapId(int $snapId): array { - return $this->model->getByRepoId($repoId); + return $this->model->getBySnapId($snapId); } /** * Associate a new environment to a snapshot */ - public function add(int $snapId, string $env, string $description = '') : void + public function add(int $snapId, string $env) : void { - $this->model->add($snapId, $env, $description); + $this->model->add($snapId, $env); } /** @@ -38,19 +37,6 @@ public function remove(int $id) : void $this->model->remove($id); } - /** - * Update environment description - */ - public function updateDescription(int $id, string $description) : void - { - // Description should not contain single quotes or backslashes - if (str_contains($description, "'") || str_contains($description, "\\") || str_contains($description, '')) { - throw new Exception('Description contains invalid characters'); - } - - $this->model->updateDescription($id, Validate::string($description)); - } - /** * Return true if the repository environment Id exists */ diff --git a/www/controllers/Repo/Listing.php b/www/controllers/Repo/Listing.php index e1101766f..84a195cb4 100644 --- a/www/controllers/Repo/Listing.php +++ b/www/controllers/Repo/Listing.php @@ -46,4 +46,20 @@ public function listSnapshots(int $repoId) : array { return $this->model->listSnapshots($repoId); } + + /** + * Return the list of repositories matching the specified filters, with their latest active snapshot Id + */ + public function listByTarget(string $groupId = '', array $tags = [], string $packageType = '') : array + { + return $this->model->listByTarget($groupId, $tags, $packageType); + } + + /** + * Return the list of all distinct tags used by repositories + */ + public function listTags(): array + { + return $this->model->listTags(); + } } diff --git a/www/controllers/Repo/Package/Sync.php b/www/controllers/Repo/Package/Sync.php index bad8438bf..e3b8bb086 100644 --- a/www/controllers/Repo/Package/Sync.php +++ b/www/controllers/Repo/Package/Sync.php @@ -58,12 +58,12 @@ private function syncPackage() if ($this->repoSnapshotController->getDateById($this->repoController->getSnapId()) != $this->repoController->getDate()) { if ($this->repoController->getPackageType() == 'rpm') { if ($this->rpmRepoController->existsSnapDate($this->repoController->getName(), $this->repoController->getReleasever(), $this->repoController->getDate())) { - throw new Exception('A snapshot already exists on the ' . Label::black($this->repoController->getDateFormatted())); + throw new Exception('A snapshot already exists on the ' . Label::white($this->repoController->getDateFormatted())); } } if ($this->repoController->getPackageType() == 'deb') { if ($this->debRepoController->existsSnapDate($this->repoController->getName(), $this->repoController->getDist(), $this->repoController->getSection(), $this->repoController->getDate())) { - throw new Exception('A snapshot already exists on the ' . Label::black($this->repoController->getDateFormatted())); + throw new Exception('A snapshot already exists on the ' . Label::white($this->repoController->getDateFormatted())); } } } diff --git a/www/controllers/Repo/Repo.php b/www/controllers/Repo/Repo.php index eb34138d1..0dbb5c70f 100644 --- a/www/controllers/Repo/Repo.php +++ b/www/controllers/Repo/Repo.php @@ -25,8 +25,9 @@ class Repo private $date; private $dateFormatted; private $time; - private $env; + private $env = []; private $description; + private $tags = []; private $group; private $advancedParams = []; private $signed; @@ -117,6 +118,11 @@ public function setDescription($description = ''): void $this->description = Validate::string($description); } + public function setTags(array $tags = []): void + { + $this->tags = $tags; + } + public function setSource(string $source): void { $this->source = $source; @@ -199,6 +205,7 @@ public function getPackageType(): string public function getEnv(): string|array { + // return $this->env ?? []; return $this->env; } @@ -257,6 +264,11 @@ public function getDescription(): string|null return $this->description; } + public function getTags(): array + { + return $this->tags; + } + public function getGroup(): string|null { return $this->group; @@ -292,74 +304,76 @@ public function getAllById(string|int|null $repoId = null, string|int|null $snap { $data = $this->model->getAllById($repoId, $snapId, $envId); - if (!empty($data['repoId'])) { - $this->setRepoId($data['repoId']); - } - if (!empty($data['snapId'])) { - $this->setSnapId($data['snapId']); - } - if (!empty($data['envId'])) { - $this->setEnvId($data['envId']); - } - if (!empty($data['Source'])) { - $this->setSource($data['Source']); - } - if (!empty($data['Name'])) { - $this->setName($data['Name']); - } - if (!empty($data['Releasever'])) { - $this->setReleasever($data['Releasever']); - } - if (!empty($data['Dist'])) { - $this->setDist($data['Dist']); - } - if (!empty($data['Section'])) { - $this->setSection($data['Section']); - } - if (!empty($data['Package_type'])) { - $this->setPackageType($data['Package_type']); - } - if (!empty($data['Date'])) { - $this->setDate($data['Date']); - } - if (!empty($data['Time'])) { - $this->setTime($data['Time']); - } - if (!empty($data['Status'])) { - $this->setStatus($data['Status']); - } - if (!empty($data['Env'])) { - $this->setEnv($data['Env']); - } - if (!empty($data['Type'])) { - $this->setType($data['Type']); - } - if (!empty($data['Signed'])) { - $this->setSigned($data['Signed']); - } - if (!empty($data['Reconstruct'])) { - $this->setRebuild($data['Reconstruct']); - } - if (!empty($data['Description'])) { - $this->setDescription($data['Description']); - } - if (!empty($data['Arch'])) { - $this->setArch(explode(',', $data['Arch'])); - } + try { + if (!empty($data['repoId'])) { + $this->setRepoId($data['repoId']); + } + if (!empty($data['snapId'])) { + $this->setSnapId($data['snapId']); + } + if (!empty($data['envId'])) { + $this->setEnvId($data['envId']); + } + if (!empty($data['Source'])) { + $this->setSource($data['Source']); + } + if (!empty($data['Name'])) { + $this->setName($data['Name']); + } + if (!empty($data['Releasever'])) { + $this->setReleasever($data['Releasever']); + } + if (!empty($data['Dist'])) { + $this->setDist($data['Dist']); + } + if (!empty($data['Section'])) { + $this->setSection($data['Section']); + } + if (!empty($data['Package_type'])) { + $this->setPackageType($data['Package_type']); + } + if (!empty($data['Date'])) { + $this->setDate($data['Date']); + } + if (!empty($data['Time'])) { + $this->setTime($data['Time']); + } + if (!empty($data['Status'])) { + $this->setStatus($data['Status']); + } + if (!empty($data['Env'])) { + $this->setEnv($data['Env']); + } + if (!empty($data['Type'])) { + $this->setType($data['Type']); + } + if (!empty($data['Signed'])) { + $this->setSigned($data['Signed']); + } + if (!empty($data['Reconstruct'])) { + $this->setRebuild($data['Reconstruct']); + } + if (!empty($data['Arch'])) { + $this->setArch(explode(',', $data['Arch'])); + } + if (!empty($data['Description'])) { + $this->setDescription($data['Description']); + } + if (!empty($data['Tags'])) { + $tags = explode(',', $data['Tags']); + $this->setTags($tags); + } - /** - * Extract Advanced_params JSON - * This includes package include/exclude, metadata custom fields and potentially other parameters in the future - * It is optional and can be empty - */ - if (!empty($data['Advanced_params'])) { - try { + /** + * Advanced_params includes package include/exclude, metadata custom fields and potentially other parameters in the future + * It is optional and can be empty + */ + if (!empty($data['Advanced_params'])) { $advancedParams = json_decode($data['Advanced_params'], true, 512, JSON_THROW_ON_ERROR); - } catch (JsonException $e) { - throw new Exception('Failed to decode advanced parameters JSON: ' . $e->getMessage()); + $this->setAdvancedParams($advancedParams); } - - $this->setAdvancedParams($advancedParams); + } catch (JsonException $e) { + throw new Exception('Failed to decode JSON data from database: ' . $e->getMessage()); } } @@ -533,4 +547,46 @@ public function updateSource(int $repoId, string $source): void { $this->model->updateSource($repoId, $source); } + + /** + * Update description + */ + public function updateDescription(int $id, string $description): void + { + // Description should not contain single quotes or backslashes + if (str_contains($description, "'") || str_contains($description, "\\") || str_contains($description, '')) { + throw new Exception('Description contains invalid characters'); + } + + $this->model->updateDescription($id, Validate::string($description)); + } + + /** + * Update tags + */ + public function updateTags(int $id, array $tags): void + { + $validTags = []; + + foreach ($tags as $tag) { + $tag = trim($tag); + + // Ignore empty tags + if (empty($tag)) { + continue; + } + + // Tags must be alphanumeric (hyphen and underscore allowed) + if (!Validate::alphaNumericHyphen($tag)) { + throw new Exception('Tag ' . $tag . ' contains invalid characters'); + } + + // Avoid duplicates + if (!in_array($tag, $validTags)) { + $validTags[] = $tag; + } + } + + $this->model->updateTags($id, implode(',', $validTags)); + } } diff --git a/www/controllers/Repo/Rpm.php b/www/controllers/Repo/Rpm.php index 264c8ad41..009995ed9 100644 --- a/www/controllers/Repo/Rpm.php +++ b/www/controllers/Repo/Rpm.php @@ -2,8 +2,6 @@ namespace Controllers\Repo; -use Exception; - class Rpm extends \Controllers\Repo\Repo { public function __construct() @@ -20,14 +18,6 @@ public function getIdByNameReleasever(string $name, string $releaseVersion) : in return $this->model->getIdByNameReleasever($name, $releaseVersion); } - /** - * Return repository environment description - */ - public function getDescriptionByName(string $name, string $releaseVersion, string $env) : string|null - { - return $this->model->getDescriptionByName($name, $releaseVersion, $env); - } - /** * Return environment Id from repo name */ diff --git a/www/controllers/Repo/Snapshot/Package.php b/www/controllers/Repo/Snapshot/Package.php index 691a2f33f..8f08e17fb 100644 --- a/www/controllers/Repo/Snapshot/Package.php +++ b/www/controllers/Repo/Snapshot/Package.php @@ -126,6 +126,11 @@ public function upload(array $packages, bool $overwrite = false, bool $ignoreIfE throw new Exception('You are not allowed to upload packages'); } + // Check if snapshot has a protected env + if ($this->repoSnapshotController->hasProtectedEnv($this->snapId)) { + throw new Exception('Snapshot has a protected environment and cannot be modified. Please contact your administrator.'); + } + // Both overwrite and ignoreIfExists cannot be true at the same time if ($overwrite && $ignoreIfExists) { throw new Exception('Options overwrite and ignore-if-exists are mutually exclusive'); @@ -331,6 +336,11 @@ public function delete(array $files): array throw new Exception('You are not allowed to delete packages.'); } + // Check if snapshot has a protected env + if ($this->repoSnapshotController->hasProtectedEnv($this->snapId)) { + throw new Exception('Snapshot has a protected environment and cannot be modified. Please contact your administrator.'); + } + foreach ($files as $file) { $file = REPOS_DIR . '/' . $file; diff --git a/www/controllers/Repo/Snapshot/Snapshot.php b/www/controllers/Repo/Snapshot/Snapshot.php index 3179e2a83..c1133c66d 100644 --- a/www/controllers/Repo/Snapshot/Snapshot.php +++ b/www/controllers/Repo/Snapshot/Snapshot.php @@ -160,6 +160,24 @@ public function updateAdvancedParams(int $snapId, array $advancedParams): void $this->model->updateAdvancedParams($snapId, $advancedParams); } + /** + * Return true if the snapshot has a protected environment + */ + public function hasProtectedEnv(int $snapId): bool + { + $envController = new \Controllers\Environment(); + $repoEnvController = new \Controllers\Repo\Environment(); + + // Get the list of environments associated with the snapshot and check if any of them is protected + foreach ($repoEnvController->getBySnapId($snapId) as $env) { + if ($envController->isProtected($env)) { + return true; + } + } + + return false; + } + /** * Clean unused snapshots and return a message */ diff --git a/www/controllers/Repo/Task/Create.php b/www/controllers/Repo/Task/Create.php index 1984aa53d..0440e183c 100644 --- a/www/controllers/Repo/Task/Create.php +++ b/www/controllers/Repo/Task/Create.php @@ -56,7 +56,7 @@ public function __construct(string $taskId) /** * Create repository */ - public function execute() + public function execute(): void { if ($this->type == 'mirror') { $this->mirror(); @@ -70,52 +70,38 @@ public function execute() /** * Create a mirror repository */ - private function mirror() + private function mirror(): void { - /** - * Define the date and time of the new mirror snapshot - */ + // Define the date and time of the new mirror snapshot $this->repoController->setDate(date('Y-m-d')); $this->repoController->setTime(date('H:i')); - /** - * Sync packages - */ + // Sync packages $this->syncPackage(); - /** - * Sign repository / packages - */ + // Sign repository / packages $this->signPackage(); - /** - * Create repository and symlinks - */ + // Create repository and symlinks $this->createMetadata(); - /** - * Finalize repository (add to database and set permissions) - */ + // Finalize repository (add to database and set permissions) $this->finalize(); } /** * Create a local repository */ - private function local() + private function local(): void { - /** - * Set today date and time as target date and time - */ + // Set today date and time as target date and time $this->repoController->setDate(date('Y-m-d')); $this->repoController->setTime(date('H:i')); $this->taskLogStepController->new('create-repo', 'CREATING REPOSITORY'); $this->taskLogSubStepController->new('create-dirs', 'CREATING DIRECTORIES'); - /** - * Check if a repo/section with the same name is already active with snapshots - */ + // Check if a repo/section with the same name is already active with snapshots if ($this->repoController->getPackageType() == 'rpm') { if ($this->rpmRepoController->isActive($this->repoController->getName(), $this->repoController->getReleasever())) { throw new Exception(Label::white($this->repoController->getName() . ' ❯ ' . $this->repoController->getReleasever()) . ' repository already exists'); @@ -127,9 +113,7 @@ private function local() } } - /** - * Define snapshot directory path - */ + // Define snapshot directory path if ($this->repoController->getPackageType() == 'rpm') { $snapshotPath = REPOS_DIR . '/rpm/' . $this->repoController->getName() . '/' . $this->repoController->getReleasever() . '/' . $this->repoController->getDate(); } @@ -137,9 +121,7 @@ private function local() $snapshotPath = REPOS_DIR . '/deb/' . $this->repoController->getName() . '/' . $this->repoController->getDist() . '/' . $this->repoController->getSection() . '/' . $this->repoController->getDate(); } - /** - * Create snapshot directory and subdirectories - */ + // Create snapshot directory and subdirectories if ($this->repoController->getPackageType() == 'rpm') { if (!is_dir($snapshotPath . '/packages')) { if (!mkdir($snapshotPath . '/packages', 0770, true)) { @@ -155,9 +137,7 @@ private function local() } } - /** - * Create environment symlink, if an environment has been specified - */ + // Create environment symlink, if an environment has been specified if (!empty($this->repoController->getEnv())) { foreach ($this->repoController->getEnv() as $env) { if ($this->repoController->getPackageType() == 'rpm') { @@ -167,18 +147,14 @@ private function local() $link = REPOS_DIR . '/deb/' . $this->repoController->getName() . '/' . $this->repoController->getDist() . '/' . $this->repoController->getSection() . '/' . $env; } - /** - * If a symlink with the same name already exists, we remove it - */ + // If a symlink with the same name already exists, we remove it if (is_link($link)) { if (!unlink($link)) { throw new Exception('Could not remove existing symlink ' . $link); } } - /** - * Create symlink - */ + // Create symlink if (!symlink($this->repoController->getDate(), $link)) { throw new Exception('Could not point environment to the repository'); } @@ -191,73 +167,50 @@ private function local() $this->taskLogSubStepController->new('updating-database', 'UPDATING DATABASE'); - /** - * Check if repository already exists in database - */ + // If currently no rpm repo of this name exists in the database then we add it if ($this->repoController->getPackageType() == 'rpm') { - $exists = $this->rpmRepoController->exists($this->repoController->getName(), $this->repoController->getReleasever()); - } - if ($this->repoController->getPackageType() == 'deb') { - $exists = $this->debRepoController->exists($this->repoController->getName(), $this->repoController->getDist(), $this->repoController->getSection()); - } - - /** - * If no repo of this name exists in database then we add it - * Note: here we set the source as $this->repoController->getName() - */ - if ($exists === false) { - /** - * Retrieve repo Id from the last insert row - */ - if ($this->repoController->getPackageType() == 'rpm') { + if (!$this->rpmRepoController->exists($this->repoController->getName(), $this->repoController->getReleasever())) { + // Note: for local repositories, source is set to repo name $this->rpmRepoController->add($this->repoController->getName(), $this->repoController->getReleasever(), $this->repoController->getName()); + + // Repository Id becomes the Id of the last inserted row in the database $this->repoController->setRepoId($this->rpmRepoController->getLastInsertRowID()); - } - if ($this->repoController->getPackageType() == 'deb') { - $this->debRepoController->add($this->repoController->getName(), $this->repoController->getDist(), $this->repoController->getSection(), $this->repoController->getName()); - $this->repoController->setRepoId($this->debRepoController->getLastInsertRowID()); - } - /** - * Else if a repo of this name exists, we attach this new snapshot and this new env to this repo - */ - } else { - /** - * Retrieve and set repo Id from database - */ - if ($this->repoController->getPackageType() == 'rpm') { - $repoId = $this->rpmRepoController->getIdByNameReleasever($this->repoController->getName(), $this->repoController->getReleasever()); + // Otherwise, if a repo of the same name exists, we retrieve its Id from the database + } else { + $this->repoController->setRepoId($this->rpmRepoController->getIdByNameReleasever($this->repoController->getName(), $this->repoController->getReleasever())); } + } - if ($this->repoController->getPackageType() == 'deb') { - $repoId = $this->debRepoController->getIdByNameDistComponent($this->repoController->getName(), $this->repoController->getDist(), $this->repoController->getSection()); - } + // If currently no deb repo of this name exists in the database then we add it + if ($this->repoController->getPackageType() == 'deb') { + if (!$this->debRepoController->exists($this->repoController->getName(), $this->repoController->getDist(), $this->repoController->getSection())) { + // Note: for local repositories, source is set to repo name + $this->debRepoController->add($this->repoController->getName(), $this->repoController->getDist(), $this->repoController->getSection(), $this->repoController->getName()); - if (empty($repoId)) { - throw new Exception('Could not retrieve repository Id from database'); - } + // Repository Id becomes the Id of the last inserted row in the database + $this->repoController->setRepoId($this->debRepoController->getLastInsertRowID()); - $this->repoController->setRepoId($repoId); + // Otherwise, if a repo of the same name exists, we retrieve its Id from the database + } else { + $this->repoController->setRepoId($this->debRepoController->getIdByNameDistComponent($this->repoController->getName(), $this->repoController->getDist(), $this->repoController->getSection())); + } } - unset($exists, $repoId); + // Apply description and tags after the repository Id has been determined, + $this->repoController->updateDescription($this->repoController->getRepoId(), (string) $this->repoController->getDescription()); + $this->repoController->updateTags($this->repoController->getRepoId(), $this->repoController->getTags()); - /** - * Add snapshot to database - */ + // Add snapshot to database $this->repoSnapshotController->add($this->repoController->getDate(), $this->repoController->getTime(), 'false', $this->repoController->getArch(), $this->repoController->getAdvancedParams(), $this->repoController->getType(), 'active', $this->repoController->getRepoId()); - /** - * Retrieve snapshot Id from the last insert row - */ + // Retrieve snapshot Id from the last insert row $this->repoController->setSnapId($this->repoSnapshotController->getLastInsertRowID()); - /** - * Add env to database if an env has been specified by the user - */ + // Add env to database if an env has been specified by the user if (!empty($this->repoController->getEnv())) { foreach ($this->repoController->getEnv() as $env) { - $this->repoEnvController->add($this->repoController->getSnapId(), $env, $this->repoController->getDescription()); + $this->repoEnvController->add($this->repoController->getSnapId(), $env); } } @@ -265,18 +218,14 @@ private function local() $this->taskLogSubStepController->new('applying-permissions', 'APPLYING PERMISSIONS'); - /** - * Apply permissions on the new repo - */ + // Apply permissions on the new repo File::recursiveChmod($snapshotPath, 'dir', 770); File::recursiveChmod($snapshotPath, 'file', 660); $this->taskLogSubStepController->completed(); $this->taskLogStepController->completed(); - /** - * Add repo to group if a group has been specified - */ + // Add repo to group if a group has been specified if (!empty($this->repoController->getGroup())) { $this->taskLogStepController->new('add-to-group', 'ADDING REPOSITORY TO GROUP'); $this->repoController->addRepoIdToGroup($this->repoController->getRepoId(), $this->repoController->getGroup()); diff --git a/www/controllers/Repo/Task/Delete.php b/www/controllers/Repo/Task/Delete.php index d01a6d1e3..602ef7cad 100644 --- a/www/controllers/Repo/Task/Delete.php +++ b/www/controllers/Repo/Task/Delete.php @@ -33,7 +33,7 @@ public function __construct(string $taskId) /** * Delete a repository snapshot */ - public function execute() + public function execute(): void { $deleted = true; diff --git a/www/controllers/Repo/Task/Duplicate.php b/www/controllers/Repo/Task/Duplicate.php index 72722a25d..77612c851 100644 --- a/www/controllers/Repo/Task/Duplicate.php +++ b/www/controllers/Repo/Task/Duplicate.php @@ -45,16 +45,12 @@ public function execute() $this->taskLogStepController->new('duplicating', 'DUPLICATING'); $this->taskLogSubStepController->new('initializing', 'INITIALIZING'); - /** - * Check if source repo snapshot exists - */ + // Check if source repo snapshot exists if ($this->sourceRepoController->existsSnapId($this->sourceRepoController->getSnapId()) === false) { throw new Exception('Source repository snapshot does not exist'); } - /** - * Check if a repo with the same name already exists - */ + // Check if a repo with the same name already exists if ($this->repoController->getPackageType() == 'rpm') { if ($this->rpmRepoController->isActive($this->repoController->getName(), $this->repoController->getReleasever())) { throw new Exception('A repo ' . $this->repoController->getName() . ' (release ver. ' . $this->repoController->getReleasever() . ') already exists'); @@ -66,9 +62,7 @@ public function execute() } } - /** - * Define source snapshot path - */ + // Define source snapshot path if ($this->sourceRepoController->getPackageType() == 'rpm') { $sourceSnapshotPath = REPOS_DIR . '/rpm/' . $this->sourceRepoController->getName() . '/' . $this->sourceRepoController->getReleasever() . '/' . $this->sourceRepoController->getDate(); } @@ -76,9 +70,7 @@ public function execute() $sourceSnapshotPath = REPOS_DIR . '/deb/' . $this->sourceRepoController->getName() . '/' . $this->sourceRepoController->getDist() . '/' . $this->sourceRepoController->getSection() . '/' . $this->sourceRepoController->getDate(); } - /** - * Define target snapshot path - */ + // Define target snapshot path if ($this->repoController->getPackageType() == 'rpm') { $parentDir = REPOS_DIR . '/rpm/' . $this->repoController->getName() . '/' . $this->repoController->getReleasever(); $targetSnapshotPath = REPOS_DIR . '/rpm/' . $this->repoController->getName() . '/' . $this->repoController->getReleasever() . '/'. $this->repoController->getDate(); @@ -88,38 +80,28 @@ public function execute() $targetSnapshotPath = REPOS_DIR . '/deb/' . $this->repoController->getName() . '/' . $this->repoController->getDist() . '/' . $this->repoController->getSection() . '/' . $this->repoController->getDate(); } - /** - * Create parent directory if it does not already exists - */ + // Create parent directory if it does not already exists if (!is_dir($parentDir)) { if (!mkdir($parentDir, 0770, true)) { throw new Exception('Cannot create directory ' . $parentDir); } } - /** - * Set temporary dir path - */ + // Set temporary dir path $tempDir = REPOS_DIR . '/temporary-task-' . $this->taskId; - /** - * Create the temporary directory if it does not already exist (might exist if the task has been stopped and restarted) - */ + // Create the temporary directory if it does not already exist (might exist if the task has been stopped and restarted) if (!file_exists($tempDir)) { if (!mkdir($tempDir, 0770, true)) { throw new Exception('Cannot create temporary directory ' . $tempDir); } } - /** - * Get all files and directories in the source repository - */ + // Get all files and directories in the source repository $dirs = File::recursiveScan($sourceSnapshotPath, 'dir', true); $files = File::recursiveScan($sourceSnapshotPath, 'file', true); - /** - * Create all directories in the temporary directory - */ + // Create all directories in the temporary directory foreach ($dirs as $dir) { if (!file_exists($tempDir . '/' . $dir)) { if (!mkdir($tempDir . '/' . $dir, 0770, true)) { @@ -139,19 +121,13 @@ public function execute() foreach ($files as $file) { $fileCounter++; - /** - * Count total number of files to copy - */ + // Count total number of files to copy $totalFiles = count($files); - /** - * Show progress - */ + // Show progress $this->taskLogSubStepController->new('copying-file-' . $fileCounter, 'COPYING FILE (' . $fileCounter . '/' . $totalFiles . ')', 'From ' . $sourceSnapshotPath . '/' . $file . '
    To ' . $tempDir . '/' . $file); - /** - * Ignore file if it was already copied (completed file exists) - */ + // Ignore file if it was already copied (completed file exists) if (file_exists($tempDir . '/' . $file) and file_exists($tempDir . '/' . $file . '.completed')) { $this->taskLogSubStepController->completed('Already exists (ignoring)'); continue; @@ -167,16 +143,12 @@ public function execute() } } - /** - * Copy the file - */ + // Copy the file if (!copy($sourceSnapshotPath . '/' . $file, $tempDir . '/' . $file)) { throw new Exception('Cannot copy file ' . $sourceSnapshotPath . '/' . $file . ' to ' . $tempDir . '/' . $file); } - /** - * Create the completed file - */ + // Create the completed file if (!touch($tempDir . '/' . $file . '.completed')) { throw new Exception('Cannot create copy-completed file ' . $tempDir . '/' . $file . '.completed'); } @@ -186,9 +158,7 @@ public function execute() unset($dirs, $files); - /** - * Rename the temporary directory to the target directory - */ + // Rename the temporary directory to the target directory $this->taskLogSubStepController->new('moving-temp-dir', 'MOVING TEMPORARY DIRECTORY'); if (!rename($tempDir, $targetSnapshotPath)) { throw new Exception('Cannot rename temporary directory ' . $tempDir . ' to ' . $targetSnapshotPath); @@ -208,23 +178,17 @@ public function execute() } } - /** - * Set step 'DUPLICATING' as completed - */ + // Set step 'DUPLICATING' as completed $this->taskLogStepController->completed(); - /** - * On a deb repo, the duplicated repo metadata must be rebuilded - */ + // On a deb repo, the duplicated repo metadata must be rebuilded if ($this->repoController->getPackageType() == 'deb') { $this->createMetadata(); } $this->taskLogStepController->new('finalizing', 'FINALIZING'); - /** - * Create a symlink to the new repo, only if the user has specified an environment - */ + // Create a symlink to the new repo, only if the user has specified an environment if (!empty($this->repoController->getEnv())) { foreach ($this->repoController->getEnv() as $env) { $this->taskLogSubStepController->new('pointing-environment', 'POINTING ENVIRONMENT'); @@ -236,18 +200,14 @@ public function execute() $link = REPOS_DIR . '/deb/' . $this->repoController->getName() . '/' . $this->repoController->getDist() . '/' . $this->repoController->getSection() . '/' . $env; } - /** - * If a symlink with the same name already exists, we remove it - */ + // If a symlink with the same name already exists, we remove it if (is_link($link)) { if (!unlink($link)) { throw new Exception('Could not remove existing symlink ' . $link); } } - /** - * Create symlink - */ + // Create symlink if (!symlink($this->repoController->getDate(), $link)) { throw new Exception('Could not point environment to the repository'); } @@ -260,9 +220,7 @@ public function execute() $this->taskLogSubStepController->new('inserting-database', 'INSERTING REPOSITORY IN DATABASE'); - /** - * Insert the new repo in database and retrieve its Id - */ + // Insert the new repo in database and retrieve its Id if ($this->repoController->getPackageType() == 'rpm') { $this->rpmRepoController->add($this->repoController->getName(), $this->repoController->getReleasever(), $this->repoController->getSource()); $targetRepoId = $this->rpmRepoController->getLastInsertRowID(); @@ -272,14 +230,13 @@ public function execute() $targetRepoId = $this->debRepoController->getLastInsertRowID(); } - /** - * Add the new repo snapshot in database - */ + // Repository Id becomes the Id of the last inserted row in the database + $this->repoController->setRepoId($targetRepoId); + + // Add the new repo snapshot in database $this->repoSnapshotController->add($this->repoController->getDate(), $this->repoController->getTime(), $this->repoController->getSigned(), $this->repoController->getArch(), $this->repoController->getAdvancedParams(), $this->repoController->getType(), $this->repoController->getStatus(), $targetRepoId); - /** - * Retrieve the Id of the new repo snapshot in database - */ + // Retrieve the Id of the new repo snapshot in database $targetSnapId = $this->repoSnapshotController->getLastInsertRowID(); // Calculate and update snapshot size @@ -287,28 +244,26 @@ public function execute() $this->repoSnapshotController->updateSize($targetSnapId, Directory::getSize($targetSnapshotPath)); $this->taskLogSubStepController->completed(); - /** - * Add the new repo environment in database, only if the user has specified an environment - */ + // Add the new repo environment in database, only if the user has specified an environment if (!empty($this->repoController->getEnv())) { foreach ($this->repoController->getEnv() as $env) { - $this->repoEnvController->add($targetSnapId, $env, $this->repoController->getDescription()); + $this->repoEnvController->add($targetSnapId, $env); } } - /** - * Add the new repo to a group if a group has been specified - */ + // Apply description and tags after the repository Id has been determined, + $this->repoController->updateDescription($this->repoController->getRepoId(), (string) $this->repoController->getDescription()); + $this->repoController->updateTags($this->repoController->getRepoId(), $this->repoController->getTags()); + + // Add the new repo to a group if a group has been specified if (!empty($this->repoController->getGroup())) { - $this->repoController->addRepoIdToGroup($targetRepoId, $this->repoController->getGroup()); + $this->repoController->addRepoIdToGroup($this->repoController->getRepoId(), $this->repoController->getGroup()); } $this->taskLogSubStepController->completed(); $this->taskLogStepController->completed(); - /** - * If an error occurred after the temporary directory was renamed, clean the target directory - */ + // If an error occurred after the temporary directory was renamed, clean the target directory } catch (Exception $e) { if (file_exists($targetSnapshotPath)) { if (!Directory::deleteRecursive($targetSnapshotPath)) { @@ -316,9 +271,7 @@ public function execute() } } - /** - * Throw initial exception to set the task as error - */ + // Throw initial exception to set the task as error throw new Exception($e->getMessage()); } } diff --git a/www/controllers/Repo/Task/Env.php b/www/controllers/Repo/Task/Env.php index 4d9634645..de7968953 100644 --- a/www/controllers/Repo/Task/Env.php +++ b/www/controllers/Repo/Task/Env.php @@ -32,11 +32,7 @@ public function __construct(string $taskId) public function execute() { foreach ($this->repoController->getEnv() as $env) { - $actualDescription = null; - - /** - * Define snapshot directory path - */ + // Define snapshot directory path if ($this->repoController->getPackageType() == 'rpm') { $snapshotPath = REPOS_DIR . '/rpm/' . $this->repoController->getName() . '/' . $this->repoController->getReleasever() . '/' . $this->repoController->getDate(); } @@ -47,44 +43,19 @@ public function execute() $this->taskLogStepController->new('point-env-' . $env, 'POINT ENVIRONMENT ' . Label::envtag($env)); $this->taskLogSubStepController->new('checking-' . $env, 'CHECKING'); - /** - * Check if the source snapshot exists - */ + // Check if the source snapshot exists if (!$this->repoController->existsSnapId($this->repoController->getSnapId())) { throw new Exception('' . $this->repoController->getDateFormatted() . ' repository snapshot does not exist anymore'); } - /** - * Check if an environment of the same name already exists on the target snapshot - */ + // Check if an environment of the same name already points to the target snapshot if ($this->repoController->existsSnapIdEnv($this->repoController->getSnapId(), $env) === true) { if ($this->repoController->getPackageType() == 'rpm') { - throw new Exception(Label::envtag($env) . ' environment already exists on ' . $this->repoController->getName() . '' . $this->repoController->getDateFormatted() . ''); + throw new Exception(Label::envtag($env) . ' environment already points to ' . $this->repoController->getName() . '' . $this->repoController->getDateFormatted() . ''); } if ($this->repoController->getPackageType() == 'deb') { - throw new Exception(Label::envtag($env) . ' environment already exists on ' . $this->repoController->getName() . ' ❯ ' . $this->repoController->getDist() . ' ❯ ' . $this->repoController->getSection() . '' . $this->repoController->getDateFormatted() . ''); - } - } - - /** - * If the user did not specify any description then we get the one currently in place on the environment of the same name (if the environment exists and if it has a description) - */ - if (empty($this->repoController->getDescription())) { - if ($this->repoController->getPackageType() == 'rpm') { - $actualDescription = $this->rpmRepoController->getDescriptionByName($this->repoController->getName(), $this->repoController->getReleasever(), $env); - } - if ($this->repoController->getPackageType() == 'deb') { - $actualDescription = $this->debRepoController->getDescriptionByName($this->repoController->getName(), $this->repoController->getDist(), $this->repoController->getSection(), $env); - } - - /** - * If the description is empty then the description will remain empty - */ - if (!empty($actualDescription)) { - $this->repoController->setDescription(htmlspecialchars_decode($actualDescription)); - } else { - $this->repoController->setDescription(''); + throw new Exception(Label::envtag($env) . ' environment already points to ' . $this->repoController->getName() . ' ❯ ' . $this->repoController->getDist() . ' ❯ ' . $this->repoController->getSection() . '' . $this->repoController->getDateFormatted() . ''); } } @@ -92,18 +63,12 @@ public function execute() $this->taskLogSubStepController->new('create-symlink-' . $env, 'CREATING SYMLINK'); if ($this->repoController->getPackageType() == 'rpm') { - /** - * If there is already an environment of the same name pointing to a snapshot. - */ + // If there is already an environment of the same name pointing to a snapshot if ($this->rpmRepoController->existsEnv($this->repoController->getName(), $this->repoController->getReleasever(), $env)) { - /** - * Retrieve the Id of the already existing environment - */ + // Retrieve the Id of the already existing environment $actualEnvIds = $this->rpmRepoController->getEnvIdFromRepoName($this->repoController->getName(), $this->repoController->getReleasever(), $env); - /** - * Delete the possible environment of the same name already pointing to a snapshot of this repo (if there is one) - */ + // Delete the possible environment of the same name already pointing to a snapshot of this repo (if there is one) if (!empty($actualEnvIds)) { foreach ($actualEnvIds as $actualEnvId) { $this->repoEnvController->remove($actualEnvId); @@ -111,36 +76,26 @@ public function execute() } } - /** - * Delete symbolic link if already exists - */ + // Delete symbolic link if already exists if (is_link(REPOS_DIR . '/rpm/' . $this->repoController->getName() . '/' . $this->repoController->getReleasever() . '/' . $env)) { if (!unlink(REPOS_DIR . '/rpm/' . $this->repoController->getName() . '/' . $this->repoController->getReleasever() . '/' . $env)) { throw new Exception('Could not delete existing symbolic link: ' . REPOS_DIR . '/rpm/' . $this->repoController->getName() . '/' . $this->repoController->getReleasever() . '/' . $env); } } - /** - * Create symbolic link - */ + // Create symbolic link if (!symlink($this->repoController->getDate(), REPOS_DIR . '/rpm/' . $this->repoController->getName() . '/' . $this->repoController->getReleasever() . '/' . $env)) { throw new Exception('Could not create symbolic link: ' . REPOS_DIR . '/rpm/' . $this->repoController->getName() . '/' . $this->repoController->getReleasever() . '/' . $env); } } if ($this->repoController->getPackageType() == 'deb') { - /** - * If there is already an environment of the same name pointing to a snapshot. - */ + // If there is already an environment of the same name pointing to a snapshot if ($this->debRepoController->existsEnv($this->repoController->getName(), $this->repoController->getDist(), $this->repoController->getSection(), $env)) { - /** - * Retrieve the Id of the already existing environment - */ + // Retrieve the Id of the already existing environment $actualEnvIds = $this->debRepoController->getEnvIdFromRepoName($this->repoController->getName(), $this->repoController->getDist(), $this->repoController->getSection(), $env); - /** - * Delete the possible environment of the same name already pointing to a snapshot of this repo (if there is one) - */ + // Delete the possible environment of the same name already pointing to a snapshot of this repo (if there is one) if (!empty($actualEnvIds)) { foreach ($actualEnvIds as $actualEnvId) { $this->repoEnvController->remove($actualEnvId); @@ -148,18 +103,14 @@ public function execute() } } - /** - * Delete symbolic link if already exists - */ + // Delete symbolic link if already exists if (is_link(REPOS_DIR . '/deb/' . $this->repoController->getName() . '/' . $this->repoController->getDist() . '/' . $this->repoController->getSection() . '/' . $env)) { if (!unlink(REPOS_DIR . '/deb/' . $this->repoController->getName() . '/' . $this->repoController->getDist() . '/' . $this->repoController->getSection() . '/' . $env)) { throw new Exception('Could not delete existing symbolic link: ' . REPOS_DIR . '/deb/' . $this->repoController->getName() . '/' . $this->repoController->getDist() . '/' . $this->repoController->getSection() . '/' . $env); } } - /** - * Create symbolic link - */ + // Create symbolic link if (!symlink($this->repoController->getDate(), REPOS_DIR . '/deb/' . $this->repoController->getName() . '/' . $this->repoController->getDist() . '/' . $this->repoController->getSection() . '/' . $env)) { throw new Exception('Could not create symbolic link: ' . REPOS_DIR . '/deb/' . $this->repoController->getName() . '/' . $this->repoController->getDist() . '/' . $this->repoController->getSection() . '/' . $env); } @@ -167,11 +118,9 @@ public function execute() $this->taskLogSubStepController->completed(); - /** - * Add environment to database - */ + // Add environment to database $this->taskLogSubStepController->new('update-database', 'UPDATING DATABASE'); - $this->repoEnvController->add($this->repoController->getSnapId(), $env, $this->repoController->getDescription()); + $this->repoEnvController->add($this->repoController->getSnapId(), $env); $this->taskLogSubStepController->completed(); $this->taskLogStepController->completed(); } diff --git a/www/controllers/Repo/Task/Finalize.php b/www/controllers/Repo/Task/Finalize.php index e2a9a38da..660b46a45 100644 --- a/www/controllers/Repo/Task/Finalize.php +++ b/www/controllers/Repo/Task/Finalize.php @@ -11,7 +11,7 @@ trait Finalize /** * Finalize the repository: add to the database and apply permissions */ - protected function finalize() + protected function finalize(): void { $this->taskLogStepController->new('finalizing', 'FINALIZING'); @@ -48,7 +48,6 @@ protected function finalize() if ($this->repoController->getPackageType() == 'deb') { if (!$this->debRepoController->exists($this->repoController->getName(), $this->repoController->getDist(), $this->repoController->getSection())) { $this->debRepoController->add($this->repoController->getName(), $this->repoController->getDist(), $this->repoController->getSection(), $this->repoController->getSource()); - // Repository Id becomes the Id of the last inserted row in the database $this->repoController->setRepoId($this->debRepoController->getLastInsertRowID()); @@ -58,6 +57,10 @@ protected function finalize() } } + // Apply description and tags after the repository Id has been determined, + $this->repoController->updateDescription($this->repoController->getRepoId(), (string) $this->repoController->getDescription()); + $this->repoController->updateTags($this->repoController->getRepoId(), $this->repoController->getTags()); + // Add snapshot in database $this->repoSnapshotController->add($this->repoController->getDate(), $this->repoController->getTime(), $this->repoController->getGpgSign(), $this->repoController->getArch(), $this->repoController->getAdvancedParams(), $this->repoController->getType(), 'active', $this->repoController->getRepoId()); @@ -67,7 +70,7 @@ protected function finalize() // Add env in database if an env has been specified by the user if (!empty($this->repoController->getEnv())) { foreach ($this->repoController->getEnv() as $env) { - $this->repoEnvController->add($this->repoController->getSnapId(), $env, $this->repoController->getDescription()); + $this->repoEnvController->add($this->repoController->getSnapId(), $env); } } } @@ -123,23 +126,6 @@ protected function finalize() $this->taskLogSubStepController->new('adding-env', 'ADDING ENVIRONMENT'); foreach ($this->repoController->getEnv() as $env) { - // If the user has not specified any description, then we retrieve the one currently in place on the environment of the same name (if the environment exists and if it has a description) - if (empty($this->repoController->getDescription())) { - if ($this->repoController->getPackageType() == 'rpm') { - $actualDescription = $this->rpmRepoController->getDescriptionByName($this->repoController->getName(), $this->repoController->getReleasever(), $env); - } - if ($this->repoController->getPackageType() == 'deb') { - $actualDescription = $this->debRepoController->getDescriptionByName($this->repoController->getName(), $this->repoController->getDist(), $this->repoController->getSection(), $env); - } - - // If the retrieved description is empty then the description will remain empty - if (!empty($actualDescription)) { - $this->repoController->setDescription(htmlspecialchars_decode($actualDescription)); - } else { - $this->repoController->setDescription(''); - } - } - // Retrieve the Id of the environment currently in place (if there is one) if ($this->repoController->getPackageType() == 'rpm') { $actualEnvIds = $this->rpmRepoController->getEnvIdFromRepoName($this->repoController->getName(), $this->repoController->getReleasever(), $env); @@ -156,7 +142,7 @@ protected function finalize() } // Then we declare the new environment and make it point to the previously created snapshot - $this->repoEnvController->add($this->repoController->getSnapId(), $env, $this->repoController->getDescription()); + $this->repoEnvController->add($this->repoController->getSnapId(), $env); } $this->taskLogSubStepController->completed(); diff --git a/www/controllers/Repo/Task/Rebuild.php b/www/controllers/Repo/Task/Rebuild.php index c74aa37bf..5e8893208 100644 --- a/www/controllers/Repo/Task/Rebuild.php +++ b/www/controllers/Repo/Task/Rebuild.php @@ -34,26 +34,18 @@ public function __construct(string $taskId) /** * Rebuild repository metadata */ - public function execute() + public function execute(): void { - /** - * Set snapshot metadata rebuild state in database - */ + // Set snapshot metadata rebuild state in database $this->repoSnapshotController->updateRebuild($this->repoController->getSnapId(), 'running'); - /** - * Sign repository / packages - */ + // Sign repository / packages $this->signPackage(); - /** - * Create repository and symlinks - */ + // Create repository and symlinks $this->createMetadata(); - /** - * Finalize repository (update database, clean temporary files, etc.) - */ + // Finalize repository (update database, clean temporary files, etc.) $this->finalize(); } } diff --git a/www/controllers/Repo/Task/RemoveEnv.php b/www/controllers/Repo/Task/RemoveEnv.php index 72268c778..090ca796e 100644 --- a/www/controllers/Repo/Task/RemoveEnv.php +++ b/www/controllers/Repo/Task/RemoveEnv.php @@ -28,14 +28,12 @@ public function __construct(string $taskId) /** * Remove snapshot environment */ - public function execute() + public function execute(): void { $this->taskLogStepController->new('removing', 'REMOVING'); $this->taskLogSubStepController->new('delete-symlink', 'DELETE SYMLINK'); - /** - * Define environment symlink - */ + // Define environment symlink if ($this->repoController->getPackageType() == 'rpm') { $symlink = REPOS_DIR . '/rpm/' . $this->repoController->getName() . '/' . $this->repoController->getReleasever() . '/' . $this->repoController->getEnv(); } @@ -44,9 +42,7 @@ public function execute() $symlink = REPOS_DIR . '/deb/' . $this->repoController->getName() . '/' . $this->repoController->getDist() . '/' . $this->repoController->getSection() . '/' . $this->repoController->getEnv(); } - /** - * Delete environment symlink - */ + // Delete environment symlink if (file_exists($symlink)) { if (!unlink($symlink)) { throw new Exception('Failed to delete symlink: ' . $symlink); @@ -55,9 +51,7 @@ public function execute() $this->taskLogSubStepController->completed(); - /** - * Delete environment from database - */ + // Delete environment from database $this->taskLogSubStepController->new('update-database', 'UPDATE DATABASE'); $this->repoEnvController->remove($this->repoController->getEnvId()); $this->taskLogSubStepController->completed(); diff --git a/www/controllers/Repo/Task/Rename.php b/www/controllers/Repo/Task/Rename.php index b893edeb3..c4b0907c3 100644 --- a/www/controllers/Repo/Task/Rename.php +++ b/www/controllers/Repo/Task/Rename.php @@ -13,8 +13,8 @@ public function __construct(string $taskId) { parent::__construct($taskId, 'rename'); - // Get source repository details from its snapshot Id - $this->sourceRepoController->getAllById(null, $this->params['snap-id'], null); + // Get source repository details from its repo Id + $this->sourceRepoController->getAllById($this->params['repo-id']); // Execute the task try { @@ -34,14 +34,14 @@ public function __construct(string $taskId) /** * Rename repository */ - public function execute() + public function execute(): void { $this->taskLogStepController->new('renaming', 'RENAMING'); $this->taskLogSubStepController->new('renaming-directory', 'RENAMING REPOSITORY DIRECTORY'); - // Check if source repo snapshot exists - if ($this->sourceRepoController->existsSnapId($this->sourceRepoController->getSnapId()) === false) { - throw new Exception('Source repository snapshot does not exist'); + // Check if source repo exists + if ($this->sourceRepoController->existsId($this->sourceRepoController->getRepoId()) === false) { + throw new Exception('Source repository does not exist'); } // Check if a repo with the same name already exists @@ -56,7 +56,7 @@ public function execute() } } - // Define source snapshot path + // Define source repository path if ($this->sourceRepoController->getPackageType() == 'rpm') { $sourcePath = REPOS_DIR . '/rpm/' . $this->sourceRepoController->getName() . '/' . $this->sourceRepoController->getReleasever(); } @@ -64,7 +64,7 @@ public function execute() $sourcePath = REPOS_DIR . '/deb/' . $this->sourceRepoController->getName() . '/' . $this->sourceRepoController->getDist() . '/' . $this->sourceRepoController->getSection(); } - // Define target snapshot path + // Define target repository path if ($this->repoController->getPackageType() == 'rpm') { $parentDir = REPOS_DIR . '/rpm/' . $this->repoController->getName(); $targetPath = REPOS_DIR . '/rpm/' . $this->repoController->getName() . '/' . $this->repoController->getReleasever(); @@ -96,24 +96,24 @@ public function execute() $this->taskLogSubStepController->completed(); $this->taskLogStepController->completed(); - if ($this->repoController->getPackageType() == 'deb') { - try { - // On a deb repo, the renamed repository needs to have its metadata rebuilded - $this->createMetadata(); - - // If an error occurred after repository has been moved, try to revert the move - } catch (Exception $e) { - // Try to revert the move - if (file_exists($targetPath)) { - if (!rename($targetPath, $sourcePath)) { - throw new Exception('An error occured while creating the repository metadata: ' . $e->getMessage() . '. Additionally, the system could not revert the renamed repository from ' . $targetPath . ' to ' . $sourcePath . '. Manual intervention is required to restore the repository to its original state.'); - } - } - - // Throw initial exception to set the task as error - throw new Exception('An error occured while creating the repository metadata: ' . $e->getMessage(). ' The renamed repository has been reverted to its original state. Please check that everything is fine before retrying the rename operation.'); - } - } + // if ($this->repoController->getPackageType() == 'deb') { + // try { + // // On a deb repo, the renamed repository needs to have its metadata rebuilded + // $this->createMetadata(); + + // // If an error occurred after repository has been moved, try to revert the move + // } catch (Exception $e) { + // // Try to revert the move + // if (file_exists($targetPath)) { + // if (!rename($targetPath, $sourcePath)) { + // throw new Exception('An error occured while creating the repository metadata: ' . $e->getMessage() . '. Additionally, the system could not revert the renamed repository from ' . $targetPath . ' to ' . $sourcePath . '. Manual intervention is required to restore the repository to its original state.'); + // } + // } + + // // Throw initial exception to set the task as error + // throw new Exception('An error occured while creating the repository metadata: ' . $e->getMessage(). ' The renamed repository has been reverted to its original state. Please check that everything is fine before retrying the rename operation.'); + // } + // } try { $this->taskLogStepController->new('finalizing', 'FINALIZING'); diff --git a/www/controllers/Repo/Task/Update.php b/www/controllers/Repo/Task/Update.php index bbe04cc2a..b2d109b1d 100644 --- a/www/controllers/Repo/Task/Update.php +++ b/www/controllers/Repo/Task/Update.php @@ -40,67 +40,49 @@ public function __construct(string $taskId) /** * Update repository */ - public function execute() + public function execute(): void { - /** - * Define default date and time - */ + // Define default date and time $this->repoController->setDate(date('Y-m-d')); $this->repoController->setTime(date('H:i')); - /** - * Sync packages (if mirror repo) - */ + // Sync packages (if mirror repo) if ($this->repoController->getType() == 'mirror') { $this->syncPackage(); } - /** - * Update repository (if local repo) - */ + // Update repository (if local repo) if ($this->repoController->getType() == 'local') { $this->updateLocal(); } - /** - * Sign repo / packages - */ + // Sign repo / packages $this->signPackage(); - /** - * Create repo and symlinks - */ + // Create repo and symlinks $this->createMetadata(); - /** - * Finalize repo (add to database and apply rights) - */ + // Finalize repo (add to database and apply rights) $this->finalize(); } /** * Update local repository */ - private function updateLocal() + private function updateLocal(): void { $this->taskLogStepController->new('updating', 'UPDATING'); $this->taskLogSubStepController->new('initializing', 'INITIALIZING'); - /** - * Define temporary working directory - */ + // Define temporary working directory $workingDir = REPOS_DIR . '/temporary-task-' . $this->taskId; - /** - * Check if a snapshot exists in the database - */ + // Check if a snapshot exists in the database if ($this->repoController->existsSnapId($this->repoController->getSnapId()) === false) { throw new Exception('Specified repo snapshot does not exist'); } - /** - * We cannot update a snapshot in the same day - */ + // We cannot update a snapshot in the same day if ($this->repoController->getPackageType() == 'rpm') { if ($this->rpmRepoController->existsSnapDate($this->repoController->getName(), $this->repoController->getReleasever(), $this->repoController->getDate())) { throw new Exception('A snapshot already exists on the ' . $this->repoController->getDateFormatted() . ''); @@ -112,16 +94,12 @@ private function updateLocal() } } - /** - * Arch must be specified - */ + // Arch must be specified if (empty($this->repoController->getArch())) { throw new Exception('Packages arch must be specified'); } - /** - * Define previous snapshot directory path - */ + // Define previous snapshot directory path if ($this->sourceRepoController->getPackageType() == 'rpm') { $previousSnapshotDir = REPOS_DIR . '/rpm/' . $this->sourceRepoController->getName() . '/' . $this->sourceRepoController->getReleasever() . '/' . $this->sourceRepoController->getDate(); } @@ -129,16 +107,12 @@ private function updateLocal() $previousSnapshotDir = REPOS_DIR . '/deb/' . $this->sourceRepoController->getName() . '/' . $this->sourceRepoController->getDist() . '/' . $this->sourceRepoController->getSection() . '/' . $this->sourceRepoController->getDate(); } - /** - * Check that previous snapshot directory exists - */ + // Check that previous snapshot directory exists if (!is_dir($previousSnapshotDir)) { throw new Exception('Previous snapshot directory does not exist: ' . $previousSnapshotDir); } - /** - * If working directory already exists, delete it - */ + // If working directory already exists, delete it if (is_dir($workingDir)) { if (!Directory::deleteRecursive($workingDir)) { throw new Exception('Cannot delete existing directory: ' . $workingDir); @@ -148,9 +122,7 @@ private function updateLocal() $this->taskLogSubStepController->completed(); $this->taskLogSubStepController->new('search-packages', 'SEARCHING PACKAGES IN PREVIOUS SNAPSHOT'); - /** - * Search for packages in the previous snapshot directory - */ + // Search for packages in the previous snapshot directory try { if ($this->repoController->getPackageType() == 'deb') { $packages = File::findRecursive($previousSnapshotDir . '/pool/' . $this->sourceRepoController->getSection(), [], ['deb', 'dsc', 'gz', 'xz']); @@ -163,17 +135,13 @@ private function updateLocal() throw new Exception('Error while retrieving previous snapshot packages: ' . $e->getMessage()); } - /** - * Count number of packages found - */ + // Count number of packages found $totalPackages = count($packages); $packageCounter = 0; $this->taskLogSubStepController->completed($totalPackages . ' package(s) found'); - /** - * Create target pool/packages directory - */ + // Create target pool/packages directory if ($this->repoController->getPackageType() == 'deb') { // Create pool directory if (!mkdir($workingDir . '/pool/' . $this->repoController->getSection(), 0770, true)) { @@ -187,9 +155,7 @@ private function updateLocal() } } - /** - * Deduplication/Copy packages from previous snapshot to the new snapshot - */ + // Deduplication/Copy packages from previous snapshot to the new snapshot foreach ($packages as $packagePath) { // Get package name $name = basename($packagePath); diff --git a/www/controllers/Service/Unit/ScheduledTask.php b/www/controllers/Service/Unit/ScheduledTask.php index fb7d61d46..51d4d5509 100644 --- a/www/controllers/Service/Unit/ScheduledTask.php +++ b/www/controllers/Service/Unit/ScheduledTask.php @@ -4,6 +4,7 @@ use Exception; use DateTime; +use Controllers\Task\Target; class ScheduledTask extends \Controllers\Service\Service { @@ -35,6 +36,7 @@ public function execute() : void } $taskToExec = []; + $tasksRawParams = []; $dateNow = date('Y-m-d'); $timeNow = date('H:i'); $minutesNow = date('i'); @@ -65,6 +67,9 @@ public function execute() : void $taskRawParams = json_decode($task['Raw_params'], true); + // Keep the parameters aside, they are needed when the task is actually executed + $tasksRawParams[$task['Id']] = $taskRawParams; + /** * Case where the task is a unique task */ @@ -161,9 +166,17 @@ public function execute() : void parent::log('Launching scheduled task #' . $taskId . '...'); try { - // Add the scheduled task to the queue and execute it - $this->taskController->updateStatus($taskId, 'queued'); - $this->taskController->executeId($taskId); + /** + * If the task targets a dynamic set of repositories, it is not executed as is: + * it generates one task per matching repository, on their latest snapshot + */ + if (Target::isDynamic($tasksRawParams[$taskId] ?? [])) { + $this->executeDynamicTask($taskId, $tasksRawParams[$taskId]); + } else { + // Add the scheduled task to the queue and execute it + $this->taskController->updateStatus($taskId, 'queued'); + $this->taskController->executeId($taskId); + } } catch (Exception $e) { throw new Exception('Error while executing scheduled task #' . $taskId . ': ' . $e->getMessage()); } @@ -174,6 +187,42 @@ public function execute() : void } } + /** + * Execute a scheduled task targeting a dynamic set of repositories + * One task is generated per matching repository, targeting its latest snapshot + */ + private function executeDynamicTask(int $taskId, array $taskRawParams): void + { + $targetController = new Target(); + + // Retrieve the parameters of the tasks to generate, one per matching repository + $tasksParams = $targetController->expand($taskRawParams); + + if (empty($tasksParams)) { + parent::log('No repository matches the target of scheduled task #' . $taskId); + } else { + parent::log('Scheduled task #' . $taskId . ' matches ' . count($tasksParams) . ' repository(ies)'); + + // Generate and execute a task for each matching repository, linked to the scheduled task that generated them + $this->taskController->execute($tasksParams, $taskId); + } + + /** + * Keep track of the last execution date and time of the scheduled task, + * it has no log of its own as it only dispatches sub-tasks + */ + $this->taskController->updateDate($taskId, date('Y-m-d')); + $this->taskController->updateTime($taskId, date('H:i:s')); + + /** + * A recurring task stays scheduled and will run again at the next occurrence + * A unique task must not run again, so it is marked as done + */ + if (($taskRawParams['schedule']['schedule-type'] ?? '') == 'unique') { + $this->taskController->updateStatus($taskId, 'done'); + } + } + /** * Send scheduled tasks reminders */ diff --git a/www/controllers/Task/Execution.php b/www/controllers/Task/Execution.php index 3f114bbf1..88998b5cd 100644 --- a/www/controllers/Task/Execution.php +++ b/www/controllers/Task/Execution.php @@ -112,6 +112,11 @@ public function __construct(int $taskId, string $action) $this->repoController->getAllById($this->params['repo-id'], $this->params['snap-id'], $this->params['env-id']); } + // Retrieve repository details from repo Id, if needed + if (isset($taskConfig['retrieve-repo-from-repo-id']) and $taskConfig['retrieve-repo-from-repo-id']) { + $this->repoController->getAllById($this->params['repo-id']); + } + // Retrieve repository details from snap Id, if needed if (isset($taskConfig['retrieve-repo-from-snap-id']) and $taskConfig['retrieve-repo-from-snap-id']) { $this->repoController->getAllById(null, $this->params['snap-id'], null); @@ -247,6 +252,7 @@ public function paramsSet($requiredParams = [], $optionalParams = []) 'gpg-sign' => 'setGpgSign', 'env' => 'setEnv', 'description' => 'setDescription', + 'tags' => 'setTags', 'group' => 'setGroup', 'advanced-params' => 'setAdvancedParams' ]; @@ -283,10 +289,11 @@ public function start() : void // Update layout containers states $this->layoutContainerReloadController->reload('header/menu'); + $this->layoutContainerReloadController->reload('repos/kpi'); $this->layoutContainerReloadController->reload('repos/list'); - $this->layoutContainerReloadController->reload('tasks/list'); - $this->layoutContainerReloadController->reload('browse/list'); - $this->layoutContainerReloadController->reload('browse/actions'); + $this->layoutContainerReloadController->reload('tasks/tasks'); + $this->layoutContainerReloadController->reload('snapshot/kpi'); + $this->layoutContainerReloadController->reload('snapshot/list'); // Add current PHP execution PID to the PID file to make sure it can be killed with the stop button $this->taskController->addsubpid(getmypid()); @@ -315,10 +322,6 @@ public function end() : void // Calculate total duration $duration = Convert::microtimeToHuman(microtime(true) - $this->timeStart); - // Create one last step for total duration - $this->taskLogStepController->new('duration', 'DURATION'); - $this->taskLogStepController->none('Total duration: ' . $duration); - // Update duration $this->taskController->updateDuration($this->taskId, $duration); @@ -358,10 +361,12 @@ public function end() : void // Update layout containers states $this->layoutContainerReloadController->reload('header/menu'); + $this->layoutContainerReloadController->reload('repos/kpi'); $this->layoutContainerReloadController->reload('repos/list'); - $this->layoutContainerReloadController->reload('tasks/list'); - $this->layoutContainerReloadController->reload('browse/list'); - $this->layoutContainerReloadController->reload('browse/actions'); + $this->layoutContainerReloadController->reload('tasks/tasks'); + $this->layoutContainerReloadController->reload('tasks/log'); + $this->layoutContainerReloadController->reload('snapshot/kpi'); + $this->layoutContainerReloadController->reload('snapshot/list'); } catch (Exception $e) { throw new Exception('Error while ending task: ' . $e->getMessage()); } diff --git a/www/controllers/Task/Form/Create.php b/www/controllers/Task/Form/Create.php index c14ec20ea..36392f3a7 100644 --- a/www/controllers/Task/Form/Create.php +++ b/www/controllers/Task/Form/Create.php @@ -53,6 +53,9 @@ public function validate(array $formParams): void // Check description Param\Description::check($formParams['description']); + // Check tags + Param\Tags::check($formParams['tags']); + // Check group if (!empty($formParams['group'])) { Param\Group::check($formParams['group']); diff --git a/www/controllers/Task/Form/Delete.php b/www/controllers/Task/Form/Delete.php index 646bfa385..f81c2847a 100644 --- a/www/controllers/Task/Form/Delete.php +++ b/www/controllers/Task/Form/Delete.php @@ -4,37 +4,45 @@ use Exception; use Controllers\Repo\Repo; +use Controllers\Repo\Snapshot\Snapshot; use Controllers\History\Save as History; +use Controllers\Utils\Generate\Html\Label; class Delete { public function validate(array $formParams): void { $repoController = new Repo(); + $repoSnaphotController = new Snapshot(); // Check that the snapshot id is valid Param\Snapshot::checkId($formParams['snap-id']); - // Retrieve all repo data from the Id - $repoController->setSnapId($formParams['snap-id']); - $repoController->getAllById('', $formParams['snap-id'], ''); + // Check that the snapshot exists + if (!$repoSnaphotController->exists($formParams['snap-id'])) { + throw new Exception('Snapshot Id #' . $formParams['snap-id'] . ' does not exist'); + } - // Check that the repo exists - if ($repoController->existsSnapId($formParams['snap-id']) === false) { - throw new Exception('Snapshot Id ' . $formParams['snap-id'] . ' does not exist'); + // Check if the snapshot has a protected environment + if ($repoSnaphotController->hasProtectedEnv($formParams['snap-id'])) { + throw new Exception('Snapshot has a protected environment and cannot be deleted'); } + // Retrieve all repo data from the Id + // $repoController->setSnapId($formParams['snap-id']); + $repoController->getAllById('', $formParams['snap-id'], ''); + // Check scheduling parameters Param\Schedule::check($formParams['schedule']); // Add history if ($repoController->getPackageType() == 'rpm') { - History::set('Running task: delete repository snapshot ' . $repoController->getName() . '' . $repoController->getDateFormatted() . ''); + History::set('Running task: delete repository snapshot ' . Label::white($repoController->getName()) . '⸺' . Label::white($repoController->getDateFormatted())); } if ($repoController->getPackageType() == 'deb') { - History::set('Running task: delete repository snapshot ' . $repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection() . '' . $repoController->getDateFormatted() . ''); + History::set('Running task: delete repository snapshot ' . Label::white($repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection()) . '⸺' . Label::white($repoController->getDateFormatted())); } - unset($repoController); + unset($repoController, $repoSnaphotController); } } diff --git a/www/controllers/Task/Form/Duplicate.php b/www/controllers/Task/Form/Duplicate.php index 463d5f135..fdcf00674 100644 --- a/www/controllers/Task/Form/Duplicate.php +++ b/www/controllers/Task/Form/Duplicate.php @@ -5,6 +5,7 @@ use Exception; use Controllers\Repo\Repo; use Controllers\History\Save as History; +use Controllers\Utils\Generate\Html\Label; class Duplicate { @@ -17,10 +18,6 @@ public function validate(array $formParams): void // Check that the snapshot id is valid Param\Snapshot::checkId($formParams['snap-id']); - // Retrieve all repo data from the Id - $repoController->setSnapId($formParams['snap-id']); - $repoController->getAllById('', $formParams['snap-id'], ''); - // Check name Param\Name::check($formParams['name']); @@ -34,20 +31,27 @@ public function validate(array $formParams): void Param\Description::check($formParams['description']); } + // Check tags + Param\Tags::check($formParams['tags']); + // Check group if (!empty($formParams['group'])) { Param\Group::check($formParams['group']); } + // Retrieve all repo data from the Id + // $repoController->setSnapId($formParams['snap-id']); + $repoController->getAllById('', $formParams['snap-id'], ''); + // Check that a repo with the same name does not already exist if ($repoController->getPackageType() == 'rpm') { if ($rpmRepoController->isActive($formParams['name'], $repoController->getReleasever())) { - throw new Exception('' . $formParams['name'] . ' ❯ ' . $repoController->getReleasever() . ' repository already exists'); + throw new Exception(Label::white($formParams['name'] . ' ❯ ' . $repoController->getReleasever()) . ' repository already exists'); } } if ($repoController->getPackageType() == 'deb') { if ($debRepoController->isActive($formParams['name'], $repoController->getDist(), $repoController->getSection())) { - throw new Exception('' . $formParams['name'] . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection() . ' repo already exists'); + throw new Exception(Label::white($formParams['name'] . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection()) . ' repo already exists'); } } @@ -56,10 +60,10 @@ public function validate(array $formParams): void // Add history if ($repoController->getPackageType() == 'rpm') { - History::set('Running task: duplicate repository ' . $repoController->getName() . '' . $repoController->getDateFormatted() . '' . $formParams['name'] . ''); + History::set('Running task: duplicate repository ' . Label::white($repoController->getName()) . '⸺' . Label::white($repoController->getDateFormatted()) . ' ➡ ' . Label::white($formParams['name'])); } if ($repoController->getPackageType() == 'deb') { - History::set('Running task: duplicate repository ' . $repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection() . '' . $repoController->getDateFormatted() . '' . $formParams['name'] . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection() . ''); + History::set('Running task: duplicate repository ' . Label::white($repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection()) . '⸺' . Label::white($repoController->getDateFormatted()) . ' ➡ ' . Label::white($formParams['name'] . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection())); } unset($repoController, $rpmRepoController, $debRepoController); diff --git a/www/controllers/Task/Form/Env.php b/www/controllers/Task/Form/Env.php index 486fbb4b7..f67b01105 100644 --- a/www/controllers/Task/Form/Env.php +++ b/www/controllers/Task/Form/Env.php @@ -2,7 +2,9 @@ namespace Controllers\Task\Form; +use Exception; use Controllers\Repo\Repo; +use Controllers\Environment; use Controllers\Utils\Generate\Html\Label; use Controllers\History\Save as History; @@ -11,23 +13,28 @@ class Env public function validate(array $formParams): void { $repoController = new Repo(); + $envController = new Environment(); // Check that the snapshot id is valid Param\Snapshot::checkId($formParams['snap-id']); - // Retrieve all repo data from the Id - $repoController->setSnapId($formParams['snap-id']); - $repoController->getAllById('', $formParams['snap-id'], ''); - // Check environment Param\Environment::check($formParams['env']); - // Check description - Param\Description::check($formParams['description']); - // Check scheduling parameters Param\Schedule::check($formParams['schedule']); + // Check if the env is protected + foreach ($formParams['env'] as $env) { + if ($envController->isProtected($env)) { + throw new Exception('Environment ' . $env . ' is protected and cannot be moved'); + } + } + + // Retrieve all repo data from the Id + // $repoController->setSnapId($formParams['snap-id']); + $repoController->getAllById('', $formParams['snap-id'], ''); + // Add history $content = ''; foreach ($formParams['env'] as $env) { @@ -35,10 +42,10 @@ public function validate(array $formParams): void } if ($repoController->getPackageType() == 'rpm') { - History::set('Running task: point environment(s) ' . trim($content) . ' to repository ' . $repoController->getName() . '' . $repoController->getDateFormatted() . ''); + History::set('Running task: point environment(s) ' . trim($content) . ' to repository ' . Label::white($repoController->getName()) . '⸺' . Label::white($repoController->getDateFormatted())); } if ($repoController->getPackageType() == 'deb') { - History::set('Running task: point environment(s) ' . trim($content) . ' to repository ' . $repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection() . '' . $repoController->getDateFormatted() . ''); + History::set('Running task: point environment(s) ' . trim($content) . ' to repository ' . Label::white($repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection()) . '⸺' . Label::white($repoController->getDateFormatted())); } unset($repoController); diff --git a/www/controllers/Task/Form/Form.php b/www/controllers/Task/Form/Form.php index fab2243f5..8c6758160 100644 --- a/www/controllers/Task/Form/Form.php +++ b/www/controllers/Task/Form/Form.php @@ -3,9 +3,14 @@ namespace Controllers\Task\Form; use Exception; +use Controllers\User\User; use Controllers\Repo\Repo; +use Controllers\Environment; +use Controllers\Task\Target; use Controllers\Utils\Validate; -use Controllers\Repo\Environment; +use Controllers\Repo\Listing as RepoListing; +use Controllers\Repo\Environment as RepoEnvironment; +use Controllers\History\Save as History; use Controllers\Task\Scheduled as ScheduledTask; use Controllers\User\Permission\Repo as RepoPermission; @@ -16,70 +21,92 @@ class Form /** * Return the task form to the user according to his selection */ - public function get(string $action, array $repos) : string + public function get(string $action, array $repos): string { - $userController = new \Controllers\User\User(); + $userController = new User(); + $repoEnvController = new RepoEnvironment(); + $repoListingController = new RepoListing(); + $scheduledTaskController = new ScheduledTask(); + + // Get the list of email addresses to populate the recipient selector $usersEmail = $userController->getEmails(); + // Get current tags to populate the tag selector + $tags = $repoListingController->listTags(); + + // Initialize the form content $content = '
    '; + // Add each repository form to the content foreach ($repos as $repo) { $repoController = new Repo(); - $repoEnvController = new Environment(); - $scheduledTaskController = new ScheduledTask(); - $repoId = Validate::int($repo['repo-id']); - $snapId = Validate::int($repo['snap-id']); + $scheduledTasksCount = 0; + $repoId = null; + $snapId = null; $envId = null; - // If an environment points to the snapshot (snapId), retrieve the envId from the repo array - if (!empty($repo['envId'])) { - $envId = Validate::int($repo['env-id']); + if (empty($repo['repo-id'])) { + throw new Exception('Repository Id is required'); } - // Check that the Ids are numeric - if (!is_numeric($repoId)) { - throw new Exception("Repository Id is invalid"); + if (!is_numeric($repo['repo-id'])) { + throw new Exception('Repository Id is invalid'); } - if (!is_numeric($snapId)) { - throw new Exception("Snapshot Id is invalid"); + + $repoId = Validate::int($repo['repo-id']); + + // If a snapshot Id is provided + if (!empty($repo['snap-id'])) { + if (!is_numeric($repo['snap-id'])) { + throw new Exception('Snapshot Id is invalid'); + } + + $snapId = Validate::int($repo['snap-id']); } - if (!empty($envId) and !is_numeric($envId)) { - throw new Exception("Environment Id is invalid"); + + // If an environment points to the snapshot (snapId), retrieve the envId from the repo array + if (!empty($repo['env-id'])) { + if (!is_numeric($repo['env-id'])) { + throw new Exception('Environment Id is invalid'); + } + + $envId = Validate::int($repo['env-id']); } // Check that the Ids exist in the database if (!$repoController->existsId($repoId)) { throw new Exception("Repository Id does not exist"); } - if (!$repoController->existsSnapId($snapId)) { + if (!empty($snapId) and !$repoController->existsSnapId($snapId)) { throw new Exception("Snapshot Id does not exist"); } if (!empty($envId) and !$repoEnvController->exists($envId)) { throw new Exception("Environment Id does not exist"); } + // Retrieve all repo data from the Ids $repoController->getAllById($repoId, $snapId, $envId); // Retrieve the package type of the repo $packageType = $repoController->getPackageType(); - // Get scheduled tasks on this snapshot (if any) and count them - $scheduledTasks = $scheduledTaskController->getBySnapId($snapId); - $scheduledTasksCount = count($scheduledTasks); + // Get scheduled tasks on the snapshot (if any) and count them + if (!empty($snapId)) { + $scheduledTasks = $scheduledTaskController->getBySnapId($snapId); + $scheduledTasksCount = count($scheduledTasks); + } // Build the form from a template ob_start(); - echo '
    '; + echo '
    '; // Include form template include(ROOT . '/views/includes/forms/tasks/' . $action . '.inc.php'); echo '
    '; - echo '

    '; - $content .= ob_get_clean(); } @@ -130,4 +157,74 @@ public function validate(array $tasksParams) : void $controller->validate($task); } } + + /** + * Validate a task targeting a dynamic set of repositories (all latest snapshots matching filters) + * Such a task has no fixed snapshot, so the per-action validators cannot be used here + * @param array $taskParams + */ + public function validateTarget(array $taskParams): array + { + $targetController = new Target(); + $envController = new Environment(); + + // Retrieve action + if (empty($taskParams['action'])) { + throw new Exception('No action has been specified'); + } + + if (!in_array($taskParams['action'], $targetController->getValidActions())) { + throw new Exception('This action cannot target all repositories: ' . $taskParams['action']); + } + + // If the user does not have permission to perform the specified action, prevent creation of the task + if (!RepoPermission::allowedAction($taskParams['action'])) { + throw new Exception('You are not allowed to execute this action'); + } + + if (empty($taskParams['target'])) { + throw new Exception('No target has been specified'); + } + + // Validate and clean the target definition + $taskParams['target'] = $targetController->validate($taskParams['target']); + + // A dynamic task is only useful when scheduled + if (empty($taskParams['schedule']['scheduled']) or $taskParams['schedule']['scheduled'] != 'true') { + throw new Exception('A task targeting all repositories must be scheduled'); + } + + // Check scheduling parameters + Param\Schedule::check($taskParams['schedule']); + + // Environment is required for the 'env' action + if ($taskParams['action'] == 'env') { + Param\Environment::check($taskParams['env'] ?? []); + + // Check if the env is protected + foreach ($taskParams['env'] as $env) { + if ($envController->isProtected($env)) { + throw new Exception('Environment ' . $env . ' is protected and cannot be moved'); + } + } + } + + // Environment is optional for the 'update' action + if ($taskParams['action'] == 'update' and !empty($taskParams['env'])) { + Param\Environment::check($taskParams['env']); + } + + // Check GPG parameters + if (in_array($taskParams['action'], ['update', 'rebuild'])) { + Param\GpgSign::check($taskParams['gpg-sign'] ?? 'false'); + } + + if ($taskParams['action'] == 'update') { + Param\GpgCheck::check($taskParams['gpg-check'] ?? 'false'); + } + + History::set('Scheduling task: ' . $taskParams['action'] . ' on ' . Target::describe($taskParams['target']) . ''); + + return $taskParams; + } } diff --git a/www/controllers/Task/Form/Param/Environment.php b/www/controllers/Task/Form/Param/Environment.php index 1fccee83b..1a0146237 100644 --- a/www/controllers/Task/Form/Param/Environment.php +++ b/www/controllers/Task/Form/Param/Environment.php @@ -6,20 +6,27 @@ class Environment { - public static function check(array $envs) : void + public static function check(array $envs): void { - $myenv = new \Controllers\Environment(); + $envController = new \Controllers\Environment(); if (empty($envs)) { throw new Exception('Environment must be specified'); } foreach ($envs as $env) { - if (!$myenv->exists($env)) { + if (!$envController->exists($env)) { throw new Exception('Specified environment does not exist'); } } - unset($envs, $myenv); + // Check that the environment is not protected + foreach ($envs as $env) { + if ($envController->isProtected($env)) { + throw new Exception('Environment ' . $env . ' is protected and cannot be modified'); + } + } + + unset($envs, $envController); } } diff --git a/www/controllers/Task/Form/Param/Repo.php b/www/controllers/Task/Form/Param/Repo.php new file mode 100644 index 000000000..55af7379a --- /dev/null +++ b/www/controllers/Task/Form/Param/Repo.php @@ -0,0 +1,20 @@ +existsId($id)) { + throw new Exception('Specified repository does not exist'); + } + + unset($repoController); + } +} diff --git a/www/controllers/Task/Form/Param/Schedule.php b/www/controllers/Task/Form/Param/Schedule.php index 0f21b78d5..3f69166e1 100644 --- a/www/controllers/Task/Form/Param/Schedule.php +++ b/www/controllers/Task/Form/Param/Schedule.php @@ -320,7 +320,11 @@ public static function clean(array $params): array } // Remove recipient if no alert or reminder is set - if ($params['schedule']['schedule-notify-error'] == 'false' && $params['schedule']['schedule-notify-success'] == 'false' && empty($params['schedule']['schedule-reminder'])) { + $notifyOnError = $params['schedule']['schedule-notify-error'] ?? 'false'; + $notifyOnSuccess = $params['schedule']['schedule-notify-success'] ?? 'false'; + $reminders = $params['schedule']['schedule-reminder'] ?? []; + + if ($notifyOnError == 'false' && $notifyOnSuccess == 'false' && empty($reminders)) { unset($params['schedule']['schedule-recipient']); } diff --git a/www/controllers/Task/Form/Param/Tags.php b/www/controllers/Task/Form/Param/Tags.php new file mode 100644 index 000000000..09768bf07 --- /dev/null +++ b/www/controllers/Task/Form/Param/Tags.php @@ -0,0 +1,22 @@ +setSnapId($formParams['snap-id']); - $repoController->getAllById('', $formParams['snap-id'], ''); - // Check gpg sign Param\GpgSign::check($formParams['gpg-sign']); // Check scheduling parameters Param\Schedule::check($formParams['schedule']); + // Check that the snapshot exists + if (!$repoSnaphotController->exists($formParams['snap-id'])) { + throw new Exception('Snapshot Id #' . $formParams['snap-id'] . ' does not exist'); + } + + // Check if the snapshot has a protected environment + if ($repoSnaphotController->hasProtectedEnv($formParams['snap-id'])) { + throw new Exception('Snapshot has a protected environment and cannot be deleted'); + } + + // Retrieve all repo data from the Id + $repoController->getAllById('', $formParams['snap-id'], ''); + // Add history if ($repoController->getPackageType() == 'rpm') { - History::set('Running task: rebuild repository metadata files of ' . $repoController->getName() . '' . $repoController->getDateFormatted() . ''); + History::set('Running task: rebuild repository metadata files of ' . Label::white($repoController->getName()) . '⸺' . Label::white($repoController->getDateFormatted())); } if ($repoController->getPackageType() == 'deb') { - History::set('Running task: rebuild repository metadata files of ' . $repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection() . '' . $repoController->getDateFormatted() . ''); + History::set('Running task: rebuild repository metadata files of ' . Label::white($repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection()) . '⸺' . Label::white($repoController->getDateFormatted())); } unset($repoController); diff --git a/www/controllers/Task/Form/RemoveEnv.php b/www/controllers/Task/Form/RemoveEnv.php index 37a0c6303..03c0a086a 100644 --- a/www/controllers/Task/Form/RemoveEnv.php +++ b/www/controllers/Task/Form/RemoveEnv.php @@ -2,14 +2,18 @@ namespace Controllers\Task\Form; +use Exception; use Controllers\Repo\Repo; +use Controllers\Environment; use Controllers\History\Save as History; +use Controllers\Utils\Generate\Html\Label; class RemoveEnv { public function validate(array $formParams): void { $repoController = new Repo(); + $envController = new Environment(); // Check that the snapshot id is valid Param\Snapshot::checkId($formParams['snap-id']); @@ -17,12 +21,17 @@ public function validate(array $formParams): void // Retrieve all repo data from the Id $repoController->getAllById($formParams['repo-id'], $formParams['snap-id'], $formParams['env-id']); + // Check if the env is protected + if ($envController->isProtected($repoController->getEnv())) { + throw new Exception('Environment ' . $repoController->getEnv() . ' is protected and cannot be removed'); + } + // Add history if ($repoController->getPackageType() == 'rpm') { - History::set('Running task: remove ' . $repoController->getEnv() . ' environment from ' . $repoController->getName() . '' . $repoController->getDateFormatted() . ''); + History::set('Running task: remove ' . $repoController->getEnv() . ' environment from ' . Label::white($repoController->getName()) . '⸺' . Label::white($repoController->getDateFormatted())); } if ($repoController->getPackageType() == 'deb') { - History::set('Running task: remove ' . $repoController->getEnv() . ' environment from ' . $repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection() . '' . $repoController->getDateFormatted() . ''); + History::set('Running task: remove ' . $repoController->getEnv() . ' environment from ' . Label::white($repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection()) . '⸺' . Label::white($repoController->getDateFormatted())); } unset($repoController); diff --git a/www/controllers/Task/Form/Rename.php b/www/controllers/Task/Form/Rename.php index ba5b560df..6ebe75975 100644 --- a/www/controllers/Task/Form/Rename.php +++ b/www/controllers/Task/Form/Rename.php @@ -17,23 +17,26 @@ public function validate(array $formParams): void $rpmRepoController = new Rpm(); $debRepoController = new Deb(); - // Check that the snapshot id is valid - Param\Snapshot::checkId($formParams['snap-id']); + // Check that the repo id is valid + Param\Repo::checkId($formParams['repo-id']); - // Retrieve all repo data from the Id - $repoController->getAllById('', $formParams['snap-id'], ''); + // Check name + Param\Name::check($formParams['name']); // Check old name Param\Name::check($formParams['old-name']); + // Check scheduling parameters + Param\Schedule::check($formParams['schedule']); + + // Retrieve all repo data from the Id + $repoController->getAllById($formParams['repo-id']); + // Old name must match the current name of the repository being renamed, otherwise someone is trying to doing something bad if ($formParams['old-name'] != $repoController->getName()) { throw new Exception('The old repository name does not match the name of the repository being renamed'); } - // Check name - Param\Name::check($formParams['name']); - // Check that a repo with the same name does not already exist if ($repoController->getPackageType() == 'rpm') { if ($rpmRepoController->isActive($formParams['name'], $repoController->getReleasever())) { @@ -46,15 +49,12 @@ public function validate(array $formParams): void } } - // Check scheduling parameters - Param\Schedule::check($formParams['schedule']); - // Add history if ($repoController->getPackageType() == 'rpm') { - History::set('Running task: renaming repository ' . Label::white($repoController->getName()) . '⸺' . Label::black($repoController->getDateFormatted()) . ' ➡ ' . Label::white($formParams['name'])); + History::set('Running task: renaming repository ' . Label::white($repoController->getName()) . ' ➡ ' . Label::white($formParams['name'])); } if ($repoController->getPackageType() == 'deb') { - History::set('Running task: renaming repository ' . Label::white($repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection()) . '⸺' . Label::black($repoController->getDateFormatted()) . ' ➡ ' . Label::white($formParams['name'] . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection())); + History::set('Running task: renaming repository ' . Label::white($repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection()) . ' ➡ ' . Label::white($formParams['name'] . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection())); } unset($repoController, $rpmRepoController, $debRepoController); diff --git a/www/controllers/Task/Form/Update.php b/www/controllers/Task/Form/Update.php index 82e25bbbb..05c1c8e46 100644 --- a/www/controllers/Task/Form/Update.php +++ b/www/controllers/Task/Form/Update.php @@ -2,8 +2,11 @@ namespace Controllers\Task\Form; +use Exception; use Controllers\Repo\Repo; +use Controllers\Repo\Snapshot\Snapshot; use Controllers\History\Save as History; +use Controllers\Utils\Generate\Html\Label; class Update { @@ -14,8 +17,17 @@ public function validate(array $formParams): void // Check that the snapshot id is valid Param\Snapshot::checkId($formParams['snap-id']); + // Check architecture + Param\Arch::check($formParams['arch']); + + // Check gpg sign + Param\GpgSign::check($formParams['gpg-sign']); + + // Check scheduling parameters + Param\Schedule::check($formParams['schedule']); + // Retrieve all repo data from the Id - $repoController->setSnapId($formParams['snap-id']); + // $repoController->setSnapId($formParams['snap-id']); $repoController->getAllById('', $formParams['snap-id'], ''); // Check env @@ -23,9 +35,6 @@ public function validate(array $formParams): void Param\Environment::check($formParams['env']); } - // Check architecture - Param\Arch::check($formParams['arch']); - // Case of a mirror repository, check additional parameters if ($repoController->getType() == 'mirror') { // Check keep latest versions of packages @@ -55,18 +64,12 @@ public function validate(array $formParams): void } } - // Check gpg sign - Param\GpgSign::check($formParams['gpg-sign']); - - // Check scheduling parameters - Param\Schedule::check($formParams['schedule']); - // Add history if ($repoController->getPackageType() == 'rpm') { - History::set('Running task: update ' . $repoController->getType() . ' repository ' . $repoController->getName() . ''); + History::set('Running task: update ' . $repoController->getType() . ' repository ' . Label::white($repoController->getName())); } if ($repoController->getPackageType() == 'deb') { - History::set('Running task: update ' . $repoController->getType() . ' repository ' . $repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection() . ''); + History::set('Running task: update ' . $repoController->getType() . ' repository ' . Label::white($repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection())); } unset($repoController); diff --git a/www/controllers/Task/Listing.php b/www/controllers/Task/Listing.php index acfcb013c..9d4b7db8b 100644 --- a/www/controllers/Task/Listing.php +++ b/www/controllers/Task/Listing.php @@ -59,4 +59,75 @@ public function getDone(string $type = 'immediate', bool $withOffset = false, in { return $this->model->getDone($type, $withOffset, $offset); } + + /** + * Get all sub-tasks generated by a parent task + * It is possible to add an offset to the request + */ + public function getByParentId(int $parentTaskId, bool $withOffset = false, int $offset = 0): array + { + return $this->model->getByParentId($parentTaskId, $withOffset, $offset); + } + + /** + * Count sub-tasks generated by a parent task, grouped by status + */ + public function countStatusByParentId(int $parentTaskId): array + { + return $this->model->countStatusByParentId($parentTaskId); + } + + /** + * Return a summary of the sub-tasks statuses of a parent task + * A parent task has no status of its own as it only dispatches sub-tasks, so its real status is a summary of theirs + * Return an empty array if the task has no sub-task + */ + public function getSubTasksSummary(int $parentTaskId): array + { + $count = $this->countStatusByParentId($parentTaskId); + + if (empty($count)) { + return []; + } + + return $this->buildSubTasksSummary($count); + } + + /** + * Same as getSubTasksSummary() but for multiple parent tasks at once + * Tasks having no sub-task are not returned + */ + public function getSubTasksSummaries(array $parentTaskIds): array + { + $summaries = []; + + foreach ($this->model->countStatusByParentIds($parentTaskIds) as $parentTaskId => $count) { + $summaries[$parentTaskId] = $this->buildSubTasksSummary($count); + } + + return $summaries; + } + + /** + * Build a sub-tasks summary from a count of sub-tasks per status + */ + private function buildSubTasksSummary(array $count): array + { + $summary = [ + 'total' => array_sum($count), + 'success' => $count['done'] ?? 0, + 'failed' => ($count['error'] ?? 0) + ($count['stopped'] ?? 0), + 'ongoing' => ($count['running'] ?? 0) + ($count['queued'] ?? 0) + ]; + + if ($summary['ongoing'] > 0) { + $summary['status'] = 'running'; + } elseif ($summary['failed'] > 0) { + $summary['status'] = 'error'; + } else { + $summary['status'] = 'done'; + } + + return $summary; + } } diff --git a/www/controllers/Task/Target.php b/www/controllers/Task/Target.php new file mode 100644 index 000000000..709b66fd1 --- /dev/null +++ b/www/controllers/Task/Target.php @@ -0,0 +1,259 @@ +validActions; + } + + /** + * Return true if the specified task parameters target a dynamic set of repositories + */ + public static function isDynamic(array $taskParams): bool + { + return !empty($taskParams['target']) and is_array($taskParams['target']); + } + + /** + * Validate and clean a target definition + * Returns the sanitized target + */ + public function validate(array $target): array + { + $groupId = ''; + $tags = []; + $packageType = ''; + + /** + * Check group, if any + * The group Id is stored instead of its name, so that renaming a group has no impact on the target + * Group Id 0 is a fictitious group ('Default') holding the repositories that do not belong to any group + */ + if (isset($target['group']) and $target['group'] !== '') { + if (!is_numeric($target['group'])) { + throw new Exception('Invalid target group Id'); + } + + $groupId = strval(intval($target['group'])); + + if ($groupId !== '0') { + $groupController = new \Controllers\Group\Repo(); + + if (!$groupController->existsId(intval($groupId))) { + throw new Exception('Target group does not exist: #' . $groupId); + } + } + } + + // Check tags, if any + if (!empty($target['tags'])) { + if (!is_array($target['tags'])) { + throw new Exception('Invalid target tags'); + } + + foreach ($target['tags'] as $tag) { + if (!is_string($tag)) { + throw new Exception('Invalid target tag'); + } + + $tag = trim($tag); + + if ($tag === '') { + continue; + } + + // Tags are stored as a comma-separated string, so a tag cannot contain a comma + if (str_contains($tag, ',')) { + throw new Exception('Invalid target tag: ' . $tag); + } + + $tags[] = $tag; + } + } + + // Check package type, if any + if (!empty($target['package-type'])) { + if (!in_array($target['package-type'], ['deb', 'rpm'])) { + throw new Exception('Invalid target package type'); + } + + $packageType = $target['package-type']; + } + + return [ + 'group' => $groupId, + 'tags' => $tags, + 'package-type' => $packageType + ]; + } + + /** + * Return the list of repositories matching the target, with their latest active snapshot Id + */ + public function resolve(array $target): array + { + $target = $this->validate($target); + + $repoListingController = new RepoListing(); + + return $repoListingController->listByTarget($target['group'], $target['tags'], $target['package-type']); + } + + /** + * Return a human readable description of the target + * The group name is resolved from its Id, so the description always reflects the current name + */ + public static function describe(array $target): string + { + $filters = []; + + if (isset($target['group']) and $target['group'] !== '') { + /** + * Keep markup in the template, but escape the group name itself + * because it comes from data and may contain HTML special characters + */ + $filters[] = 'group ' . htmlspecialchars(self::groupName(intval($target['group'])), ENT_QUOTES) . ''; + } + + if (!empty($target['tags'])) { + /** + * Escape each tag value before injecting it in the HTML-formatted + * description. This preserves the surrounding tags while ensuring + * tag content is displayed as text. + */ + $escapedTags = array_map(function ($tag) { + return htmlspecialchars($tag, ENT_QUOTES); + }, $target['tags']); + + $filters[] = 'tag' . (count($escapedTags) > 1 ? 's' : '') . ' #' . implode(' #', $escapedTags) . ''; + } + + if (!empty($target['package-type'])) { + $filters[] = 'package type ' . $target['package-type']; + } + + if (empty($filters)) { + return 'All repositories (latest snapshots)'; + } + + return 'All repositories with ' . implode(' and ', $filters) . ' (latest snapshots)'; + } + + /** + * Return the current name of a group from its Id + * Group Id 0 is the fictitious 'Default' group + */ + private static function groupName(int $groupId): string + { + if ($groupId === 0) { + return 'Default'; + } + + try { + $groupController = new \Controllers\Group\Repo(); + + return $groupController->getNameById(intval($groupId)); + } catch (Exception $e) { + // The group may have been deleted since the task was scheduled + return '#' . $groupId . ' (deleted)'; + } + } + + /** + * Expand a dynamic task into a list of concrete task parameters, one per matching repository + * Each generated task targets the latest snapshot of the repository and is meant to be executed immediately + */ + public function expand(array $taskParams): array + { + $tasksParams = []; + + if (empty($taskParams['action'])) { + throw new Exception('No action has been specified'); + } + + $action = $taskParams['action']; + + if (!in_array($action, $this->validActions)) { + throw new Exception('Action does not support a dynamic target: ' . $action); + } + + if (empty($taskParams['target'])) { + throw new Exception('No target has been specified'); + } + + // Retrieve all the repositories matching the target + $repos = $this->resolve($taskParams['target']); + + foreach ($repos as $repo) { + $params = [ + 'action' => $action, + 'repo-id' => strval($repo['repoId']), + 'snap-id' => strval($repo['snapId']), + 'env-id' => '', + + // Generated tasks are executed immediately, the recurrence is carried by the parent task + 'schedule' => [ + 'scheduled' => 'false' + ] + ]; + + /** + * Retrieve the current repository settings, as some parameters cannot be known + * when the task is scheduled (e.g. the architectures of a repository created later) + */ + $repoController = new Repo(); + $repoController->getAllById(null, $repo['snapId'], null); + + if ($action == 'update') { + // Each repository is updated with its own current architectures and advanced parameters + $params['arch'] = $repoController->getArch(); + $params['advanced-params'] = $repoController->getAdvancedParams(); + $params['gpg-sign'] = $taskParams['gpg-sign'] ?? 'false'; + + // GPG check only applies to mirror repositories + if ($repoController->getType() == 'mirror') { + $params['gpg-check'] = $taskParams['gpg-check'] ?? 'false'; + } + + // Environments to point to the new snapshot are optional + if (!empty($taskParams['env'])) { + $params['env'] = $taskParams['env']; + } + } + + if ($action == 'env') { + $params['env'] = $taskParams['env'] ?? []; + } + + if ($action == 'rebuild') { + $params['gpg-sign'] = $taskParams['gpg-sign'] ?? 'false'; + } + + unset($repoController); + + $tasksParams[] = $params; + } + + return $tasksParams; + } +} diff --git a/www/controllers/Task/Task.php b/www/controllers/Task/Task.php index a0deb4abc..9f8a7802f 100644 --- a/www/controllers/Task/Task.php +++ b/www/controllers/Task/Task.php @@ -2,10 +2,13 @@ namespace Controllers\Task; +use Controllers\User\Permission\Task as TaskPermission; use Controllers\Task\Form\Param\Schedule; +use Controllers\History\Save as History; use Controllers\Task\Log\SubStep; use Controllers\Task\Log\Step; use Controllers\Utils\Cron; +use Controllers\Repo\Repo; use Controllers\Process; use JsonException; use Exception; @@ -179,10 +182,11 @@ public function somethingRunning() /** * Return repository from task Id + * Return string if $string is true, otherwise return an array with the repository details */ - public function getRepo(int $id): string + public function getRepo(int $id, $string = true): string|array { - $repoController = new \Controllers\Repo\Repo(); + $repoController = new Repo(); try { // Retrieve task informations @@ -194,6 +198,22 @@ public function getRepo(int $id): string throw new Exception('could not decode task parameters: ' . $e->getMessage()); } + // Case where the task targets a dynamic set of repositories, there is no fixed repository + if (Target::isDynamic($taskRawParams)) { + unset($repoController); + + $description = Target::describe($taskRawParams['target']); + + if ($string) { + return $description; + } + + return [ + 'type' => 'target', + 'name' => $description + ]; + } + if (!empty($taskRawParams['source-snap-id'])) { if (is_numeric($taskRawParams['source-snap-id'])) { $repoController->getAllById('', $taskRawParams['source-snap-id'], ''); @@ -248,15 +268,38 @@ public function getRepo(int $id): string } } + unset($repoController); + if (!empty($dist) and !empty($component)) { - return $name . ' ❯ ' . $dist . ' ❯ ' . $component; + $repo = [ + 'type' => 'deb', + 'name' => $name, + 'dist' => $dist, + 'component' => $component + ]; + + if ($string) { + return $name . ' ❯ ' . $dist . ' ❯ ' . $component; + } } if (!empty($releasever)) { - return $name . ' ❯ ' . $releasever; + $repo = [ + 'type' => 'rpm', + 'name' => $name, + 'releasever' => $releasever + ]; + + if ($string) { + return $name . ' ❯ ' . $releasever; + } } - throw new Exception('unknown repository'); + if (empty($repo)) { + throw new Exception('unknown repository'); + } + + return $repo; } catch (Exception $e) { return 'Error retrieving repository: ' . strtolower($e->getMessage()); } @@ -265,8 +308,9 @@ public function getRepo(int $id): string /** * Add a new task in database * @param array $params + * @param int|null $parentTaskId Id of the task that generated this task, if any (e.g. a scheduled task targeting a group of repositories) */ - private function new(array $params) : int + private function new(array $params, int|null $parentTaskId = null) : int { /** * Default values @@ -316,15 +360,16 @@ private function new(array $params) : int /** * Add the task in database */ - $taskId = $this->model->new($type, $paramsJson, $status); + $taskId = $this->model->new($type, $paramsJson, $status, $parentTaskId); return $taskId; } /** * Execute one or more tasks + * @param int|null $parentTaskId Id of the task that generated these tasks, if any (e.g. a scheduled task targeting a group of repositories) */ - public function execute(array $tasksParams): int|array + public function execute(array $tasksParams, int|null $parentTaskId = null): int|array { $tasks = []; @@ -344,7 +389,7 @@ public function execute(array $tasksParams): int|array $params['releasever'] = $releasever; // Generate a new task containing all the parameters needed to execute the task retrieve its Id - $taskId = $this->new($params); + $taskId = $this->new($params, $parentTaskId); // Execute the task now if it is not scheduled if ($params['schedule']['scheduled'] != 'true') { @@ -367,7 +412,7 @@ public function execute(array $tasksParams): int|array $params['section'] = $section; // Generate a new task containing all the parameters needed to execute the task retrieve its Id - $taskId = $this->new($params); + $taskId = $this->new($params, $parentTaskId); // Execute the task now if it is not scheduled if ($params['schedule']['scheduled'] != 'true') { @@ -383,7 +428,7 @@ public function execute(array $tasksParams): int|array // Every other task can be executed directly } else { // Generate a new task containing all the parameters needed to execute the task retrieve its Id - $taskId = $this->new($taskParams); + $taskId = $this->new($taskParams, $parentTaskId); // Execute the task now if it is not scheduled if ($taskParams['schedule']['scheduled'] != 'true') { @@ -426,7 +471,7 @@ public function exists(int $id) : bool */ public function relaunch(int $id) : void { - if (!IS_ADMIN and !in_array('relaunch', USER_PERMISSIONS['tasks']['allowed-actions'])) { + if (!TaskPermission::allowedAction('relaunch')) { throw new Exception('You are not allowed to relaunch a task'); } @@ -450,7 +495,7 @@ public function relaunch(int $id) : void $this->executeId($newTaskId); $this->layoutContainerReloadController->reload('tasks/logs'); - $this->layoutContainerReloadController->reload('tasks/list'); + $this->layoutContainerReloadController->reload('tasks/tasks'); } /** @@ -466,7 +511,7 @@ public function duplicate(int $id) : int */ public function stop(int $taskId): void { - if (!IS_ADMIN and !in_array('stop', USER_PERMISSIONS['tasks']['allowed-actions'])) { + if (!TaskPermission::allowedAction('stop')) { throw new Exception('You are not allowed to stop a task'); } @@ -537,8 +582,13 @@ public function stop(int $taskId): void // Update layout containers states $this->layoutContainerReloadController->reload('header/menu'); + $this->layoutContainerReloadController->reload('repos/kpi'); $this->layoutContainerReloadController->reload('repos/list'); - $this->layoutContainerReloadController->reload('tasks/list'); + $this->layoutContainerReloadController->reload('tasks/tasks'); + $this->layoutContainerReloadController->reload('tasks/log'); + + // Save history + History::set('Stopped task #' . $taskId); if (!empty($killError)) { throw new Exception($killError); @@ -612,7 +662,7 @@ public static function getChildrenPid(int $pid) : array|bool */ public function enable(array $tasksId): void { - if (!IS_ADMIN and !in_array('enable', USER_PERMISSIONS['tasks']['allowed-actions'])) { + if (!TaskPermission::allowedAction('enable')) { throw new Exception('You are not allowed to enable a task'); } @@ -651,7 +701,7 @@ public function enable(array $tasksId): void */ public function disable(array $tasksId) : void { - if (!IS_ADMIN and !in_array('disable', USER_PERMISSIONS['tasks']['allowed-actions'])) { + if (!TaskPermission::allowedAction('disable')) { throw new Exception('You are not allowed to disable a task'); } @@ -690,7 +740,7 @@ public function disable(array $tasksId) : void */ public function delete(array $tasksId) : void { - if (!IS_ADMIN and !in_array('delete', USER_PERMISSIONS['tasks']['allowed-actions'])) { + if (!TaskPermission::allowedAction('delete')) { throw new Exception('You are not allowed to delete a task'); } @@ -709,9 +759,8 @@ public function delete(array $tasksId) : void throw new Exception('Could not decode task #' . $id . ' JSON parameters: ' . $e->getMessage()); } - - // Check if task is a scheduled task - if ($taskRawParams['schedule']['scheduled'] != 'true') { + // Check if task is a scheduled task or a queued task, only these types of tasks can be deleted + if ($taskRawParams['schedule']['scheduled'] != 'true' and $task['Status'] != 'queued') { throw new Exception('Task #' . $id . ' is not a scheduled task and cannot be deleted'); } @@ -1007,4 +1056,77 @@ public function getLatestStatus(string $snapId) : array { return $this->model->getLatestStatus($snapId); } + + /** + * Generate a human readable literal action from the technical action name + */ + public static function generateLiteralAction(string $action): string + { + if ($action == 'create') { + return 'Create repository'; + } + if ($action == 'update') { + return 'Update repository'; + } + if ($action == 'env') { + return 'Point an environment'; + } + if ($action == 'removeEnv') { + return 'Remove an environment'; + } + if ($action == 'rebuild') { + return 'Rebuild metadata'; + } + if ($action == 'rename') { + return 'Rename repository'; + } + if ($action == 'duplicate') { + return 'Duplicate repository'; + } + if ($action == 'delete') { + return 'Delete snapshot'; + } + + return ucfirst($action); + } + + /** + * Generate a human readable literal action from the technical action name + */ + public static function generateLiteralAction2(array $task): string + { + // Try to decode the task parameters + try { + $taskRawParams = json_decode($task['Raw_params'], true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + return 'Error: could not decode task #' . $task['Id'] . ' JSON parameters: ' . $e->getMessage(); + } + + if ($taskRawParams['action'] == 'create') { + return $taskRawParams['repo-type'] == 'local' ? 'New local repository' : 'New mirror repository'; + } + if ($taskRawParams['action'] == 'update') { + return 'Update repository'; + } + if ($taskRawParams['action'] == 'env') { + return 'Point an environment'; + } + if ($taskRawParams['action'] == 'removeEnv') { + return 'Remove an environment'; + } + if ($taskRawParams['action'] == 'rebuild') { + return 'Rebuild repository metadata'; + } + if ($taskRawParams['action'] == 'rename') { + return 'Rename repository'; + } + if ($taskRawParams['action'] == 'duplicate') { + return 'Duplicate repository'; + } + if ($taskRawParams['action'] == 'delete') { + return 'Delete snapshot'; + } + + return ucfirst($taskRawParams['action']); + } } diff --git a/www/controllers/User/Create.php b/www/controllers/User/Create.php index e6d7cfc70..82abc3b69 100644 --- a/www/controllers/User/Create.php +++ b/www/controllers/User/Create.php @@ -13,7 +13,7 @@ public function __construct() { parent::__construct(); $this->model = new \Models\User\Create(); - $this->userPermissionController = new \Controllers\User\Permission(); + $this->userPermissionController = new \Controllers\User\Permission\Permission(); } /** diff --git a/www/controllers/User/Permission.php b/www/controllers/User/Permission/Permission.php similarity index 88% rename from www/controllers/User/Permission.php rename to www/controllers/User/Permission/Permission.php index bb13b794f..02b2a616c 100644 --- a/www/controllers/User/Permission.php +++ b/www/controllers/User/Permission/Permission.php @@ -1,19 +1,20 @@ [ 'allowed-actions' => [], 'view' => [ - 'all', - 'groups' => [] + // 'all', + 'groups' => [], + 'repos' => [] ], ], 'tasks' => [ @@ -129,6 +130,22 @@ public function set(int $id, array $reposView, array $reposActions, array $tasks // Add group Id key to permissions $permissions['repositories']['view']['groups'][] = $groupId; } + + /** + * If entry starts with 'repo-', then it's an individual repository + */ + if (strpos($repoOrGroup, 'repo-') === 0) { + // Get repository Id + $repoId = str_replace('repo-', '', $repoOrGroup); + + // Check that repository Id is valid + if (!is_numeric($repoId)) { + throw new Exception('Invalid repository Id: ' . $repoId); + } + + // Add repository Id key to permissions + $permissions['repositories']['view']['repos'][] = $repoId; + } } /** diff --git a/www/controllers/User/Permission/Repo.php b/www/controllers/User/Permission/Repo.php index 48fcd0b82..43cffc48e 100644 --- a/www/controllers/User/Permission/Repo.php +++ b/www/controllers/User/Permission/Repo.php @@ -4,6 +4,9 @@ class Repo { + /** + * Check if the user is allowed to perform a specific action on repositories + */ public static function allowedAction(string $action) : bool { // Admins are allowed to do everything @@ -17,4 +20,45 @@ public static function allowedAction(string $action) : bool return false; } + + /** + * Check if the user is allowed to view a specific repository + */ + public static function allowedToView(int $repoId) : bool + { + // Admins are allowed to view everything + if (IS_ADMIN) { + return true; + } + + if (in_array('all', USER_PERMISSIONS['repositories']['view'])) { + return true; + } + + // The repository can be granted individually + if (in_array($repoId, USER_PERMISSIONS['repositories']['view']['repos'])) { + return true; + } + + if (empty(USER_PERMISSIONS['repositories']['view']['groups'])) { + return false; + } + + $groupController = new \Controllers\Group\Repo(); + $groupsIds = $groupController->getRepoGroupsIds($repoId); + + // Repositories that are member of no group belong to the fictitious 'Default' group, which Id is 0 + if (empty($groupsIds)) { + $groupsIds = [0]; + } + + // Or granted through one of the groups it is member of + foreach ($groupsIds as $groupId) { + if (in_array($groupId, USER_PERMISSIONS['repositories']['view']['groups'])) { + return true; + } + } + + return false; + } } diff --git a/www/controllers/User/Preference/Preference.php b/www/controllers/User/Preference/Preference.php new file mode 100644 index 000000000..71feb7d2a --- /dev/null +++ b/www/controllers/User/Preference/Preference.php @@ -0,0 +1,180 @@ + [ + 'list' => [ + 'expand' => false, + 'one-line' => false + ] + ] + ]; + + public function __construct() + { + parent::__construct(); + $this->preferenceModel = new \Models\User\Preference(); + } + + /** + * Get user preferences + */ + public function get(int $id): array + { + // Check if user exists + if (!$this->existsId($id)) { + throw new Exception('User does not exist'); + } + + // Get user preferences + $preferences = $this->preferenceModel->get($id); + + // If preferences are empty, create default preferences and return them + if (empty($preferences)) { + try { + $this->preferenceModel->set($id, json_encode($this->defaultPreferences, JSON_THROW_ON_ERROR)); + } catch (JsonException $e) { + throw new Exception('Error setting default preferences: ' . $e->getMessage()); + } + + return $this->defaultPreferences; + } + + // Decode preferences (JSON) and return them + try { + $preferences = json_decode($preferences, true, 512, JSON_THROW_ON_ERROR); + } catch (Exception $e) { + throw new Exception('Error decoding preferences: ' . $e->getMessage()); + } + + // Merge with default preferences if some keys are missing + $preferences = array_replace_recursive($this->defaultPreferences, $preferences); + + return $preferences; + } + + /** + * Get default preferences definition + */ + public function getDefault(): array + { + return $this->defaultPreferences; + } + + /** + * Filter preferences to only keep keys that exist in the schema + */ + private function filterPreferencesBySchema(array $preferences, array $schema): array + { + $filtered = []; + + foreach ($schema as $key => $value) { + if (!isset($preferences[$key])) { + continue; + } + + if (is_array($value)) { + // Recursive filtering for nested arrays + $filtered[$key] = $this->filterPreferencesBySchema($preferences[$key], $value); + } else { + // Keep scalar values + $filtered[$key] = $preferences[$key]; + } + } + + return $filtered; + } + + /** + * Set user preferences + */ + public function set(int $id, array $preferences): void + { + // Check if user exists + if (!$this->existsId($id)) { + throw new Exception('User does not exist'); + } + + // Get current preferences (includes defaults for missing keys) + $currentPreferences = $this->get($id); + + // Update preferences using dot-separated keys + foreach ($preferences as $key => $value) { + $keys = explode('.', $key); + $lastKey = array_key_last($keys); + + // Navigate through the default preferences to find the expected type + $defaultValue = $this->defaultPreferences; + $isValidKey = true; + + foreach ($keys as $k) { + if (isset($defaultValue[$k])) { + $defaultValue = $defaultValue[$k]; + } else { + $isValidKey = false; + break; + } + } + + // Reject invalid keys (not in the schema) + if (!$isValidKey) { + throw new Exception('Invalid preference key: ' . $key); + } + + // Validate value type based on the default value's type + $expectedType = gettype($defaultValue); + + if ($expectedType === 'boolean') { + // For booleans, accept string 'true'/'false' or actual boolean + if (!is_bool($value) && !(is_string($value) && in_array($value, ['true', 'false'], true))) { + throw new Exception('Preference ' . $key . ' expects a boolean, got ' . gettype($value)); + } + + $validatedValue = is_bool($value) ? $value : Convert::toBool($value); + } elseif ($expectedType === 'array') { + // For arrays, accept arrays only + if (!is_array($value)) { + throw new Exception('Preference ' . $key . ' expects an array, got ' . gettype($value)); + } + + $validatedValue = $value; + } else { + // For strings, numbers, etc. - accept same type only + if (gettype($value) !== $expectedType) { + throw new Exception('Preference ' . $key . ' expects a ' . $expectedType . ', got ' . gettype($value)); + } + + $validatedValue = $value; + } + + // Set the validated value + $current = &$currentPreferences; + foreach ($keys as $i => $k) { + if ($i === $lastKey) { + $current[$k] = $validatedValue; + } else { + $current = &$current[$k]; + } + } + unset($current); + } + + // Filter preferences to only keep keys that exist in the schema + $currentPreferences = $this->filterPreferencesBySchema($currentPreferences, $this->defaultPreferences); + + // Save updated preferences + try { + $this->preferenceModel->set($id, json_encode($currentPreferences, JSON_THROW_ON_ERROR)); + } catch (JsonException $e) { + throw new Exception('Error encoding preferences: ' . $e->getMessage()); + } + } +} diff --git a/www/controllers/Utils/Generate/Html/Label.php b/www/controllers/Utils/Generate/Html/Label.php index e001fe973..dc40270f2 100644 --- a/www/controllers/Utils/Generate/Html/Label.php +++ b/www/controllers/Utils/Generate/Html/Label.php @@ -7,36 +7,55 @@ class Label /** * Generate environment tag */ - public static function envtag(string $name, string|null $css = null) : string + public static function envtag(string $name, $additionalCss = ''): string { + $name = trim($name); + // Default class and colors - $class = 'env'; $color = '#000000'; $background = '#ffffff'; $border = ''; + $protected = false; // Retrieve color from ENVS array if (defined('ENVS')) { foreach (ENVS as $env) { - if ($env['Name'] == $name and !empty($env['Color'])) { - $background = $env['Color']; - // Get contrasting text color - $color = Color::contrastingText($background); + if (($env['Name'] ?? '') === $name) { + if (!empty($env['Color'])) { + $background = $env['Color']; + // Get contrasting text color + $color = Color::contrastingText($background); + } + + $protected = ($env['Protected'] ?? 'false') === 'true'; + break; } } } + // Outlined style: transparent bg, colored border and text if ($background == '#ffffff') { - $border = '1px solid #949494'; + // No color configured: use a subtle gray outline + $color = '#c0d0e2'; + $border = '1.5px solid #c0d0e2'; } else { - $border = '1px solid ' . $background; + // Use the configured color for border and text + $color = $background; + $border = '1.5px solid ' . $background; } - if ($css == 'fit') { - $class = 'env-fit'; + $style = 'background-color: transparent; color: ' . $color . '; border: ' . $border; + + $content = ''; + + // If environment is protected, add a lock icon + if ($protected) { + $content .= ''; } - return '' . $name . ''; + $content .= '' . $name . ''; + + return $content; } /** @@ -46,12 +65,4 @@ public static function white(string $string): string { return '' . $string . ''; } - - /** - * Generate black label - */ - public static function black(string $string): string - { - return '' . $string . ''; - } } diff --git a/www/controllers/Websocket/BrowserClient/Process.php b/www/controllers/Websocket/BrowserClient/Process.php index 3586a2cf6..2c5c8e3ec 100644 --- a/www/controllers/Websocket/BrowserClient/Process.php +++ b/www/controllers/Websocket/BrowserClient/Process.php @@ -20,15 +20,21 @@ public function reload($socket) return; } + // Keep track of what has been broadcasted to avoid deleting containers that have been added while broadcasting + $broadcasted = []; + // For each container, send a reload request to all browser clients foreach ($containers as $container) { $this->broadcast($socket, 'browser-client', array( 'type' => 'reload-container', 'container' => $container['Container'] )); + + $broadcasted[] = $container['Container']; } - // Clean up the layout container state - $this->layoutContainerReloadController->clean(); + // Only remove what has just been broadcasted: a task may have requested a reload while + // broadcasting, and it must be kept so it is sent on the next round + $this->layoutContainerReloadController->delete($broadcasted); } } diff --git a/www/controllers/ajax/environment.php b/www/controllers/ajax/environment.php index 1a65de9e8..0ed872980 100644 --- a/www/controllers/ajax/environment.php +++ b/www/controllers/ajax/environment.php @@ -1,27 +1,25 @@ add($_POST['name'], $_POST['color']); + $envController->add($_POST['name'], $_POST['color']); } catch (Exception $e) { response(HTTP_BAD_REQUEST, $e->getMessage()); } - response(HTTP_OK, 'Environment ' . $_POST['name'] . ' has been added'); + response(HTTP_OK, 'Environment ' . $_POST['name'] . ' added'); } /** * Delete an environment */ -if ($action == 'delete-env' and !empty($_POST['id'])) { - $myenv = new \Controllers\Environment(); - +if ($action == 'delete' and !empty($_POST['id'])) { try { - $myenv->delete($_POST['id']); + $envController->delete($_POST['id']); } catch (Exception $e) { response(HTTP_BAD_REQUEST, $e->getMessage()); } @@ -30,18 +28,16 @@ } /** - * Add / edit actual envs + * Edit environment(s) */ -if ($action == 'edit-env' and !empty($_POST['envs'])) { - $myenv = new \Controllers\Environment(); - +if ($action == 'edit' and !empty($_POST['envs'])) { try { - $myenv->edit($_POST['envs']); + $envController->edit($_POST['envs']); } catch (Exception $e) { response(HTTP_BAD_REQUEST, $e->getMessage()); } - response(HTTP_OK, 'Environments edited'); + response(HTTP_OK, 'Environment' . (count($_POST['envs']) > 1 ? 's' : '') . ' updated'); } response(HTTP_BAD_REQUEST, 'Invalid action'); diff --git a/www/controllers/ajax/hosts/settings.php b/www/controllers/ajax/hosts/settings.php index 79d671d2a..72e9e1b80 100644 --- a/www/controllers/ajax/hosts/settings.php +++ b/www/controllers/ajax/hosts/settings.php @@ -2,11 +2,11 @@ /** * Edit hosts settings */ -if ($action == 'edit' and isset($_POST['complianceThresholdCount']) and isset($_POST['complianceThresholdDays']) and isset($_POST['complianceRebootRequired'])) { +if ($action == 'edit' and isset($_POST['complianceThresholdCount']) and isset($_POST['complianceThresholdDays']) and isset($_POST['complianceSecurityUpdate']) and isset($_POST['complianceRebootRequired'])) { $hostController = new \Controllers\Host\Host(); try { - $hostController->setSettings($_POST['complianceThresholdCount'], $_POST['complianceThresholdDays'], $_POST['complianceRebootRequired']); + $hostController->setSettings($_POST['complianceThresholdCount'], $_POST['complianceThresholdDays'], $_POST['complianceSecurityUpdate'], $_POST['complianceRebootRequired']); } catch (Exception $e) { response(HTTP_BAD_REQUEST, $e->getMessage()); } diff --git a/www/controllers/ajax/repo/edit.php b/www/controllers/ajax/repo/edit.php index 99d3d4ec9..f7f6cdfbf 100644 --- a/www/controllers/ajax/repo/edit.php +++ b/www/controllers/ajax/repo/edit.php @@ -1,8 +1,62 @@ updateDescription($_POST['repoId'], $_POST['description']); + } catch (Exception $e) { + response(HTTP_BAD_REQUEST, $e->getMessage()); + } + + response(HTTP_OK, 'Description has been saved'); +} + +/** + * Edit repository tags + */ +if ($_POST['action'] == 'tags' and !empty($_POST['repoId']) and isset($_POST['tags'])) { + $repoController = new Repo(); + + // Tags are sent as a comma-separated string + $tags = array_filter(array_map('trim', explode(',', $_POST['tags'])), function ($tag) { + return $tag !== ''; + }); + + try { + if (!RepoPermission::allowedAction('edit')) { + throw new Exception('You do not have permission to edit repositories'); + } + + if (!RepoPermission::allowedToView($_POST['repoId'])) { + throw new Exception('You do not have permission to edit this repository'); + } + + $repoController->updateTags($_POST['repoId'], $tags); + } catch (Exception $e) { + response(HTTP_BAD_REQUEST, $e->getMessage()); + } + + response(HTTP_OK, 'Tags have been saved'); +} + /** * Validate form and edit repositories */ -if ($_POST['action'] == 'validateForm' and !empty($_POST['params'])) { +if ($_POST['action'] == 'validate-execute' and !empty($_POST['params'])) { $repoEditForm = new \Controllers\Repo\Edit\Form(); try { diff --git a/www/controllers/ajax/repo/environment.php b/www/controllers/ajax/repo/environment.php deleted file mode 100644 index adf4405f4..000000000 --- a/www/controllers/ajax/repo/environment.php +++ /dev/null @@ -1,18 +0,0 @@ -updateDescription($_POST['envId'], $_POST['description']); - } catch (Exception $e) { - response(HTTP_BAD_REQUEST, $e->getMessage()); - } - - response(HTTP_OK, "Description has been saved"); -} - -response(HTTP_BAD_REQUEST, 'Invalid action'); diff --git a/www/controllers/ajax/repo/snapshot/browse.php b/www/controllers/ajax/repo/snapshot/browse.php index 293e81743..ef5e18b6d 100644 --- a/www/controllers/ajax/repo/snapshot/browse.php +++ b/www/controllers/ajax/repo/snapshot/browse.php @@ -84,7 +84,7 @@ } /** - * Delete packages from repo + * Delete packages from a snapshot */ if ($action == 'delete-package' and !empty($_POST['snapId']) and !empty($_POST['packages'])) { $repoSnapshotPackageController = new \Controllers\Repo\Snapshot\Package($_POST['snapId']); @@ -98,6 +98,28 @@ response(HTTP_OK, $deleted); } +/** + * Upload packages to a snapshot + */ +if ($action == 'upload-package' and !empty($_POST['snapId']) and is_numeric($_POST['snapId']) and !empty($_FILES['packages'])) { + $repoSnapshotPackageController = new \Controllers\Repo\Snapshot\Package($_POST['snapId']); + + try { + // Validate the overwrite value if it has been provided + if (isset($_POST['overwrite']) and is_null($overwrite = \Controllers\Utils\Convert::toBool($_POST['overwrite']))) { + throw new Exception('Invalid overwrite value'); + } + + $uploaded = $repoSnapshotPackageController->upload(\Controllers\Utils\Array\Sort::byPostFiles($_FILES['packages']), $overwrite ?? false); + } catch (\Controllers\Exception\AppException $e) { + response(HTTP_BAD_REQUEST, $e->getDetails()); + } catch (Exception $e) { + response(HTTP_BAD_REQUEST, $e->getMessage()); + } + + response(HTTP_OK, $uploaded); +} + /** * Rebuild repo metadata files */ diff --git a/www/controllers/ajax/settings/user.php b/www/controllers/ajax/settings/user.php index a158a9886..f4845d5c8 100644 --- a/www/controllers/ajax/settings/user.php +++ b/www/controllers/ajax/settings/user.php @@ -46,7 +46,7 @@ */ if ($action == 'edit-permissions' and !empty($_POST['id']) and isset($_POST['reposView']) and isset($_POST['reposActions']) and isset($_POST['tasksActions']) and isset($_POST['hostsActions'])) { try { - $userPermissionController = new \Controllers\User\Permission(); + $userPermissionController = new \Controllers\User\Permission\Permission(); $userPermissionController->set($_POST['id'], $_POST['reposView'], $_POST['reposActions'], $_POST['tasksActions'], $_POST['hostsActions']); } catch (Exception $e) { response(HTTP_BAD_REQUEST, $e->getMessage()); diff --git a/www/controllers/ajax/task.php b/www/controllers/ajax/task.php index 7a1347773..9901babbb 100644 --- a/www/controllers/ajax/task.php +++ b/www/controllers/ajax/task.php @@ -1,11 +1,11 @@ getMessage()); } - $myTaskForm->validate($taskRawParams); - $myTask->execute($taskRawParams); + $taskFormController->validate($taskRawParams); + $task = $taskController->execute($taskRawParams); + + // If the task is an array, it means that multiple tasks have been executed, redirect to the tasks page + if (is_array($task)) { + $count = count($task); + $link = '/tasks'; + } + + // If there is only one task, redirect to the task log page + if (is_int($task)) { + $count = 1; + $link = '/task/' . $task; + } + + if (isset($taskRawParams[0]['schedule']['scheduled']) and $taskRawParams[0]['schedule']['scheduled'] == 'true') { + response(HTTP_OK, 'Task' . ($count > 1 ? 's are scheduled' : ' is scheduled') . ': view'); + } + + response(HTTP_OK, 'Task' . ($count > 1 ? 's are running' : ' is running') . ': view'); } catch (Exception $e) { response(HTTP_BAD_REQUEST, $e->getMessage()); } +} + +/** + * Validate and schedule a task targeting a dynamic set of repositories (all latest snapshots matching filters) + */ +if ($_POST['action'] == 'validate-execute-target' and !empty($_POST['taskParams'])) { + $taskFormController = new \Controllers\Task\Form\Form(); + + try { + try { + $taskRawParams = json_decode($_POST['taskParams'], true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw new Exception('Could not decode task parameters: ' . $e->getMessage()); + } - if (isset($taskRawParams[0]['schedule']['scheduled']) and $taskRawParams[0]['schedule']['scheduled'] == 'true') { - response(HTTP_OK, 'Task is scheduled: visualize'); + // Validate the task and retrieve the sanitized parameters + $taskRawParams = $taskFormController->validateTarget($taskRawParams); + + // Create the scheduled task, it will not be executed now + $taskController->execute([$taskRawParams]); + } catch (Exception $e) { + response(HTTP_BAD_REQUEST, $e->getMessage()); + } + + response(HTTP_OK, 'Task is scheduled: view'); +} + +/** + * Return a description of the repositories currently matching a target definition + */ +if ($_POST['action'] == 'count-target-repos' and isset($_POST['target'])) { + $targetController = new \Controllers\Task\Target(); + + try { + try { + $target = json_decode($_POST['target'], true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw new Exception('Could not decode target: ' . $e->getMessage()); + } + + $repos = $targetController->resolve($target); + $count = count($repos); + } catch (Exception $e) { + response(HTTP_BAD_REQUEST, $e->getMessage()); } - response(HTTP_OK, 'Task is running: visualize'); + response(HTTP_OK, 'Target: ' . \Controllers\Task\Target::describe($target) . '. ' . $count . ' repositor' . ($count == 1 ? 'y' : 'ies') . ' currently matching.'); } /** @@ -53,7 +112,7 @@ */ if ($_POST['action'] == 'disable' and !empty($_POST['id'])) { try { - $myTask->disable($_POST['id']); + $taskController->disable($_POST['id']); } catch (Exception $e) { response(HTTP_BAD_REQUEST, $e->getMessage()); } @@ -66,7 +125,7 @@ */ if ($_POST['action'] == 'enable' and !empty($_POST['id'])) { try { - $myTask->enable($_POST['id']); + $taskController->enable($_POST['id']); } catch (Exception $e) { response(HTTP_BAD_REQUEST, $e->getMessage()); } @@ -79,12 +138,12 @@ */ if ($_POST['action'] == 'delete' and !empty($_POST['id'])) { try { - $myTask->delete($_POST['id']); + $taskController->delete($_POST['id']); } catch (Exception $e) { response(HTTP_BAD_REQUEST, $e->getMessage()); } - response(HTTP_OK, 'Task has been deleted'); + response(HTTP_OK, 'Task' . (count($_POST['id']) > 1 ? 's have' : ' has') . ' been deleted'); } /** @@ -92,12 +151,12 @@ */ if ($_POST['action'] == 'relaunch' and !empty($_POST['id'])) { try { - $myTask->relaunch($_POST['id']); + $taskController->relaunch($_POST['id']); } catch (Exception $e) { response(HTTP_BAD_REQUEST, $e->getMessage()); } - response(HTTP_OK, 'Task has been relaunched using the same parameters'); + response(HTTP_OK, 'Task' . (count($_POST['id']) > 1 ? 's have' : ' has') . ' been relaunched using the same parameters'); } /** @@ -105,12 +164,12 @@ */ if ($_POST['action'] == 'stop' and !empty($_POST['id'])) { try { - $myTask->stop($_POST['id']); + $taskController->stop($_POST['id']); } catch (Exception $e) { response(HTTP_BAD_REQUEST, $e->getMessage()); } - response(HTTP_OK, 'Task stopped'); + response(HTTP_OK, 'Task' . (count($_POST['id']) > 1 ? 's have' : ' has') . ' been stopped'); } /** @@ -157,7 +216,7 @@ if ($_POST['action'] == 'get-task-status' and !empty($_POST['taskId'])) { try { - $task = $myTask->getById($_POST['taskId']); + $task = $taskController->getById($_POST['taskId']); $status = $task['Status']; } catch (Exception $e) { response(HTTP_BAD_REQUEST, $e->getMessage()); diff --git a/www/controllers/ajax/user/preferences.php b/www/controllers/ajax/user/preferences.php new file mode 100644 index 000000000..b05ef1967 --- /dev/null +++ b/www/controllers/ajax/user/preferences.php @@ -0,0 +1,17 @@ +set($_SESSION['id'], $_POST['preferences']); + } catch (Exception $e) { + response(HTTP_BAD_REQUEST, $e->getMessage()); + } + + response(HTTP_OK, 'Preferences have been saved'); +} + +response(HTTP_BAD_REQUEST, 'Invalid action'); diff --git a/www/models/Connection.php b/www/models/Connection.php index d2c513b40..871d5ae78 100644 --- a/www/models/Connection.php +++ b/www/models/Connection.php @@ -45,9 +45,7 @@ public function __construct(string $database, int|null $databaseId = null, bool $this->generateMainTables(); } - /** - * Case where database is 'stats', it is the stats database 'repomanager-stats.db' - */ + // Case where database is 'stats', it is the stats database 'repomanager-stats.db' } elseif ($database == 'stats') { $this->open(STATS_DB); $this->busyTimeout(30000); @@ -64,9 +62,7 @@ public function __construct(string $database, int|null $databaseId = null, bool $this->generateStatsTables(); } - /** - * Case where database is 'hosts', it is the hosts database 'repomanager-hosts.db' - */ + // Case where database is 'hosts', it is the hosts database 'repomanager-hosts.db' } elseif ($database == 'hosts') { $this->open(HOSTS_DB); $this->busyTimeout(30000); @@ -83,9 +79,7 @@ public function __construct(string $database, int|null $databaseId = null, bool $this->generateHostsTables(); } - /** - * Case where database is 'host', it is a host database 'properties.db', databaseId must be set - */ + // Case where database is 'host', it is a host database 'properties.db', databaseId must be set } elseif ($database == 'host' and !empty($databaseId)) { $this->open(HOSTS_DIR . '/' . $databaseId . '/properties.db'); $this->busyTimeout(30000); @@ -93,9 +87,7 @@ public function __construct(string $database, int|null $databaseId = null, bool $this->enableWAL(); $this->generateHostTables(); - /** - * Case where database is 'ws', it is the websockets database 'repomanager-ws.db' - */ + // Case where database is 'ws', it is the websockets database 'repomanager-ws.db' } elseif ($database == 'ws') { $this->open(WS_DB); $this->busyTimeout(30000); @@ -103,9 +95,7 @@ public function __construct(string $database, int|null $databaseId = null, bool $this->enableWAL(); $this->generateWsTables(); - /** - * Case where database is 'task-log', it is a task log database 'task--log.db' - */ + // Case where database is 'task-log', it is a task log database 'task--log.db' } elseif ($database == 'task-log' and !empty($databaseId)) { $this->open(MAIN_LOGS_DIR . '/repomanager-task-' . $databaseId . '-log.db'); $this->busyTimeout(30000); @@ -113,9 +103,7 @@ public function __construct(string $database, int|null $databaseId = null, bool $this->enableWAL(); $this->generateTaskLogTables(); - /** - * Case where database is not 'main', 'stats', 'hosts' or 'host' - */ + // Case where database is not 'main', 'stats', 'hosts' or 'host' } else { throw new Exception("unknown database: $database"); } @@ -132,12 +120,6 @@ private function enableWAL(): void $this->exec('pragma journal_mode = WAL; pragma synchronous = normal; pragma temp_store = memory; pragma mmap_size = 30000000000;'); } - /** - * - * Functions to check if all tables are present - * - */ - /** * Count the number of tables in the database */ @@ -153,7 +135,7 @@ public function countTables(): int */ public function checkMainTables(): bool { - $this->required = 28; + $this->required = 29; $this->count = $this->countTables(); // If the number of required tables != $required then try to regenerate the tables @@ -240,20 +222,12 @@ public function checkWsTables(): bool return true; } - /** - * - * Functions to generate tables if not exists - * - */ - /** * Generate tables in the main database */ private function generateMainTables(): void { - /** - * repos table - */ + // repos table $this->exec("CREATE TABLE IF NOT EXISTS repos ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Name VARCHAR(255) NOT NULL, @@ -261,16 +235,14 @@ private function generateMainTables(): void Dist VARCHAR(255), Section VARCHAR(255), Source VARCHAR(255) NOT NULL, - Package_type VARCHAR(10) NOT NULL)"); + Package_type VARCHAR(10) NOT NULL, + Description VARCHAR(255), + Tags VARCHAR(255))"); - /** - * Create indexes - */ + // Create indexes $this->exec("CREATE INDEX IF NOT EXISTS repos_ALL_index ON repos (Name, Releasever, Dist, Section, Source, Package_type)"); - /** - * repos_snap table - */ + // repos_snap table $this->exec("CREATE TABLE IF NOT EXISTS repos_snap ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Date DATE NOT NULL, @@ -285,61 +257,45 @@ private function generateMainTables(): void Status CHAR(8) NOT NULL, Id_repo INTEGER NOT NULL)"); - /** - * Create indexes - */ + // Create indexes // TODO: uncomment in release > 5.14.0 // $this->exec("CREATE INDEX IF NOT EXISTS idx_repos_snap ON repos_snap (Date, Time, Signed, Arch, Type, Reconstruct, Size, Size_human, Status, Id_repo)"); $this->exec("CREATE INDEX IF NOT EXISTS idx_repos_snap_status_id_repo ON repos_snap (Status, Id_repo)"); $this->exec("CREATE INDEX IF NOT EXISTS idx_repos_snap_id_repo ON repos_snap (Id_repo)"); - /** - * repos_env table - */ + // repos_env table $this->exec("CREATE TABLE IF NOT EXISTS repos_env ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Env VARCHAR(255), - Description VARCHAR(255), Id_snap INTEGER NOT NULL)"); - /** - * Create indexes - */ - $this->exec("CREATE INDEX IF NOT EXISTS repos_env_index ON repos_env (Env, Description, Id_snap)"); + // Create indexes + $this->exec("CREATE INDEX IF NOT EXISTS idx_repos_env ON repos_env (Env, Id_snap)"); - /** - * Create indexes - */ - $this->exec("CREATE INDEX IF NOT EXISTS repos_env_id_snap_index ON repos_env (Id_snap)"); + // Create indexes + $this->exec("CREATE INDEX IF NOT EXISTS idx_repos_env_id_snap ON repos_env (Id_snap)"); - /** - * env table - */ + // env table $this->exec("CREATE TABLE IF NOT EXISTS env ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Name VARCHAR(255) NOT NULL, - Color VARCHAR(255))"); + Color VARCHAR(255), + Protected CHAR(5))"); /* true, false */ - /** - * Insert default env if table is empty - */ + // Insert default env if table is empty $result = $this->query("SELECT Id FROM env"); if ($this->isempty($result) === true) { $this->exec("INSERT INTO env ('Name', 'Color') VALUES ('preprod', '#ffffff')"); $this->exec("INSERT INTO env ('Name', 'Color') VALUES ('prod', '#F32F63')"); } - /** - * sources table - */ + // sources table $this->exec("CREATE TABLE IF NOT EXISTS sources ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Definition TEXT, Method VARCHAR(255))"); - /** - * users table - */ + // users table $this->exec("CREATE TABLE IF NOT EXISTS users ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Username VARCHAR(255) NOT NULL, @@ -352,44 +308,35 @@ private function generateMainTables(): void Type CHAR(5) NOT NULL, State CHAR(7) NOT NULL)"); /* active / deleted */ - /** - * Create user_permissions table - */ + // Create user_permissions table $this->exec("CREATE TABLE IF NOT EXISTS user_permissions ( Permissions VARCHAR(255), User_id INTEGER NOT NULL)"); - /** - * user_role table - */ + // Create user_preferences table + $this->exec("CREATE TABLE IF NOT EXISTS user_preferences ( + Preferences TEXT, + User_id INTEGER NOT NULL UNIQUE)"); + + // user_role table $this->exec("CREATE TABLE IF NOT EXISTS user_role ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Name CHAR(15) NOT NULL UNIQUE)"); - /** - * If user_role table is empty (just created) then we create default roles - */ + // If user_role table is empty (just created) then we create default roles $result = $this->query("SELECT Id FROM user_role"); if ($this->isempty($result) === true) { - /** - * super-administrator role: all permissions - */ + // super-administrator role: all permissions $this->exec("INSERT INTO user_role ('Name') VALUES ('super-administrator')"); - /** - * administrator role: all permissions except user management (only super-administrator can manage users) - */ + // administrator role: all permissions except user management (only super-administrator can manage users) $this->exec("INSERT INTO user_role ('Name') VALUES ('administrator')"); - /** - * usage role: read-only permissions - */ + // usage role: read-only permissions $this->exec("INSERT INTO user_role ('Name') VALUES ('usage')"); } - /** - * If users table is empty (just created) then we create admin user (default password 'repomanager' and role n°1 (super-administrator)) - */ + // If users table is empty (just created) then we create admin user (default password 'repomanager' and role n°1 (super-administrator)) $result = $this->query("SELECT Id FROM users"); if ($this->isempty($result) === true) { $password_hashed = '$2y$10$FD6/70o2nXPf76SAPYIGSutauQ96LqKie5PLanoYBNbCWen492cX6'; @@ -403,9 +350,7 @@ private function generateMainTables(): void } } - /** - * history table - */ + // history table $this->exec("CREATE TABLE IF NOT EXISTS history ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Date DATE NOT NULL, @@ -418,24 +363,18 @@ private function generateMainTables(): void Action VARCHAR(255) NOT NULL, State CHAR(7))"); /* success or error */ - /** - * groups table - */ + // groups table $this->exec("CREATE TABLE IF NOT EXISTS groups ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Name VARCHAR(255) UNIQUE NOT NULL)"); - /** - * group_members table - */ + // group_members table $this->exec("CREATE TABLE IF NOT EXISTS group_members ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Id_repo INTEGER NOT NULL, Id_group INTEGER NOT NULL);"); - /** - * Create indexes - */ + // Create indexes $this->exec("CREATE INDEX IF NOT EXISTS group_members_id_repo_index ON group_members (Id_repo)"); $this->exec("CREATE INDEX IF NOT EXISTS group_members_id_group_index ON group_members (Id_group)"); @@ -448,31 +387,26 @@ private function generateMainTables(): void Pid INTEGER, Logfile VARCHAR(255), Duration INTEGER, - Status CHAR(9))"); /* new, scheduled, running, done, stopped */ + Status CHAR(9), /* new, scheduled, running, done, stopped */ + Parent_task_id INTEGER)"); - /** - * Create indexes - */ + // Create indexes $this->exec("CREATE INDEX IF NOT EXISTS tasks_rawparams_status ON tasks (Raw_params, Status)"); $this->exec("CREATE INDEX IF NOT EXISTS tasks_status ON tasks (Status)"); + // TODO: uncomment this after 6.0.0 + // $this->exec("CREATE INDEX IF NOT EXISTS tasks_parent_task_id ON tasks (Parent_task_id)"); - /** - * profile_settings table - */ + // profile_settings table $this->exec("CREATE TABLE IF NOT EXISTS profile_settings ( Package_type VARCHAR(255))"); - /** - * If profile_settings table is empty (just created) then we populate it - */ + // If profile_settings table is empty (just created) then we populate it $result = $this->query("SELECT * FROM profile_settings"); if ($this->isempty($result) === true) { $this->exec("INSERT INTO profile_settings (Package_type) VALUES ('deb,rpm')"); } - /** - * profile table - */ + // profile table $this->exec("CREATE TABLE IF NOT EXISTS profile ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Name VARCHAR(255) NOT NULL, @@ -482,46 +416,34 @@ private function generateMainTables(): void Service_restart VARCHAR(255), Notes VARCHAR(255))"); - /** - * profile_repo_members table - */ + // profile_repo_members table $this->exec("CREATE TABLE IF NOT EXISTS profile_repo_members ( Id_profile INTEGER NOT NULL, Id_repo INTEGER NOT NULL)"); - /** - * profile_package table - */ + // profile_package table $this->exec("CREATE TABLE IF NOT EXISTS profile_package ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Name VARCHAR(255) UNIQUE NOT NULL)"); - /** - * If profile_package table is empty (just created) then we populate it - */ + // If profile_package table is empty (just created) then we populate it $result = $this->query("SELECT Id FROM profile_package"); if ($this->isempty($result) === true) { $this->exec("INSERT INTO profile_package (Name) VALUES ('apache'), ('httpd'), ('php'), ('php-fpm'), ('mysql'), ('fail2ban'), ('nrpe'), ('munin-node'), ('node'), ('newrelic'), ('nginx'), ('haproxy'), ('netdata'), ('nfs'), ('rsnapshot'), ('kernel'), ('java'), ('redis'), ('varnish'), ('mongo'), ('rabbit'), ('clamav'), ('clam'), ('gpg'), ('gnupg')"); } - /** - * profile_service table - */ + // profile_service table $this->exec("CREATE TABLE IF NOT EXISTS profile_service ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Name VARCHAR(255) UNIQUE NOT NULL)"); - /** - * If profile_service table is empty (just created) then we populate it - */ + // If profile_service table is empty (just created) then we populate it $result = $this->query("SELECT Id FROM profile_service"); if ($this->isempty($result) === true) { $this->exec("INSERT INTO profile_service (Name) VALUES ('apache'), ('httpd'), ('php-fpm'), ('mysqld'), ('fail2ban'), ('nrpe'), ('munin-node'), ('nginx'), ('haproxy'), ('netdata'), ('nfsd'), ('redis'), ('varnish'), ('mongod'), ('clamd')"); } - /** - * Create system_monitoring table - */ + // Create system_monitoring table $this->exec("CREATE TABLE IF NOT EXISTS system_monitoring ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Timestamp VARCHAR(255) NOT NULL, @@ -529,14 +451,10 @@ private function generateMainTables(): void Memory_usage REAL, Disk_usage REAL)"); - /** - * Create indexes on system_monitoring table - */ + // Create indexes on system_monitoring table $this->exec("CREATE INDEX IF NOT EXISTS system_monitoring_index ON system_monitoring (Timestamp, Cpu_usage, Memory_usage, Disk_usage)"); - /** - * notifications table - */ + // notifications table $this->exec("CREATE TABLE IF NOT EXISTS notifications ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Id_notification CHAR(5) NOT NULL, @@ -544,9 +462,7 @@ private function generateMainTables(): void Message VARCHAR(255) NOT NULL, Status CHAR(9) NOT NULL)"); /* new, acquitted */ - /** - * settings table - */ + // settings table $this->exec("CREATE TABLE IF NOT EXISTS settings ( /* General settings */ DEBUG_MODE CHAR(5), @@ -614,26 +530,18 @@ private function generateMainTables(): void OIDC_HTTP_PROXY VARCHAR(255), OIDC_CERT_PATH VARCHAR(255))"); - /** - * If settings table is empty then populate it - */ + // If settings table is empty then populate it $result = $this->query("SELECT * FROM settings"); if ($this->isempty($result) === true) { - /** - * Set default values - */ + // Set default values $fqdn = 'localhost'; - /** - * FQDN file is created on container startup (entrypoint) - */ + // FQDN file is created on container startup (entrypoint) if (file_exists(ROOT . '/.fqdn')) { $fqdn = trim(file_get_contents(ROOT . '/.fqdn')); } - /** - * GPG key Id - */ + // GPG key Id $gpgKeyId = 'repomanager@' . $fqdn; /** @@ -767,10 +675,7 @@ private function generateMainTables(): void )"); } - /** - * Generate cve table if not exists - * CVEs table - */ + // Generate cve table if not exists $this->exec("CREATE TABLE IF NOT EXISTS cve ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Name VARCHAR(255) NOT NULL, @@ -822,16 +727,12 @@ private function generateMainTables(): void Duration INTEGER, Status CHAR(7) NOT NULL)"); /* running, error, done */ - /** - * Create indexes - */ + // Create indexes $this->exec("CREATE INDEX IF NOT EXISTS cve_index ON cve (Name, Updated_date, Updated_time)"); $this->exec("CREATE INDEX IF NOT EXISTS cve_cpe_index ON cve_cpe (Part, Vendor, Product, Version, Id_cve)"); $this->exec("CREATE INDEX IF NOT EXISTS cve_affected_hosts_index ON cve_affected_hosts (Status, Id_cve)"); - /** - * Generate logs table if not exists - */ + // Generate logs table if not exists $this->exec("CREATE TABLE IF NOT EXISTS logs ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Date DATE NOT NULL, @@ -842,9 +743,7 @@ private function generateMainTables(): void Details TEXT, Status CHAR(9) NOT NULL)"); /* new, acquitted */ - /** - * Generate layout_container_state table if not exists - */ + // Generate layout_container_state table if not exists $this->exec("CREATE TABLE IF NOT EXISTS layout_container_state ( Container VARCHAR(255) NOT NULL)"); } @@ -854,9 +753,7 @@ private function generateMainTables(): void */ private function generateStatsTables(): void { - /** - * stats table - */ + // stats table $this->exec("CREATE TABLE IF NOT EXISTS repo_stats ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Timestamp INTEGER NOT NULL, @@ -865,9 +762,7 @@ private function generateStatsTables(): void Snapshot_packages_count INTEGER NOT NULL, Id_repo INTEGER NOT NULL)"); - /** - * access_deb table - */ + // access_deb table $this->exec("CREATE TABLE IF NOT EXISTS access_deb ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Timestamp INTEGER NOT NULL, @@ -880,9 +775,7 @@ private function generateStatsTables(): void Request VARCHAR(255) NOT NULL, Request_result VARCHAR(8) NOT NULL)"); - /** - * access_rpm table - */ + // access_rpm table $this->exec("CREATE TABLE IF NOT EXISTS access_rpm ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Timestamp INTEGER NOT NULL, @@ -894,16 +787,12 @@ private function generateStatsTables(): void Request VARCHAR(255) NOT NULL, Request_result VARCHAR(8) NOT NULL)"); - /** - * access_queue table - */ + // access_queue table $this->exec("CREATE TABLE IF NOT EXISTS access_queue ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Request VARCHAR(255) NOT NULL)"); - /** - * Create indexes - */ + // Create indexes $this->exec("CREATE INDEX IF NOT EXISTS idx_access_deb ON access_deb (Name, Dist, Section, Env, Source, IP, Request, Request_result, Timestamp)"); $this->exec("CREATE INDEX IF NOT EXISTS idx_access_rpm ON access_rpm (Name, Releasever, Env, Source, IP, Request, Request_result, Timestamp)"); $this->exec("CREATE INDEX IF NOT EXISTS idx_repo_stats ON repo_stats (Snapshot_date, Snapshot_size, Snapshot_packages_count, Id_repo, Timestamp)"); @@ -922,9 +811,7 @@ private function generateStatsTables(): void */ private function generateHostsTables(): void { - /** - * requests table - */ + // requests table $this->exec("CREATE TABLE IF NOT EXISTS requests ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Date DATE NOT NULL, @@ -938,9 +825,7 @@ private function generateHostsTables(): void Next_retry VARCHAR(255), Id_host INTEGER NOT NULL)"); - /** - * hosts table - */ + // hosts table $this->exec("CREATE TABLE IF NOT EXISTS hosts ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Ip VARCHAR(15) NOT NULL, @@ -965,27 +850,22 @@ private function generateHostsTables(): void Uptime VARCHAR(255), Agent_version VARCHAR(255))"); /* active / disabled / deleted */ - /** - * groups table - */ + // groups table $this->exec("CREATE TABLE IF NOT EXISTS groups ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Name VARCHAR(255) UNIQUE NOT NULL)"); - /** - * group_members table - */ + // group_members table $this->exec("CREATE TABLE IF NOT EXISTS group_members ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Id_host INTEGER NOT NULL, Id_group INTEGER NOT NULL)"); - /** - * settings table - */ + // settings table $this->exec("CREATE TABLE IF NOT EXISTS settings ( compliance_threshold_count INTEGER NOT NULL DEFAULT 1, compliance_threshold_days INTEGER NOT NULL DEFAULT 30, + compliance_security_update INTEGER NOT NULL DEFAULT 1, compliance_reboot_required INTEGER NOT NULL DEFAULT 1)"); /* 1 = true, 0 = false */ // Insert default values in settings table, if empty @@ -993,9 +873,7 @@ private function generateHostsTables(): void $this->query("INSERT INTO settings (compliance_threshold_count, compliance_threshold_days, compliance_reboot_required) VALUES (1, 30, 1)"); } - /** - * Create indexes - */ + // Create indexes // hosts table indexes: $this->exec("CREATE INDEX IF NOT EXISTS idx_hosts ON hosts (Ip, Hostname, Os, Os_version, Os_family, Kernel, Arch, Type, Profile, Env, AuthId, Token, Online_status, Online_status_date, Online_status_time, Reboot_required, Agent_version)"); $this->exec("CREATE INDEX IF NOT EXISTS idx_hosts_authid ON hosts (AuthId)"); @@ -1026,46 +904,38 @@ private function generateHostsTables(): void */ public function generateHostTables(): void { - /** - * packages table - * Inventory of all packages installed on the host - */ + // packages table, inventory of all packages installed on the host $this->exec("CREATE TABLE IF NOT EXISTS packages ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Name VARCHAR(255), Version VARCHAR(255), State VARCHAR(255), Type VARCHAR(255), + Security CHAR(5), /* true, false */ Date DATE NOT NULL, Time TIME NOT NULL, Id_event INTEGER)"); - /** - * history table - * History of all packages installed on the host (installed, updated, removed) - */ + // history table, history of all packages installed on the host (installed, updated, removed) $this->exec("CREATE TABLE IF NOT EXISTS packages_history ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Name VARCHAR(255), Version VARCHAR(255), State VARCHAR(255), Type VARCHAR(255), + Security CHAR(5), /* true, false */ Date DATE NOT NULL, Time TIME NOT NULL, Id_event INTEGER)"); - /** - * packages_available table - * Available packages for update - */ + // packages_available table, available packages for update $this->exec("CREATE TABLE IF NOT EXISTS packages_available ( Name VARCHAR(255), Version VARCHAR(255), + Security CHAR(5), /* true, false */ Repository VARCHAR(255))"); - /** - * events table - */ + // events table $this->exec("CREATE TABLE IF NOT EXISTS events ( Id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, Date DATE NOT NULL, @@ -1076,9 +946,7 @@ public function generateHostTables(): void Command VARCHAR(255), Status VARCHAR(7))"); /* error / warning / unknown / done */ - /** - * Create indexes - */ + // Create indexes // packages table indexes: $this->exec("CREATE INDEX IF NOT EXISTS host_packages_available_name_version ON packages_available (Name, Version);"); $this->exec("CREATE INDEX IF NOT EXISTS host_packages_name_version ON packages (Name, Version);"); @@ -1097,9 +965,7 @@ public function generateHostTables(): void */ private function generateWsTables(): void { - /** - * ws_connections table - */ + // ws_connections table $this->exec("CREATE TABLE IF NOT EXISTS ws_connections ( Connection_id INTEGER, Type VARCHAR(255), @@ -1154,16 +1020,12 @@ public function isempty($result): bool { $count = 0; - /** - * Count the number of rows returned by the query - */ + // Count the number of rows returned by the query while ($row = $result->fetchArray(SQLITE3_ASSOC)) { $count++; } - /** - * If count == 0 then result is empty - */ + // If count == 0 then result is empty if ($count == 0) { return true; } diff --git a/www/models/Environment.php b/www/models/Environment.php index 05080ac41..85d717ce9 100644 --- a/www/models/Environment.php +++ b/www/models/Environment.php @@ -34,12 +34,13 @@ public function getAllByName(): array /** * Add new environment in database */ - public function add(string $name, string $color) : void + public function add(string $name, string $color, string $protected) : void { try { - $stmt = $this->db->prepare("INSERT INTO env (Name, Color) VALUES (:name, :color)"); + $stmt = $this->db->prepare("INSERT INTO env (Name, Color, Protected) VALUES (:name, :color, :protected)"); $stmt->bindValue(':name', $name); $stmt->bindValue(':color', $color); + $stmt->bindValue(':protected', $protected); $stmt->execute(); } catch (Exception $e) { DbLog::error($e); @@ -71,23 +72,39 @@ public function deleteAll() /** * List all environments */ - public function listAll() + public function listAll(): array { - $datas = []; + $data = []; $result = $this->db->query("SELECT * FROM env"); while ($row = $result->fetchArray(SQLITE3_ASSOC)) { - $datas[] = $row; + $data[] = $row; } - return $datas; + return $data; + } + + /** + * Return the list of protected environments + */ + public function getProtected(): array + { + $data = []; + + $result = $this->db->query("SELECT Name FROM env WHERE Protected = 'true'"); + + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $data[] = $row['Name']; + } + + return $data; } /** * List default environment */ - public function default() + public function default(): string { $default = ''; diff --git a/www/models/Group/Group.php b/www/models/Group/Group.php index 7f8e867f9..299f97d01 100644 --- a/www/models/Group/Group.php +++ b/www/models/Group/Group.php @@ -218,6 +218,28 @@ public function getReposMembers(int $id) return $data; } + /** + * Return the list of group Ids the specified repo is member of + */ + public function getRepoGroupsIds(int $repoId): array + { + $data = []; + + try { + $stmt = $this->db->prepare("SELECT Id_group FROM group_members WHERE Id_repo = :repoId"); + $stmt->bindValue(':repoId', $repoId); + $result = $stmt->execute(); + } catch (Exception $e) { + DbLog::error($e); + } + + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $data[] = $row['Id_group']; + } + + return $data; + } + /** * Return the list of repos not in any group */ diff --git a/www/models/Host/Host.php b/www/models/Host/Host.php index c4f79fc18..2a22db6ee 100644 --- a/www/models/Host/Host.php +++ b/www/models/Host/Host.php @@ -123,12 +123,13 @@ public function getSettings(): array /** * Edit the display settings on the hosts page */ - public function setSettings(int $complianceThresholdCount, int $complianceThresholdDays, int $complianceRebootRequired): void + public function setSettings(int $complianceThresholdCount, int $complianceThresholdDays, int $complianceSecurityUpdate, int $complianceRebootRequired): void { try { - $stmt = $this->db->prepare("UPDATE settings SET compliance_threshold_count = :compliance_threshold_count, compliance_threshold_days = :compliance_threshold_days, compliance_reboot_required = :compliance_reboot_required"); + $stmt = $this->db->prepare("UPDATE settings SET compliance_threshold_count = :compliance_threshold_count, compliance_threshold_days = :compliance_threshold_days, compliance_security_update = :compliance_security_update, compliance_reboot_required = :compliance_reboot_required"); $stmt->bindValue(':compliance_threshold_count', $complianceThresholdCount, SQLITE3_INTEGER); $stmt->bindValue(':compliance_threshold_days', $complianceThresholdDays, SQLITE3_INTEGER); + $stmt->bindValue(':compliance_security_update', $complianceSecurityUpdate, SQLITE3_INTEGER); $stmt->bindValue(':compliance_reboot_required', $complianceRebootRequired, SQLITE3_INTEGER); $stmt->execute(); } catch (Exception $e) { diff --git a/www/models/Host/Package/Package.php b/www/models/Host/Package/Package.php index 384660c27..5bf38a587 100644 --- a/www/models/Host/Package/Package.php +++ b/www/models/Host/Package/Package.php @@ -138,13 +138,35 @@ public function getPackageState(string $packageName, string|null $packageVersion /** * Return the list of inventoried packages in database + * Supports optional pagination and search filter on package name */ - public function getInventory() : array + public function getInventory(string $search, bool $withOffset = false, int $offset = 0): array { $datas = []; try { - $result = $this->db->query("SELECT * FROM packages ORDER BY Name ASC"); + $query = "SELECT * FROM packages"; + + if (!empty($search)) { + $query .= " WHERE Name LIKE :search OR Version LIKE :search"; + } + + $query .= " ORDER BY Name ASC"; + + if ($withOffset === true) { + $query .= " LIMIT 10 OFFSET :offset"; + } + + $stmt = $this->db->prepare($query); + + if (!empty($search)) { + $stmt->bindValue(':search', '%' . $search . '%', SQLITE3_TEXT); + } + if ($withOffset === true) { + $stmt->bindValue(':offset', $offset, SQLITE3_INTEGER); + } + + $result = $stmt->execute(); } catch (Exception $e) { DbLog::error($e); } @@ -156,6 +178,38 @@ public function getInventory() : array return $datas; } + /** + * Return the total count of inventoried packages, optionally filtered by a search on package name/version + */ + public function countInventory(string $search): int + { + $count = 0; + + try { + $query = "SELECT COUNT(*) AS total FROM packages"; + + if (!empty($search)) { + $query .= " WHERE Name LIKE :search OR Version LIKE :search"; + } + + $stmt = $this->db->prepare($query); + + if (!empty($search)) { + $stmt->bindValue(':search', '%' . $search . '%', SQLITE3_TEXT); + } + + $result = $stmt->execute(); + } catch (Exception $e) { + DbLog::error($e); + } + + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $count = (int) $row['total']; + } + + return $count; + } + /** * Return the list of installed packages in database */ @@ -185,7 +239,7 @@ public function getAvailable(bool $withOffset, int $offset) : array $data = []; try { - $query = "SELECT packages_available.Name, packages.Version as Current_version, packages_available.Version, packages_available.Repository FROM packages_available"; + $query = "SELECT packages_available.Name, packages.Version as Current_version, packages_available.Version, packages_available.Security, packages_available.Repository FROM packages_available"; // Join with packages table to also get the current version of the package installed on the host $query .= " LEFT JOIN packages ON packages_available.Name = packages.Name AND (packages.State = 'inventored' or packages.State = 'installed' or packages.State = 'dep-installed' or packages.State = 'upgraded' or packages.State = 'downgraded')"; @@ -403,12 +457,13 @@ public function addPackage(string $name, string $version, string $state, string /** * Add a package in the available packages list */ - public function addPackageAvailable(string $name, string $version, string $repository) : void + public function addPackageAvailable(string $name, string $version, string $security, string $repository) : void { try { - $stmt = $this->db->prepare("INSERT INTO packages_available ('Name', 'Version', 'Repository') VALUES (:name, :version, :repository)"); + $stmt = $this->db->prepare("INSERT INTO packages_available ('Name', 'Version', 'Security', 'Repository') VALUES (:name, :version, :security, :repository)"); $stmt->bindValue(':name', $name); $stmt->bindValue(':version', $version); + $stmt->bindValue(':security', $security); $stmt->bindValue(':repository', $repository); $stmt->execute(); } catch (Exception $e) { @@ -419,12 +474,13 @@ public function addPackageAvailable(string $name, string $version, string $repos /** * Update a package in the available packages list */ - public function updatePackageAvailable(string $name, string $version, string $repository) : void + public function updatePackageAvailable(string $name, string $version, string $security, string $repository) : void { try { - $stmt = $this->db->prepare("UPDATE packages_available SET Version = :version, Repository = :repository WHERE Name = :name"); + $stmt = $this->db->prepare("UPDATE packages_available SET Version = :version, Security = :security, Repository = :repository WHERE Name = :name"); $stmt->bindValue(':name', $name); $stmt->bindValue(':version', $version); + $stmt->bindValue(':security', $security); $stmt->bindValue(':repository', $repository); $stmt->execute(); } catch (Exception $e) { diff --git a/www/models/Layout/ContainerReload.php b/www/models/Layout/ContainerReload.php index c2dbfcb54..a080a93d0 100644 --- a/www/models/Layout/ContainerReload.php +++ b/www/models/Layout/ContainerReload.php @@ -66,6 +66,30 @@ public function exists(string $name) return true; } + /** + * Delete the specified layout container states + */ + public function delete(array $names): void + { + if (empty($names)) { + return; + } + + try { + $placeholders = implode(',', array_fill(0, count($names), '?')); + + $stmt = $this->db->prepare("DELETE FROM layout_container_state WHERE Container IN (" . $placeholders . ")"); + + foreach (array_values($names) as $index => $name) { + $stmt->bindValue($index + 1, $name); + } + + $stmt->execute(); + } catch (Exception $e) { + DbLog::error($e); + } + } + /** * Clean all containers entries */ diff --git a/www/models/Repo/Deb.php b/www/models/Repo/Deb.php index fa0891cf0..64c699dde 100644 --- a/www/models/Repo/Deb.php +++ b/www/models/Repo/Deb.php @@ -36,40 +36,6 @@ public function getIdByNameDistComponent(string $name, string $distribution, str return $id; } - /** - * Return repository environment description - */ - public function getDescriptionByName(string $name, string $dist, string $component, string $env) : string|null - { - $description = null; - - try { - $stmt = $this->db->prepare("SELECT repos_env.Description FROM repos_env - INNER JOIN repos_snap - ON repos_snap.Id = repos_env.Id_snap - INNER JOIN repos - ON repos.Id = repos_snap.Id_repo - WHERE repos.Name = :name - AND repos.Dist = :dist - AND repos.Section = :component - AND repos_env.Env = :env - AND repos_snap.Status = 'active'"); - $stmt->bindValue(':name', $name); - $stmt->bindValue(':dist', $dist); - $stmt->bindValue(':component', $component); - $stmt->bindValue(':env', $env); - $result = $stmt->execute(); - } catch (Exception $e) { - DbLog::error($e); - } - - while ($row = $result->fetchArray(SQLITE3_ASSOC)) { - $description = $row['Description']; - } - - return $description; - } - /** * Return environment Id from repo name */ diff --git a/www/models/Repo/Environment.php b/www/models/Repo/Environment.php index 284d322b4..d8c285ae5 100644 --- a/www/models/Repo/Environment.php +++ b/www/models/Repo/Environment.php @@ -13,18 +13,17 @@ public function __construct() } /** - * Return all environments associated to a repository ID + * Return all environments associated to a snapshot Id */ - public function getByRepoId(int $repoId): array + public function getBySnapId(int $snapId): array { $data = []; try { $stmt = $this->db->prepare("SELECT Env FROM repos_env INNER JOIN repos_snap ON repos_snap.Id = repos_env.Id_snap - INNER JOIN repos ON repos.Id = repos_snap.Id_repo - WHERE repos.Id = :repoId"); - $stmt->bindValue(':repoId', $repoId); + WHERE repos_snap.Id = :snapId"); + $stmt->bindValue(':snapId', $snapId); $result = $stmt->execute(); } catch (Exception $e) { DbLog::error($e); @@ -40,12 +39,11 @@ public function getByRepoId(int $repoId): array /** * Associate a new env to a snapshot */ - public function add(int $snapId, string $env, string $description) : void + public function add(int $snapId, string $env) : void { try { - $stmt = $this->db->prepare("INSERT INTO repos_env ('Env', 'Description', 'Id_snap') VALUES (:env, :description, :snapId)"); + $stmt = $this->db->prepare("INSERT INTO repos_env ('Env', 'Id_snap') VALUES (:env, :snapId)"); $stmt->bindValue(':env', $env); - $stmt->bindValue(':description', $description); $stmt->bindValue(':snapId', $snapId); $stmt->execute(); } catch (Exception $e) { @@ -67,21 +65,6 @@ public function remove(int $id) : void } } - /** - * Update environment description - */ - public function updateDescription(int $id, string $description) : void - { - try { - $stmt = $this->db->prepare("UPDATE repos_env SET Description = :description WHERE Id = :envId"); - $stmt->bindValue(':description', $description); - $stmt->bindValue(':envId', $id); - $stmt->execute(); - } catch (Exception $e) { - DbLog::error($e); - } - } - /** * Return true if the repository environment Id exists */ diff --git a/www/models/Repo/Listing.php b/www/models/Repo/Listing.php index 6fc56f6f6..868addaef 100644 --- a/www/models/Repo/Listing.php +++ b/www/models/Repo/Listing.php @@ -30,6 +30,8 @@ public function list() : array repos.Releasever, repos.Source, repos.Package_type, + repos.Description, + repos.Tags, repos_env.Env, repos_snap.Date, repos_snap.Time, @@ -38,8 +40,7 @@ public function list() : array repos_snap.Type, repos_snap.Reconstruct, repos_snap.Size, - repos_snap.Size_human, - repos_env.Description + repos_snap.Size_human FROM repos LEFT JOIN repos_snap ON repos.Id = repos_snap.Id_repo @@ -78,6 +79,8 @@ public function listByGroup(string $groupName) : array repos.Releasever, repos.Source, repos.Package_type, + repos.Description, + repos.Tags, repos_env.Env, repos_snap.Date, repos_snap.Time, @@ -87,8 +90,7 @@ public function listByGroup(string $groupName) : array repos_snap.Reconstruct, repos_snap.Size, repos_snap.Size_human, - repos_snap.Status, - repos_env.Description + repos_snap.Status FROM repos LEFT JOIN repos_snap ON repos.Id = repos_snap.Id_repo @@ -107,6 +109,8 @@ public function listByGroup(string $groupName) : array repos.Releasever, repos.Source, repos.Package_type, + repos.Description, + repos.Tags, repos_env.Env, repos_snap.Date, repos_snap.Time, @@ -116,8 +120,7 @@ public function listByGroup(string $groupName) : array repos_snap.Reconstruct, repos_snap.Size, repos_snap.Size_human, - repos_snap.Status, - repos_env.Description + repos_snap.Status FROM repos LEFT JOIN repos_snap ON repos.Id = repos_snap.Id_repo @@ -168,7 +171,9 @@ public function listNameOnly(bool $withActiveSnapshots) repos.Dist, repos.Section, repos.Source, - repos.Package_type + repos.Package_type, + repos.Description, + repos.Tags FROM repos INNER JOIN repos_snap ON repos_snap.Id_repo = repos.Id @@ -205,7 +210,16 @@ public function listSnapshots(int $repoId) : array ORDER BY Env ), '' - ) AS Environments + ) AS Environments, + COALESCE( + ( + SELECT GROUP_CONCAT(Id, ',') + FROM repos_env + WHERE Id_snap = repos_snap.Id + ORDER BY Env + ), + '' + ) AS EnvironmentIds FROM repos_snap WHERE repos_snap.Id_repo = :repoId AND repos_snap.Status = 'active' @@ -222,4 +236,117 @@ public function listSnapshots(int $repoId) : array return $data; } + + /** + * Return the list of repositories matching the specified filters, with their latest active snapshot Id + * Repositories without any active snapshot are ignored + * An empty group Id / tags / package type means 'no filtering on this criteria' + */ + public function listByTarget(string $groupId = '', array $tags = [], string $packageType = '') : array + { + $data = []; + $conditions = []; + $bindings = []; + + // Filter on package type + if ($packageType !== '') { + $conditions[] = 'repos.Package_type = :packageType'; + $bindings[':packageType'] = $packageType; + } + + /** + * Filter on group + * Group Id 0 is a fictitious group ('Default') containing all the repositories that do not belong to any group + * The group Id is used instead of its name, so that renaming a group has no impact on the target + */ + if ($groupId !== '') { + if ($groupId === '0') { + $conditions[] = 'repos.Id NOT IN (SELECT Id_repo FROM group_members)'; + } else { + $conditions[] = 'repos.Id IN (SELECT group_members.Id_repo FROM group_members WHERE group_members.Id_group = :groupId)'; + $bindings[':groupId'] = $groupId; + } + } + + /** + * Filter on tags + * A repository must have ALL the specified tags to be selected + * Tags are stored as a comma-separated string, so the column is surrounded by commas to search for an exact tag + */ + foreach (array_values($tags) as $index => $tag) { + $placeholder = ':tag' . $index; + $conditions[] = "(',' || IFNULL(repos.Tags, '') || ',') LIKE " . $placeholder; + $bindings[$placeholder] = '%,' . $tag . ',%'; + } + + $where = empty($conditions) ? '' : ' WHERE ' . implode(' AND ', $conditions); + + try { + // The latest active snapshot Id is retrieved with a subquery, then rows without snapshot are filtered out + $stmt = $this->db->prepare("SELECT * FROM ( + SELECT + repos.Id AS repoId, + repos.Name, + repos.Dist, + repos.Section, + repos.Releasever, + repos.Package_type, + repos.Tags, + ( + SELECT repos_snap.Id + FROM repos_snap + WHERE repos_snap.Id_repo = repos.Id + AND repos_snap.Status = 'active' + ORDER BY repos_snap.Date DESC, repos_snap.Time DESC, repos_snap.Id DESC + LIMIT 1 + ) AS snapId + FROM repos" . $where . " + ) WHERE snapId IS NOT NULL + ORDER BY Name ASC, Releasever ASC, Dist ASC, Section ASC"); + + foreach ($bindings as $placeholder => $value) { + $stmt->bindValue($placeholder, $value); + } + + $result = $stmt->execute(); + } catch (Exception $e) { + DbLog::error($e); + return $data; + } + + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $data[] = $row; + } + + return $data; + } + + /** + * Return the list of all distinct tags used by repositories + */ + public function listTags(): array + { + $data = []; + + try { + $result = $this->db->query("SELECT DISTINCT Tags FROM repos WHERE Tags IS NOT NULL AND Tags != ''"); + } catch (Exception $e) { + DbLog::error($e); + return $data; + } + + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + foreach (explode(',', $row['Tags']) as $tag) { + $tag = trim($tag); + + if ($tag !== '' and !in_array($tag, $data)) { + $data[] = $tag; + } + } + } + + sort($data); + + return $data; + } } diff --git a/www/models/Repo/Repo.php b/www/models/Repo/Repo.php index f1f005500..c101f1180 100644 --- a/www/models/Repo/Repo.php +++ b/www/models/Repo/Repo.php @@ -52,6 +52,8 @@ public function getAllById(string|null $repoId, string|null $snapId, string|null repos.Section, repos.Source, repos.Package_type, + repos.Description, + repos.Tags, repos_snap.Id AS snapId, repos_snap.Date, repos_snap.Time, @@ -66,7 +68,6 @@ public function getAllById(string|null $repoId, string|null $snapId, string|null repos_snap.Id_repo, repos_env.Id AS envId, repos_env.Env, - repos_env.Description, repos_env.Id_snap FROM repos INNER JOIN repos_snap @@ -88,6 +89,8 @@ public function getAllById(string|null $repoId, string|null $snapId, string|null repos.Section, repos.Source, repos.Package_type, + repos.Description, + repos.Tags, repos_snap.Id AS snapId, repos_snap.Date, repos_snap.Time, @@ -115,7 +118,9 @@ public function getAllById(string|null $repoId, string|null $snapId, string|null repos.Dist, repos.Section, repos.Source, - repos.Package_type + repos.Package_type, + repos.Description, + repos.Tags FROM repos WHERE repos.Id = :repoId"); $stmt->bindValue(':repoId', $repoId); @@ -128,6 +133,8 @@ public function getAllById(string|null $repoId, string|null $snapId, string|null repos.Section, repos.Source, repos.Package_type, + repos.Description, + repos.Tags, repos_snap.Id AS snapId, repos_snap.Date, repos_snap.Time, @@ -154,6 +161,8 @@ public function getAllById(string|null $repoId, string|null $snapId, string|null repos.Section, repos.Source, repos.Package_type, + repos.Description, + repos.Tags, repos_snap.Id AS snapId, repos_snap.Date, repos_snap.Time, @@ -168,7 +177,6 @@ public function getAllById(string|null $repoId, string|null $snapId, string|null repos_snap.Id_repo, repos_env.Id AS envId, repos_env.Env, - repos_env.Description, repos_env.Id_snap FROM repos INNER JOIN repos_snap @@ -178,6 +186,7 @@ public function getAllById(string|null $repoId, string|null $snapId, string|null WHERE repos_env.Id = :envId"); $stmt->bindValue(':envId', $envId); } + $result = $stmt->execute(); } catch (Exception $e) { DbLog::error($e); @@ -519,4 +528,34 @@ public function updateSource(int $repoId, string $source): void unset($stmt); } + + /** + * Update description + */ + public function updateDescription(int $id, string $description): void + { + try { + $stmt = $this->db->prepare("UPDATE repos SET Description = :description WHERE Id = :id"); + $stmt->bindValue(':description', $description); + $stmt->bindValue(':id', $id); + $stmt->execute(); + } catch (Exception $e) { + DbLog::error($e); + } + } + + /** + * Update tags + */ + public function updateTags(int $id, string $tags): void + { + try { + $stmt = $this->db->prepare("UPDATE repos SET Tags = :tags WHERE Id = :id"); + $stmt->bindValue(':tags', $tags); + $stmt->bindValue(':id', $id); + $stmt->execute(); + } catch (Exception $e) { + DbLog::error($e); + } + } } diff --git a/www/models/Repo/Rpm.php b/www/models/Repo/Rpm.php index 29755488a..e5d65b79e 100644 --- a/www/models/Repo/Rpm.php +++ b/www/models/Repo/Rpm.php @@ -35,38 +35,6 @@ public function getIdByNameReleasever(string $name, string $releaseVersion) : in return $id; } - /** - * Return repository environment description - */ - public function getDescriptionByName(string $name, string $releaseVersion, string $env) : string|null - { - $description = null; - - try { - $stmt = $this->db->prepare("SELECT repos_env.Description FROM repos_env - INNER JOIN repos_snap - ON repos_snap.Id = repos_env.Id_snap - INNER JOIN repos - ON repos.Id = repos_snap.Id_repo - WHERE repos.Name = :name - AND repos.Releasever = :releaseVersion - AND repos_env.Env = :env - AND repos_snap.Status = 'active'"); - $stmt->bindValue(':name', $name); - $stmt->bindValue(':releaseVersion', $releaseVersion); - $stmt->bindValue(':env', $env); - $result = $stmt->execute(); - } catch (Exception $e) { - DbLog::error($e); - } - - while ($row = $result->fetchArray(SQLITE3_ASSOC)) { - $description = $row['Description']; - } - - return $description; - } - /** * Return environment Id from repo name */ @@ -186,7 +154,7 @@ public function add(string $name, string $releaseVersion, string $source = '') : try { $stmt = $this->db->prepare("INSERT INTO repos ('Name', 'Releasever', 'Source', 'Package_type') VALUES (:name, :releaseVersion, :source, 'rpm')"); $stmt->bindValue(':name', $name, SQLITE3_TEXT); - $stmt->bindValue(':releaseVersion', $releaseVersion); + $stmt->bindValue(':releaseVersion', $releaseVersion, SQLITE3_TEXT); $stmt->bindValue(':source', $source, SQLITE3_TEXT); $stmt->execute(); } catch (Exception $e) { diff --git a/www/models/Task/Listing.php b/www/models/Task/Listing.php index 8f44e308e..37cf33b11 100644 --- a/www/models/Task/Listing.php +++ b/www/models/Task/Listing.php @@ -206,4 +206,90 @@ public function getDone(string $type, bool $withOffset, int $offset): array return $data; } + + /** + * Get all sub-tasks generated by a parent task + * (e.g. a scheduled task targeting a group of repositories generates one task per repository) + * It is possible to add an offset to the request + */ + public function getByParentId(int $parentTaskId, bool $withOffset = false, int $offset = 0): array + { + $data = []; + + try { + $query = "SELECT * FROM tasks + WHERE Parent_task_id = :parentTaskId + ORDER BY Date DESC, Time DESC, Id ASC"; + + // Add offset if needed + if ($withOffset === true) { + $query .= " LIMIT 10 OFFSET :offset"; + } + + // Prepare query + $stmt = $this->db->prepare($query); + $stmt->bindValue(':parentTaskId', $parentTaskId, SQLITE3_INTEGER); + $stmt->bindValue(':offset', $offset, SQLITE3_INTEGER); + $result = $stmt->execute(); + } catch (Exception $e) { + DbLog::error($e); + } + + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $data[] = $row; + } + + return $data; + } + + /** + * Count sub-tasks generated by a parent task, grouped by status + */ + public function countStatusByParentId(int $parentTaskId): array + { + $data = []; + + try { + $stmt = $this->db->prepare("SELECT Status, COUNT(*) AS Total FROM tasks WHERE Parent_task_id = :parentTaskId GROUP BY Status"); + $stmt->bindValue(':parentTaskId', $parentTaskId, SQLITE3_INTEGER); + $result = $stmt->execute(); + } catch (Exception $e) { + DbLog::error($e); + } + + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $data[$row['Status']] = (int) $row['Total']; + } + + return $data; + } + + /** + * Count sub-tasks generated by multiple parent tasks, grouped by parent task Id and status + */ + public function countStatusByParentIds(array $parentTaskIds): array + { + $data = []; + + // Ids are casted to int as they are directly injected into the query + $parentTaskIds = array_map('intval', array_filter($parentTaskIds, 'is_numeric')); + + if (empty($parentTaskIds)) { + return $data; + } + + try { + $result = $this->db->query("SELECT Parent_task_id, Status, COUNT(*) AS Total FROM tasks + WHERE Parent_task_id IN (" . implode(',', $parentTaskIds) . ") + GROUP BY Parent_task_id, Status"); + } catch (Exception $e) { + DbLog::error($e); + } + + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $data[$row['Parent_task_id']][$row['Status']] = (int) $row['Total']; + } + + return $data; + } } diff --git a/www/models/Task/Task.php b/www/models/Task/Task.php index e863ccba4..a3519f81a 100644 --- a/www/models/Task/Task.php +++ b/www/models/Task/Task.php @@ -282,14 +282,17 @@ public function exists(int $id) : bool /** * Add a new task to the database + * If $parentTaskId is provided, the task is linked to the task that generated it + * (e.g. a scheduled task targeting a group of repositories) */ - public function new(string $type, string $rawParams, string $status) : int + public function new(string $type, string $rawParams, string $status, int|null $parentTaskId = null) : int { try { - $stmt = $this->db->prepare("INSERT INTO tasks (Type, Raw_params, Status) VALUES (:type, :rawParams, :status)"); + $stmt = $this->db->prepare("INSERT INTO tasks (Type, Raw_params, Status, Parent_task_id) VALUES (:type, :rawParams, :status, :parentTaskId)"); $stmt->bindValue(':type', $type); $stmt->bindValue(':rawParams', $rawParams); $stmt->bindValue(':status', $status); + $stmt->bindValue(':parentTaskId', $parentTaskId); $stmt->execute(); } catch (Exception $e) { DbLog::error($e); diff --git a/www/models/User/Preference.php b/www/models/User/Preference.php new file mode 100644 index 000000000..f0e8e37d2 --- /dev/null +++ b/www/models/User/Preference.php @@ -0,0 +1,69 @@ +getConnection('main'); + } + + /** + * Get user preferences + */ + public function get(int $id): string + { + $data = ''; + + try { + $stmt = $this->db->prepare("SELECT Preferences FROM user_preferences WHERE User_id = :id"); + $stmt->bindValue(':id', $id); + $result = $stmt->execute(); + } catch (Exception $e) { + DbLog::error($e); + } + + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $data = $row['Preferences']; + } + + return $data; + } + + /** + * Set user preferences + */ + public function set(int $id, string $preferences): void + { + // First, delete existing preferences if any + $this->delete($id); + + try { + // Then insert new preferences + $stmt = $this->db->prepare("INSERT INTO user_preferences (User_id, Preferences) VALUES (:id, :preferences)"); + $stmt->bindValue(':preferences', $preferences); + $stmt->bindValue(':id', $id); + $stmt->execute(); + } catch (Exception $e) { + DbLog::error($e); + } + } + + /** + * Delete user preferences + */ + public function delete(int $id): void + { + try { + $stmt = $this->db->prepare("DELETE FROM user_preferences WHERE User_id = :id"); + $stmt->bindValue(':id', $id); + $stmt->execute(); + } catch (Exception $e) { + DbLog::error($e); + } + } +} diff --git a/www/public/assets/icons/calendar.svg b/www/public/assets/icons/calendar.svg new file mode 100644 index 000000000..ababc8c46 --- /dev/null +++ b/www/public/assets/icons/calendar.svg @@ -0,0 +1,40 @@ + + + + + + + diff --git a/www/public/assets/icons/cloud-download.svg b/www/public/assets/icons/cloud-download.svg new file mode 100644 index 000000000..f3e627837 --- /dev/null +++ b/www/public/assets/icons/cloud-download.svg @@ -0,0 +1,42 @@ + + + + + + + diff --git a/www/public/assets/icons/deb.svg b/www/public/assets/icons/deb.svg deleted file mode 100644 index d4a816fed..000000000 --- a/www/public/assets/icons/deb.svg +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - -DEB - diff --git a/www/public/assets/icons/disk.svg b/www/public/assets/icons/disk.svg new file mode 100644 index 000000000..65cd5a162 --- /dev/null +++ b/www/public/assets/icons/disk.svg @@ -0,0 +1,46 @@ + + + + + + + + diff --git a/www/public/assets/icons/key2.svg b/www/public/assets/icons/key2.svg deleted file mode 100644 index b99ae7ae0..000000000 --- a/www/public/assets/icons/key2.svg +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - diff --git a/www/public/assets/icons/locked-red.svg b/www/public/assets/icons/locked-red.svg new file mode 100644 index 000000000..8c66eb377 --- /dev/null +++ b/www/public/assets/icons/locked-red.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/www/public/assets/icons/locked.svg b/www/public/assets/icons/locked.svg new file mode 100644 index 000000000..961c46d6e --- /dev/null +++ b/www/public/assets/icons/locked.svg @@ -0,0 +1,40 @@ + + + + + + diff --git a/www/public/assets/icons/save.svg b/www/public/assets/icons/save.svg new file mode 100644 index 000000000..07f7781f8 --- /dev/null +++ b/www/public/assets/icons/save.svg @@ -0,0 +1,44 @@ + + + + + + + diff --git a/www/public/assets/icons/server.svg b/www/public/assets/icons/server.svg index dacbe0db8..128b042e5 100644 --- a/www/public/assets/icons/server.svg +++ b/www/public/assets/icons/server.svg @@ -1,20 +1,23 @@ + + + id="defs10" /> + inkscape:current-layer="svg6" /> + style="stroke-width:1.19298;fill:#ffffff;fill-opacity:1" /> - + style="stroke-width:1.19298;fill:#ffffff;fill-opacity:1" /> diff --git a/www/public/assets/icons/shield-warning.svg b/www/public/assets/icons/shield-warning.svg new file mode 100644 index 000000000..2fdd49f3e --- /dev/null +++ b/www/public/assets/icons/shield-warning.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + diff --git a/www/public/assets/icons/shield.svg b/www/public/assets/icons/shield.svg new file mode 100644 index 000000000..30ec19dd0 --- /dev/null +++ b/www/public/assets/icons/shield.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + diff --git a/www/public/assets/icons/tag.svg b/www/public/assets/icons/tag.svg new file mode 100644 index 000000000..df590be23 --- /dev/null +++ b/www/public/assets/icons/tag.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/www/public/assets/icons/trash.svg b/www/public/assets/icons/trash.svg new file mode 100644 index 000000000..b9434a636 --- /dev/null +++ b/www/public/assets/icons/trash.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/www/public/assets/icons/unlocked.svg b/www/public/assets/icons/unlocked.svg new file mode 100644 index 000000000..512e65f19 --- /dev/null +++ b/www/public/assets/icons/unlocked.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/www/public/assets/icons/upload.svg b/www/public/assets/icons/upload.svg new file mode 100644 index 000000000..f4238b3ef --- /dev/null +++ b/www/public/assets/icons/upload.svg @@ -0,0 +1,42 @@ + + + + + + + diff --git a/www/public/resources/js/app/checkbox.config.js b/www/public/resources/js/app/checkbox.config.js index 6870dc631..0e07aba46 100644 --- a/www/public/resources/js/app/checkbox.config.js +++ b/www/public/resources/js/app/checkbox.config.js @@ -193,7 +193,6 @@ const checkboxesCallback = { 'buttons': [ { 'text': 'Edit', - 'color': 'blue-alt', 'callback': function () { mypanel.get('tasks/edit', { tasks: id @@ -202,7 +201,6 @@ const checkboxesCallback = { }, { 'text': 'Enable', - 'color': 'blue-alt', 'callback': function () { ajaxRequest( // Controller: @@ -218,13 +216,12 @@ const checkboxesCallback = { // Print error alert: true ).then(function () { - mycontainer.reload('tasks/list'); + mycontainer.reload('tasks/tasks'); }); } }, { 'text': 'Disable', - 'color': 'blue-alt', 'callback': function () { ajaxRequest( // Controller: @@ -240,7 +237,7 @@ const checkboxesCallback = { // Print error alert: true ).then(function () { - mycontainer.reload('tasks/list'); + mycontainer.reload('tasks/tasks'); }); } }, @@ -262,7 +259,7 @@ const checkboxesCallback = { // Print error alert: true ).then(function () { - mycontainer.reload('tasks/list'); + mycontainer.reload('tasks/tasks'); }); } }] @@ -295,7 +292,7 @@ const checkboxesCallback = { // Print error alert: true ).then(function () { - mycontainer.reload('tasks/list'); + mycontainer.reload('tasks/tasks'); }); } }] diff --git a/www/public/resources/js/app/container.config.js b/www/public/resources/js/app/container.config.js index ed5e1412a..5fdec9a12 100644 --- a/www/public/resources/js/app/container.config.js +++ b/www/public/resources/js/app/container.config.js @@ -6,6 +6,32 @@ const containersUsingMorphdom = [ // 'repos/properties', ]; +/** + * Containers supporting a partial reload. + * When the user is actively interacting with such a container (focused element, checked + * checkbox, non-empty text input), the whole container is not replaced. Instead, only the + * items that are not busy themselves are refreshed, so the user selections and inputs are kept + * while the rest of the container stays up to date. + * + * item: selector of the repeated item inside the container + * key: attribute uniquely identifying an item, used to match current and new items + * @type {Object} + */ +const containersPartialReload = { + 'repos/list': { item: '.repo-item-wrapper', key: 'repo-id' }, +}; + +/** + * Functions executed after a container has been reloaded + * @type {Object} + */ +const postReloadFunctions = { + // Re-apply the current search filter on the repositories that have just been refreshed + 'repos/list': function () { + myrepo.search(); + }, +}; + /** * Default morphdom skip rules, apply to all containers * @type {Array} diff --git a/www/public/resources/js/classes/Checkbox.js b/www/public/resources/js/classes/Checkbox.js index e093eefd2..8adbcd1c8 100644 --- a/www/public/resources/js/classes/Checkbox.js +++ b/www/public/resources/js/classes/Checkbox.js @@ -18,12 +18,6 @@ class Checkbox // If the 'Select all' checkbox is checked, check the child checkbox and make it visible, else uncheck the child checkbox and remove any custom visibility so it returns to default checkboxes.each(function () { - // If the checkbox is the latest one, directly simulate a click on it to trigger any event attached to it - if (--count === 0) { - $(this).trigger('click'); - return; - } - if (status) { $(this).prop('checked', true); $(this).css('opacity', '1'); @@ -31,7 +25,15 @@ class Checkbox $(this).prop('checked', false); $(this).css('opacity', ''); } + + // Trigger change event so visual state (e.g. task-selected) is updated + $(this).trigger('change'); }); + + // Trigger select on the last checkbox to update the confirmbox, without toggling its state + if (checkboxes.length) { + this.select(checkboxes.last()); + } } /** diff --git a/www/public/resources/js/classes/ConfirmBox.js b/www/public/resources/js/classes/ConfirmBox.js index e7c33e787..158d4d980 100644 --- a/www/public/resources/js/classes/ConfirmBox.js +++ b/www/public/resources/js/classes/ConfirmBox.js @@ -17,7 +17,7 @@ class ConfirmBox // If there is a title if (data.title != "") { - innerHtml += '
    '; + innerHtml += '
    '; innerHtml += '
    ' + data.title.toUpperCase() + '
    '; innerHtml += ''; innerHtml += '
    '; @@ -38,7 +38,7 @@ class ConfirmBox if (!empty(data.buttons)) { var id = 0; for (const [key, value] of Object.entries(data.buttons)) { - innerHtml += '
    ' + value.text + '
    '; + innerHtml += '
    ' + value.text + '
    '; id++; } } diff --git a/www/public/resources/js/classes/Container.js b/www/public/resources/js/classes/Container.js index 374676bc1..1d27f8bfc 100644 --- a/www/public/resources/js/classes/Container.js +++ b/www/public/resources/js/classes/Container.js @@ -7,6 +7,8 @@ class Container { reload(container, identifier = null) { var useMorphdom = false; + var partialConfig = (typeof containersPartialReload !== 'undefined') ? containersPartialReload[container] : null; + var partial = false; return new Promise((resolve, reject) => { try { @@ -15,14 +17,25 @@ class Container { return; } + /** + * If the user is actively interacting with the container, only the items that + * are not busy will be refreshed, to avoid losing selections and inputs + */ + if (partialConfig && this._isBusy($('.reloadable-container[container="' + container + '"]'))) { + partial = true; + } + // Print a loading icon on the bottom of the page mylayout.printLoading(); /** * Check if container has children with class .veil-on-reload * If so print a veil on them + * The veil is not printed on a partial reload, as the user is working on the container */ - mylayout.printLoadingVeilByParentClass('reloadable-container[container="' + container + '"]'); + if (!partial) { + mylayout.printLoadingVeilByParentClass('reloadable-container[container="' + container + '"]'); + } ajaxRequest( // Controller: @@ -45,8 +58,12 @@ class Container { useMorphdom = true; } + // If only the non-busy items must be refreshed + if (partial) { + this._partialReplace(container, partialConfig, jsonValue.message); + // If morphdom must be used - if (useMorphdom) { + } else if (useMorphdom) { // Replace with new content using morphdom morphdom($('.reloadable-container[container="' + container + '"]')[0], jsonValue.message, { // Avoid some elements to be updated @@ -172,4 +189,65 @@ class Container { return false; } + + /** + * Replace only the items of a container that are not busy, keeping the ones the user is working on + * @param {string} container + * @param {Object} config + * @param {string} html + */ + _partialReplace(container, config, html) { + const current = $('.reloadable-container[container="' + container + '"]'); + const received = $('
    ').html(html); + + received.find(config.item).each((index, newItem) => { + const key = $(newItem).attr(config.key); + + // Item cannot be matched without a key + if (!key) { + return; + } + + // Find the matching item currently displayed + const existing = current.find(config.item).filter((i, el) => $(el).attr(config.key) === key); + + // The item does not exist yet on the page, it will show up on the next complete reload + if (!existing.length) { + return; + } + + // Leave the items the user is currently working on untouched + if (this._isBusy(existing)) { + return; + } + + existing.replaceWith(newItem); + }); + } + + /** + * Check if the user is actively interacting with an element (focus, checked checkbox, filled input) + * @param {jQuery} element + * @returns {boolean} + */ + _isBusy(element) { + // An element inside has the focus + if (element.find(':focus').length) { + return true; + } + + // A checkbox inside is checked + if (element.find('input[type="checkbox"]:checked').length) { + return true; + } + + // A text input or a textarea inside is filled + if (element.find('input[type="text"], input[type="search"], input:not([type]), textarea').filter(function () { + return $(this).val().trim() !== ''; + }).length) { + return true; + } + + return false; + } } diff --git a/www/public/resources/js/classes/Environment.js b/www/public/resources/js/classes/Environment.js index ae2ba40ba..da6d75354 100644 --- a/www/public/resources/js/classes/Environment.js +++ b/www/public/resources/js/classes/Environment.js @@ -1,26 +1,82 @@ class Environment { /** - * Upodate environment description - * @param {*} id - * @param {*} description + * Add a new environment */ - updateDescription(id, description) + add() { ajaxRequest( // Controller: - 'repo/environment', + 'environment', // Action: - 'update-description', + 'add', // Data: { - envId: id, - description: description + name: $('input[type="text"][name="add-env-name"]').val(), + color: $('input[type="color"][name="add-env-color"]').val() }, // Print success alert: true, // Print error alert: true - ); + ).then(function () { + mylayout.reloadContentById('envs-container'); + }); + } + + /** + * Edit environments + */ + edit() + { + var envs = []; + + $('div#current-envs').find('.env-line').each(function () { + envs.push({ + name: $(this).find('input[type="text"][name="env-name"]').val(), + color: $(this).find('input[type="color"][name="env-color"]').val(), + protected: $(this).find('img.env-protection').attr('protected') + }); + }); + + ajaxRequest( + // Controller: + 'environment', + // Action: + 'edit', + // Data: + { + envs: envs, + }, + // Print success alert: + true, + // Print error alert: + true + ).then(function () { + mylayout.reloadContentById('envs-container'); + }); + } + + /** + * Delete an environment + */ + delete(id) + { + ajaxRequest( + // Controller: + 'environment', + // Action: + 'delete', + // Data: + { + id: id, + }, + // Print success alert: + true, + // Print error alert: + true + ).then(function () { + mylayout.reloadContentById('envs-container'); + }); } } diff --git a/www/public/resources/js/classes/Host.js b/www/public/resources/js/classes/Host.js index ff1702665..23eb4ecba 100644 --- a/www/public/resources/js/classes/Host.js +++ b/www/public/resources/js/classes/Host.js @@ -50,7 +50,6 @@ class Host buttons.push( { 'text': 'Request general information', - 'color': 'blue-alt', 'callback': function () { executeAction('request-general-infos', hosts); } @@ -62,7 +61,6 @@ class Host buttons.push( { 'text': 'Request package information', - 'color': 'blue-alt', 'callback': function () { executeAction('request-packages-infos', hosts); } @@ -74,7 +72,6 @@ class Host buttons.push( { 'text': 'Update packages', - 'color': 'blue-alt', 'callback': function () { mypanel.get('hosts/requests/update-packages', { hostsId: hosts @@ -88,7 +85,6 @@ class Host buttons.push( { 'text': 'Export to CSV', - 'color': 'blue-alt', 'callback': function () { myhost.export(hosts); } diff --git a/www/public/resources/js/classes/Host/HostSearch.js b/www/public/resources/js/classes/Host/HostSearch.js index d0d78dc2c..9f00e252e 100644 --- a/www/public/resources/js/classes/Host/HostSearch.js +++ b/www/public/resources/js/classes/Host/HostSearch.js @@ -36,7 +36,9 @@ class HostSearch // Show all group containers (in case they were hidden during a previous search) $('.hosts-group-container').show(); - $('.host-line').removeClass('flex').hide(); + + // Hide all host lines, only those corresponding to the search will be re-displayed + $('.host-line').hide(); /** * Parse filter parameters from search input. @@ -243,7 +245,6 @@ class HostSearch $('#hosts-search').addClass('hide'); $('.hosts-group-container, .host-line').show(); - $('.host-line').addClass('flex'); $('#hosts').show(); } diff --git a/www/public/resources/js/classes/Permission/Repo.js b/www/public/resources/js/classes/Permission/Repo.js new file mode 100644 index 000000000..91d903420 --- /dev/null +++ b/www/public/resources/js/classes/Permission/Repo.js @@ -0,0 +1,27 @@ +/** + * Check user permissions on repositories + * Mirrors the backend Controllers\User\Permission\Repo class + */ +class RepoPermission { + /** + * Check if the user is allowed to perform a specific action on repositories + * Admins are allowed to do everything (no 'user_permissions' cookie means the user is an admin, + * see Controllers\App\Permissions::load() which deletes the cookie for admins) + * @param {string} action + * @returns {boolean} + */ + allowedAction(action) + { + if (!mycookie.exists('user_permissions')) { + return true; + } + + const userPermissions = JSON.parse(mycookie.get('user_permissions')); + + if (userPermissions.repositories && userPermissions.repositories['allowed-actions'] && userPermissions.repositories['allowed-actions'].includes(action)) { + return true; + } + + return false; + } +} diff --git a/www/public/resources/js/classes/Permission/Tasks.js b/www/public/resources/js/classes/Permission/Tasks.js new file mode 100644 index 000000000..868d00c2f --- /dev/null +++ b/www/public/resources/js/classes/Permission/Tasks.js @@ -0,0 +1,46 @@ +/** + * Check user permissions on tasks + * Mirrors the backend Controllers\User\Permission\Task class + */ +class TaskPermission { + /** + * Check if the user has any permission on tasks + * Admins are allowed to do everything (no 'user_permissions' cookie means the user is an admin, + * see Controllers\App\Permissions::load() which deletes the cookie for admins) + * @returns {boolean} + */ + allowed() + { + if (!mycookie.exists('user_permissions')) { + return true; + } + + const userPermissions = JSON.parse(mycookie.get('user_permissions')); + + if (userPermissions.tasks && userPermissions.tasks['allowed-actions'] && userPermissions.tasks['allowed-actions'].length > 0) { + return true; + } + + return false; + } + + /** + * Check if the user is allowed to perform a specific action on tasks + * @param {string} action + * @returns {boolean} + */ + allowedAction(action) + { + if (!mycookie.exists('user_permissions')) { + return true; + } + + const userPermissions = JSON.parse(mycookie.get('user_permissions')); + + if (userPermissions.tasks && userPermissions.tasks['allowed-actions'] && userPermissions.tasks['allowed-actions'].includes(action)) { + return true; + } + + return false; + } +} diff --git a/www/public/resources/js/classes/Repo.js b/www/public/resources/js/classes/Repo.js index 8e9163358..a97d867c3 100644 --- a/www/public/resources/js/classes/Repo.js +++ b/www/public/resources/js/classes/Repo.js @@ -6,7 +6,7 @@ class Repo { { // If input is empty, then show all repos and quit if (!$("#repo-search-input").val()) { - $('.repos-list-group, .repos-list-group-flex-div').show(); + $('.repos-list-group, .repos-row, .repo-item-wrapper, .repo-item, .group-repo-name').show(); return; } @@ -14,79 +14,93 @@ class Repo { /** * Retrieve search input value - * Convert to uppercase to ignore case when searching + * Split into individual terms for multi-word search */ - const search = $("#repo-search-input").val().toUpperCase().trim(); - - // First, hide all repos groups - $('.repos-list-group, .repos-list-group-flex-div').hide(); - - // Then search in every repo group of there is a repo or dist or section matching the search - $('.repos-list-group').each(function () { - // Retrieve all repos lines - $('.item-repo').each(function () { - const name = $(this).attr('name'); - const dist = $(this).attr('dist'); - const section = $(this).attr('section'); - const releasever = $(this).attr('releasever'); - - // If repo name contains the search then display 'repos-list-group-flex-div' and its parent 'repos-list-group' - if (name.toUpperCase().indexOf(search) > -1) { - $(this).parents('.repos-list-group-flex-div').show(); - $(this).parents('.repos-list-group').show(); - } + const terms = $("#repo-search-input").val().toUpperCase().trim().split(/\s+/); + + // First, hide all groups and repo items (hide the wrapper too, otherwise + // it stays in the grid flow as an empty cell and shifts the visible cards) + $('.repos-list-group, .repos-row, .repo-item-wrapper, .repo-item, .group-repo-name').hide(); + + // Search through all repo items using their data attributes and visible text + $('.repo-item').each(function () { + const name = ($(this).attr('data-name') || '').toUpperCase(); + const dist = ($(this).attr('data-dist') || '').toUpperCase(); + const section = ($(this).attr('data-section') || '').toUpperCase(); + const releasever = ($(this).attr('data-releasever') || '').toUpperCase(); + const type = ($(this).attr('data-type') || '').toUpperCase(); + const packageType = ($(this).attr('data-package-type') || '').toUpperCase(); + const description = ($(this).attr('data-description') || '').toUpperCase(); + const tags = ($(this).attr('data-tags') || '').toUpperCase(); + + // Individual tags of this repo, used for exact 'tag:' matching + const tagsList = tags.split(',').map(function (t) { + return t.trim(); + }).filter(function (t) { + return t.length > 0; + }); - // If repo dist contains the search then display 'repos-list-group-flex-div' and its parent 'repos-list-group' - if (dist.toUpperCase().indexOf(search) > -1) { - $(this).parents('.repos-list-group-flex-div').show(); - $(this).parents('.repos-list-group').show(); - } + // Also include the whole visible text of the repo card (header line with + // name/distribution/section, snapshot dates, environments, tags...) so the + // search keeps working even if a data attribute is missing + const visibleText = ($(this).text() || '').toUpperCase(); - // If repo section contains the search then display 'repos-list-group-flex-div' and its parent 'repos-list-group' - if (section.toUpperCase().indexOf(search) > -1) { - $(this).parents('.repos-list-group-flex-div').show(); - $(this).parents('.repos-list-group').show(); - } + // All searchable content for this repo + const searchable = name + ' ' + dist + ' ' + section + ' ' + releasever + ' ' + type + ' ' + packageType + ' ' + description + ' ' + tags + ' ' + visibleText; - // If repo releasever contains the search then display 'repos-list-group-flex-div' and its parent 'repos-list-group' - if (releasever.toUpperCase().indexOf(search) > -1) { - $(this).parents('.repos-list-group-flex-div').show(); - $(this).parents('.repos-list-group').show(); + // Check that ALL terms match somewhere in the searchable content + // A 'TAG:xxx' term must match one of this repo's tags exactly, instead of + // matching anywhere in the searchable content (name, description, etc.) + const allMatch = terms.every(function (term) { + if (term.indexOf('TAG:') === 0) { + return tagsList.indexOf(term.substring(4)) > -1; } - }); - // Retrieve all repos environments - $('.item-env').each(function () { - const env = $(this).text().trim(); - - // If env is not empty - if (env != "") { - // If env name contains the search then display 'repos-list-group-flex-div' and its parent 'repos-list-group' - if (env.toUpperCase().indexOf(search) > -1) { - $(this).parents('.repos-list-group-flex-div').show(); - $(this).parents('.repos-list-group').show(); - } - } + return searchable.indexOf(term) > -1; }); - // Retrieve all repos descriptions - $('input[type="text"].repo-description-input').each(function () { - const description = $(this).val().trim(); - - // If description is not empty - if (description != "") { - // If description contains the search then display 'repos-list-group-flex-div' and its parent 'repos-list-group' - if (description.toUpperCase().indexOf(search) > -1) { - $(this).parents('.repos-list-group-flex-div').show(); - $(this).parents('.repos-list-group').show(); - } - } - }); + if (allMatch) { + $(this).show(); + $(this).closest('.repo-item-wrapper').show(); + $(this).closest('.repos-row').show(); + $(this).closest('.repos-row').prev('.group-repo-name').show(); + // Reveal ALL ancestor groups (they can be nested), not just the closest one + $(this).parents('.repos-list-group').show(); + } }); mylayout.hideLoading(); } + /** + * Add a tag to the search input and filter repositories by it + * @param {string} tag + */ + searchByTag(tag) + { + const input = $('#repo-search-input'); + + // Retrieve the terms already present in the search input + const current = input.val().trim(); + const terms = current.length > 0 ? current.split(/\s+/) : []; + + // Use a 'tag:' prefix so the search only matches this exact tag, + // instead of matching the tag name anywhere (e.g. in a repo name) + const searchTerm = 'tag:' + tag; + + // Add the tag only if it is not already present (case-insensitive) + if (!terms.some(function (term) { + return term.toUpperCase() === searchTerm.toUpperCase(); + })) { + terms.push(searchTerm); + } + + input.val(terms.join(' ')); + + // Filter the repositories with the updated search terms + this.search(); + } + /** * Get latest task status for all repos */ @@ -125,191 +139,202 @@ class Repo { } /** - * Print packages tree (root level only – sub-directories are lazy-loaded on expand) + * Switch a repository description

    into an editable input + * @param {*} p jQuery element of the description

    */ - printTree(path) + startEditDescription(p) { - const self = this; + if (!myrepopermission.allowedAction('edit')) { + myalert.print('You do not have permission to edit repositories', 'error'); + return; + } - ajaxRequest( - // Controller: - 'repo/snapshot/browse', - // Action: - 'tree', - // Data: - { - path: path - }, - // Print success alert: - false, - // Print error alert: - true - ).then(function () { - $('#packages-list').html(jsonValue.message); - self._initTreeLevel($('#packages-list')); - self._initLoadMore($('#packages-list')); - self._initSearch(path); - $('#loading-tree').remove(); - $('#browse-search-bar').show(); - $('#explorer').show(); - }); + // If already in edit mode, do nothing + if (p.find('input').length > 0) { + return; + } + + const currentDescription = p.text().trim(); + const repoId = p.attr('repo-id'); + const envId = p.attr('env-id'); + + // Remove empty class (hide placeholder) + p.removeClass('repo-description-empty'); + + // Create input field + const input = $('') + .attr('repo-id', repoId) + .attr('env-id', envId) + .attr('data-original', currentDescription) + .val(currentDescription); + + // Replace

    content with input + p.html(input); + input.focus(); } /** - * Initialise click handlers for one tree level. - * Directories are fetched from the server the first time they are expanded. - * Call this again on any newly injected subtree container. + * Switch a repository tags display into an editable select2 + * @param {*} display jQuery element containing the tags display */ - _initTreeLevel($container) + startEditTags(display) { - const self = this; - - $container.find('div.explorer-toggle').each(function () { - const $toggle = $(this); - - $toggle.prepend('+ '); - - $toggle.on('click', function () { - const $subtree = $toggle.next('ul'); - - // Already loaded: just toggle visibility - if ($toggle.data('loaded')) { - $subtree.toggle(100); - const v = $toggle.html().substring(0, 1); - if (v == '+') { - $toggle.html('-' + $toggle.html().substring(1)); - } else if (v == '-') { - $toggle.html('+' + $toggle.html().substring(1)); - } - return; - } + // Ignore if the user does not have permission to edit repositories + if (!myrepopermission.allowedAction('edit')) { + return; + } + + // If already in edit mode, do nothing + if (display.hasClass('editing')) { + return; + } + + const repoId = display.attr('repo-id'); + + // Current tags of this repo + const currentTags = (display.attr('data-tags') || '').split(',').map(function (tag) { + return tag.trim(); + }).filter(function (tag) { + return tag.length > 0; + }); - // First expand: fetch this directory level from the server - $toggle.data('loaded', true); - $toggle.html('-' + $toggle.html().substring(1)); - - // Show loading indicator inside the empty placeholder - $subtree.html('

  • Loading
  • '); - $subtree.show(); - - ajaxRequest( - // Controller: - 'repo/snapshot/browse', - // Action: - 'tree', - // Data: - { - path: $toggle.data('path') - }, - // Print success alert: - false, - // Print error alert: - true - ).then(function () { - // The server returns a
    ; replace the placeholder
    '; - } - - try { - // If the user does not have permission to rebuild repositories, prevent access to this panel. - if (!RepoPermission::allowedAction('rebuild')) { - throw new Exception('You are not allowed to rebuild repositories'); - } ?> - -

    REBUILD REPOSITORY

    - - getPackageType() == 'rpm' && RPM_SIGN_PACKAGES == 'true') { - $gpgSignChecked = 'checked'; - } - if ($repoController->getPackageType() == 'deb' && DEB_SIGN_REPO == 'true') { - $gpgSignChecked = 'checked'; - } ?> - -
    -
    SIGN WITH GPG
    -

    Signature can extend the task duration.

    - - -

    - -
    - '; - // echo $e->getMessage(); - // echo '
    '; - } ?> - diff --git a/www/views/includes/containers/browse/list.inc.php b/www/views/includes/containers/browse/list.inc.php deleted file mode 100644 index b786b3a7e..000000000 --- a/www/views/includes/containers/browse/list.inc.php +++ /dev/null @@ -1,65 +0,0 @@ -
    -

    SNAPSHOT

    - - getPackageType() == 'rpm') { - $repo = $myrepo->getName() . ' ❯ ' . $myrepo->getReleasever(); - } - if ($myrepo->getPackageType() == 'deb') { - $repo = $myrepo->getName() . ' ❯ ' . $myrepo->getDist() . ' ❯ ' . $myrepo->getSection(); - } ?> - -
    -
    -
    REPOSITORY
    -
    -

    - getPackageType()) ?> -
    -
    - -
    -
    SNAPSHOT
    -

    getDateFormatted() ?>

    -
    - -
    -
    SIZE
    -

    -
    - -
    -
    PACKAGES
    -

    -
    -
    - -
    - getRebuild() == 'needed') { - echo '

    Repository snapshot content has been modified. You have to rebuild metadata.

    '; - } ?> - -
    - -

    Generating tree structure...

    -
    - - - -
    - -
    -
    -
    -
    -
    - - -
    \ No newline at end of file diff --git a/www/views/includes/containers/header/debug-mode.inc.php b/www/views/includes/containers/header/debug-mode.inc.php index 24e976643..4a442d8f6 100644 --- a/www/views/includes/containers/header/debug-mode.inc.php +++ b/www/views/includes/containers/header/debug-mode.inc.php @@ -1,4 +1,4 @@ -
    +
    @@ -31,4 +31,4 @@
    -
    +
    diff --git a/www/views/includes/containers/header/general-error-messages.inc.php b/www/views/includes/containers/header/general-error-messages.inc.php index 907892fe9..60d9624ab 100644 --- a/www/views/includes/containers/header/general-error-messages.inc.php +++ b/www/views/includes/containers/header/general-error-messages.inc.php @@ -1,4 +1,4 @@ -
    +
    -
    +
    ' . $message . '

    '; @@ -30,4 +30,4 @@ -
    +
    diff --git a/www/views/includes/containers/header/general-log-messages.inc.php b/www/views/includes/containers/header/general-log-messages.inc.php index acac98b91..87525d4a9 100644 --- a/www/views/includes/containers/header/general-log-messages.inc.php +++ b/www/views/includes/containers/header/general-log-messages.inc.php @@ -1,4 +1,4 @@ -
    +
    -
    \ No newline at end of file +
    \ No newline at end of file diff --git a/www/views/includes/containers/header/menu.inc.php b/www/views/includes/containers/header/menu.inc.php index 7d45ec77d..4dfe65084 100644 --- a/www/views/includes/containers/header/menu.inc.php +++ b/www/views/includes/containers/header/menu.inc.php @@ -1,197 +1,115 @@ - -
    -
    \ No newline at end of file +
  • + + diff --git a/www/views/includes/containers/header/service-status.inc.php b/www/views/includes/containers/header/service-status.inc.php index 02d9594fe..cb1ed3e01 100644 --- a/www/views/includes/containers/header/service-status.inc.php +++ b/www/views/includes/containers/header/service-status.inc.php @@ -1,4 +1,4 @@ -
    +
    @@ -10,4 +10,4 @@ -
    \ No newline at end of file + \ No newline at end of file diff --git a/www/views/includes/containers/host/packages.inc.php b/www/views/includes/containers/host/packages.inc.php index 1f8303d25..efa0f6992 100644 --- a/www/views/includes/containers/host/packages.inc.php +++ b/www/views/includes/containers/host/packages.inc.php @@ -3,7 +3,7 @@

    Packages installed and available updates.

    + if (empty($packagesInventoredTotal) and empty($packagesAvailableTotal)) : ?>

    Nothing for now!

    The host did not send any packages inventory yet.

    @@ -11,7 +11,7 @@ + if (!empty($packagesInventoredTotal) or !empty($packagesAvailableTotal)) : ?>
    @@ -50,36 +50,9 @@
    -

    packages inventored

    + - - -
    - -
    -
    - -
    - -
    -

    - ' . $item['Name'] . ' (uninstalled)'; - } else { - echo $item['Name']; - } ?> -

    -

    -
    -
    - -
    +
    diff --git a/www/views/includes/containers/host/requests.inc.php b/www/views/includes/containers/host/requests.inc.php index c2b06255e..7f43bf102 100644 --- a/www/views/includes/containers/host/requests.inc.php +++ b/www/views/includes/containers/host/requests.inc.php @@ -16,8 +16,6 @@
    \ No newline at end of file diff --git a/www/views/includes/containers/host/summary.inc.php b/www/views/includes/containers/host/summary.inc.php index 2b2e86f42..8cc37abec 100644 --- a/www/views/includes/containers/host/summary.inc.php +++ b/www/views/includes/containers/host/summary.inc.php @@ -10,28 +10,22 @@
    '; + echo ''; } else { - echo ''; + echo ''; } ?>
    -
    +
    -
    - - Reset -
    +

    Reset

    -
    - - Delete -
    +

    Delete

    @@ -79,7 +73,7 @@ $tooltip = 'No network information available'; } - echo '' . $interface . ''; + echo '' . $interface . ''; } } else { echo '

    Unknown

    '; @@ -135,12 +129,15 @@
    ENVIRONMENT
    - ' . \Controllers\Utils\Generate\Html\Label::envtag($env) . '

    '; - } else { - echo '

    Unknown

    '; - } ?> + +
    + Unknown

    '; + } ?> +
    diff --git a/www/views/includes/containers/hosts/list.inc.php b/www/views/includes/containers/hosts/list.inc.php index ed33977b1..dfdd72c22 100644 --- a/www/views/includes/containers/hosts/list.inc.php +++ b/www/views/includes/containers/hosts/list.inc.php @@ -59,11 +59,11 @@
    -

    No host registered yet.

    +

    No host registered yet!

    Install linupdate on your hosts to register them to Repomanager. This page will display dashboards and informations about the hosts and their packages (installed, available, updated...).

    = 1) : ?>
    -
    +
    -
    +
    SEARCH HOST
    @@ -82,7 +82,7 @@
    -
    +
    SEARCH PACKAGE
    @@ -91,10 +91,10 @@
    -
    -
    -

    Select all hosts

    - +
    +
    + Select all hosts +
    @@ -119,7 +119,7 @@ } ?> '> -
    +
    -
    -
    -

    -

    +
    +
    + + +
    + + 0) : ?> +
    + Select all + +
    +
    -
    -
    - - - -
    +
    +
    = $complianceThresholdCount) { - $class = 'bkg-red'; - } } // Print the host informations - // Here the
    will contain all the host informations in order to be able to search on it (input 'search a host') ?> -
    -
    -
    - - - will contain all the host informations in order to be able to search on it (input 'search a host') + $hostAccent = $agentStatus === 'running' ? 'host-accent-green' : 'host-accent-red'; ?> - if ($agentStatus != 'running') : ?> - - -
    +
    +
    + +
    +
    + -
    - -
    -
    -

    - - - -

    - -

    -
    +

    + + + +

    +

    +
    +
    + +
    +
    +

    +

    -
    - - + -
    + if (!$compactView) : ?> +
    + +

    +

    +
    -
    -
    -
    IP
    -

    -
    - -
    -
    TYPE
    -

    -
    +
    +
    +
    OS
    +

    +
    + +
    +
    TYPE
    +

    +
    -
    -
    AGENT VERSION
    -

    -
    +
    +
    KERNEL
    +

    +
    -
    -
    REBOOT REQUIRED
    -

    - '; - echo 'Yes'; - } else { - echo 'No'; - } ?> -

    -
    +
    +
    ARCH
    +

    +
    -
    -
    OS
    -
    -

    - -
    -
    +
    +
    PROFILE
    +

    +
    -
    -
    OS VERSION
    -

    -
    +
    +
    ENVIRONMENT
    +

    + +

    +
    -
    -
    KERNEL
    -

    -
    +
    +
    AGENT VERSION
    +

    +
    -
    -
    ARCH
    -

    -
    +
    +
    REBOOT REQUIRED
    +

    + '; + echo 'Yes'; + } else { + echo 'No'; + } ?> +

    +
    -
    -
    PROFILE
    -

    -
    +
    +
    INSTALLED
    +

    + +

    +
    -
    -
    ENVIRONMENT
    -

    - -

    -
    +
    +
    AVAILABLE
    +

    + = $complianceThresholdCount) { + echo '' . $packagesAvailableTotal . ''; + } else { + echo '' . $packagesAvailableTotal . ''; + } ?> +

    +
    +
    + +
    + -
    INSTALLED
    -

    - -

    -
    + // Request status + if ($lastPendingRequest['Status'] == 'new') { + $requestStatus = 'Pending'; + $requestStatusIcon = 'pending.svg'; + } + if ($lastPendingRequest['Status'] == 'sent') { + $requestStatus = 'Sent'; + $requestStatusIcon = 'pending.svg'; + } + if ($lastPendingRequest['Status'] == 'running') { + $requestStatus = 'Running'; + $requestStatusIcon = 'loading.svg'; + } + if ($lastPendingRequest['Status'] == 'canceled') { + $requestStatus = 'Canceled'; + $requestStatusIcon = 'warning-red.svg'; + } + if ($lastPendingRequest['Status'] == 'failed') { + $requestStatus = 'Failed'; + $requestStatusIcon = 'error.svg'; + } + if ($lastPendingRequest['Status'] == 'completed') { + $requestStatus = 'Completed'; + $requestStatusIcon = 'check.svg'; + } -
    -
    AVAILABLE
    -

    - = $complianceThresholdCount) { - echo '' . $packagesAvailableTotal . ''; - } else { - echo '' . $packagesAvailableTotal . ''; - } ?> -

    -
    -
    - -
    - = 1 and $failedCount >= 1) { + $requestStatus = 'Partial success'; + $requestStatusIcon = 'warning.svg'; } - // If there was packages to update, retrieve the number of packages updated - if ($responseJson['update']['status'] == 'done' or $responseJson['update']['status'] == 'failed') { - $successCount = $responseJson['update']['success']['count']; - $failedCount = $responseJson['update']['failed']['count']; - - // If the update was successful - if ($responseJson['update']['status'] == 'done') { - $requestStatus = 'Successful'; - $requestStatusIcon = 'check.svg'; - } - - // If the update failed - if ($responseJson['update']['status'] == 'failed') { - $requestStatus = 'Failed with errors'; - $requestStatusIcon = 'error.svg'; - } - - // If there was packages updated AND packages failed - if ($successCount >= 1 and $failedCount >= 1) { - $requestStatus = 'Partial success'; - $requestStatusIcon = 'warning.svg'; - } - - // If there was no packages updated AND packages failed - if ($successCount == 0 and $failedCount >= 1) { - $requestStatus = 'Failed'; - $requestStatusIcon = 'error.svg'; - } - - // Build a short info message - $responseDetails = $successCount . ' package(s) updated, ' . $failedCount . ' failed'; - - // Retrieve the list of packages updated - // $successPackages = $responseJson['update']['success']['packages']; - - // Retrieve the list of packages failed - // $failedPackages = $responseJson['update']['failed']['packages']; + // If there was no packages updated AND packages failed + if ($successCount == 0 and $failedCount >= 1) { + $requestStatus = 'Failed'; + $requestStatusIcon = 'error.svg'; } + + // Build a short info message + $responseDetails = $successCount . ' package(s) updated, ' . $failedCount . ' failed'; + + // Retrieve the list of packages updated + // $successPackages = $responseJson['update']['success']['packages']; + + // Retrieve the list of packages failed + // $failedPackages = $responseJson['update']['failed']['packages']; } } + } - // Only print the request title if it was executed less than 1h ago - if (strtotime($lastPendingRequest['Date'] . ' ' . $lastPendingRequest['Time']) >= strtotime(date('Y-m-d H:i:s') . ' - 1 hour')) : ?> -
    LAST REQUEST
    -
    + // Only print the request title if it was executed less than 1h ago + if (strtotime($lastPendingRequest['Date'] . ' ' . $lastPendingRequest['Time']) >= strtotime(date('Y-m-d H:i:s') . ' - 1 hour')) : ?> +
    LAST REQUEST
    +
    + '; + } else { + echo ' '; + } + } ?> +

    '; - } else { - echo ' '; - } + echo $requestTitleShort; + + if (!empty($requestInfo)) { + echo ' - ' . $requestInfo; + } + + if (!empty($responseDetails)) { + echo ' - ' . $responseDetails; } ?> -

    - -

    -
    - +
    + -
    - - -
    -
    + endif; + endif ?> +
    + -
    - -
    +
    + +
    +
    diff --git a/www/views/includes/containers/hosts/overview.inc.php b/www/views/includes/containers/hosts/overview.inc.php index 1e5051460..de2b2f44c 100644 --- a/www/views/includes/containers/hosts/overview.inc.php +++ b/www/views/includes/containers/hosts/overview.inc.php @@ -174,7 +174,6 @@ \ No newline at end of file diff --git a/www/views/includes/containers/repos/properties.inc.php b/www/views/includes/containers/repos/properties.inc.php deleted file mode 100644 index f9de977aa..000000000 --- a/www/views/includes/containers/repos/properties.inc.php +++ /dev/null @@ -1,124 +0,0 @@ -
    -

    PROPERTIES

    - -
    -
    -
    -
    - - - -
    -
    - - - -
    -
    -
    - - -
    - - -
    - -
    - - -
    LAST SCHEDULED TASK
    -
    - -
    - - - -
    -
    NEXT SCHEDULED TASKS
    -
    - -
    - -
    -
    - - 0) { - echo $scheduledTask['left']['days'] . 'd'; - } else { - echo $scheduledTask['left']['time']; - } ?> - -
    - - -
    - -
    -
    - -
    - - - -
    \ No newline at end of file diff --git a/www/views/includes/containers/repos/repo.inc.php b/www/views/includes/containers/repos/repo.inc.php new file mode 100644 index 000000000..0906498e8 --- /dev/null +++ b/www/views/includes/containers/repos/repo.inc.php @@ -0,0 +1,205 @@ + + +
    +
    +
    +
    +

    + + + + + + + + + + + + + +

    + + +
    + + Rename +
    + + + + + +
    + +
    +
    + '; + echo 'local'; + } elseif ($repo['Type'] == 'mirror') { + echo ''; + echo 'mirror'; + } ?> +
    + +
    + '; + echo 'deb'; + } elseif ($repo['Package_type'] == 'rpm') { + echo ''; + echo 'rpm'; + } ?> +
    +
    +
    + + + +
    + listSnapshots($repo['repoId']) as $snapshot) : + // Generate repo relative path + if ($repo['Package_type'] == 'rpm') { + $repoRelativePath = 'rpm/' .$repo['Name'] . '/' . $repo['Releasever'] . '/' . $snapshot['Date']; + } + + if ($repo['Package_type'] == 'deb') { + $repoRelativePath = 'deb/' . $repo['Name'] . '/' . $repo['Dist'] . '/' . $repo['Section'] . '/' . $snapshot['Date']; + } + + // Check if a task is running on the snapshot + $taskRunning = $repoSnapshotController->taskRunning($snapshot['Id']); ?> + +
    +
    + + + + + + + + +
    + +
    +
    + + +
    + +
    + + + Signed + + + Unsigned + +
    + +
    + + + +
    +
    +
    + +
    + $env) : + $envId = $envIds[$index] ?? ''; + $protectedEnv = in_array($env, $protectedEnvs, true); ?> + +
    > + > + +
    + +
    +
    + +
    + + + + +
    +
    diff --git a/www/views/includes/containers/settings/right-section.inc.php b/www/views/includes/containers/settings/right-section.inc.php index 4e5a3e13c..6586a0038 100644 --- a/www/views/includes/containers/settings/right-section.inc.php +++ b/www/views/includes/containers/settings/right-section.inc.php @@ -1,6 +1,5 @@
    + \Controllers\Layout\Container\Render::render('settings/debug-mode'); ?>
    \ No newline at end of file diff --git a/www/views/includes/containers/settings/settings.inc.php b/www/views/includes/containers/settings/settings.inc.php index e62bb4e10..a95368cb7 100644 --- a/www/views/includes/containers/settings/settings.inc.php +++ b/www/views/includes/containers/settings/settings.inc.php @@ -42,7 +42,7 @@ Send a test email'; + echo ''; } ?>
    SESSION TIMEOUT
    @@ -311,11 +311,11 @@

    You can edit environments names and colors. The default environment is the first one in the list, it will be pre-selected in tasks forms.

    Renaming an environment here will not rename it in repositories, this to prevent breaking existing configurations.

    -
    -
    +
    +
    At least 1 environment must be configured

    '; + echo '

    At least one environment should be configured

    '; } if (!empty(ENVS)) : @@ -324,9 +324,21 @@ $color = '#ffffff'; } else { $color = $env['Color']; + } + + if ($env['Protected'] == 'true') { + $protected = 'true'; + $protectionIcon = 'locked-red'; + $protectedClass = 'icon'; + $title = 'Unprotect environment'; + } else { + $protected = 'false'; + $protectionIcon = 'unlocked'; + $protectedClass = 'icon-lowopacity'; + $title = 'Protect environment'; } ?> -
    +
    @@ -334,36 +346,34 @@
    + + -
    - - - -
    +
    +
    -
    ADD NEW ENVIRONMENT
    +
    ADD A NEW ENVIRONMENT

    Choose a color and a name for the new environment.

    -
    +
    - - -
    - -
    +
    + +
    +

    diff --git a/www/views/includes/containers/settings/status.inc.php b/www/views/includes/containers/settings/status.inc.php deleted file mode 100644 index 6baccfddb..000000000 --- a/www/views/includes/containers/settings/status.inc.php +++ /dev/null @@ -1,6 +0,0 @@ -
    -
    -

    STATUS & MONITORING

    - -
    -
    diff --git a/www/views/includes/containers/snapshot/kpi.inc.php b/www/views/includes/containers/snapshot/kpi.inc.php new file mode 100644 index 000000000..d1cb7fa9d --- /dev/null +++ b/www/views/includes/containers/snapshot/kpi.inc.php @@ -0,0 +1,79 @@ +
    +

    SNAPSHOT

    + + getPackageType() == 'rpm') { + $repository = $repoController->getName() . ' ❯ ' . $repoController->getReleasever(); + $accent = 'blue'; + } + if ($repoController->getPackageType() == 'deb') { + $repository = $repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection(); + $accent = 'red'; + } + + $class = 'grid-rfr-1-4'; + + if (in_array($rebuild, ['needed', 'running'])) { + $class = 'grid-rfr-1-5'; + + if ($rebuild == 'needed') { + $title = 'Repository snapshot content has been modified, metadata rebuild is required'; + $icon = 'warning'; + $text = 'Rebuild required'; + $kpiAccent = 'yellow'; + } + + if ($rebuild == 'running') { + $title = 'A task is running on this repository snapshot'; + $icon = 'loading'; + $text = 'Rebuild running'; + $kpiAccent = 'purple'; + } + } ?> + +
    +
    + +
    + +

    Repository

    +
    +
    + +
    + +
    +

    getDateFormatted() ?>

    +

    Date

    +
    +
    + +
    + +
    +

    +

    Size

    +
    +
    + +
    + +
    +

    +

    Packages

    +
    +
    + + +
    + +
    +

    +

    Metadata

    +
    +
    + +
    +
    diff --git a/www/views/includes/containers/snapshot/list.inc.php b/www/views/includes/containers/snapshot/list.inc.php new file mode 100644 index 000000000..d3585ec76 --- /dev/null +++ b/www/views/includes/containers/snapshot/list.inc.php @@ -0,0 +1,50 @@ +
    +
    + +

    Generating tree structure...

    +
    + +
    +
    + +
    + + Upload packages +
    + + +
    + + Rebuild metadata +
    + +
    + + This snapshot has a protected environment and cannot be modified. +
    + +
    + + +
    + +
    + +
    +
    +
    +
    + + +
    \ No newline at end of file diff --git a/www/views/includes/containers/status/health.inc.php b/www/views/includes/containers/status/health.inc.php index e80b26ac3..0c5678606 100644 --- a/www/views/includes/containers/status/health.inc.php +++ b/www/views/includes/containers/status/health.inc.php @@ -1,40 +1,35 @@

    DATABASES HEALTH

    -
    -
    - $properties) : - $icon = 'check'; ?> - -
    -
    -
    - -
    +
    + $properties) : + $icon = 'check'; - + if (!empty($properties['errors'])) { + $icon = 'warning'; + } ?> +
    + +
    +

    - -
    +

    Healthy

    '; + echo 'Healthy'; } else { foreach ($properties['errors'] as $error) { - echo '

    ' . $error . '

    '; + echo $error; } } ?> -
    +

    ()

    - -
    +
    +
    \ No newline at end of file diff --git a/www/views/includes/containers/tasks/log.inc.php b/www/views/includes/containers/tasks/log.inc.php index 77e1301a5..9e3153dd9 100644 --- a/www/views/includes/containers/tasks/log.inc.php +++ b/www/views/includes/containers/tasks/log.inc.php @@ -1,39 +1,9 @@ -
    -

    LOG

    - -
    -
    > - -
    - -
    -
    - -
    - -
    - -
    - -
    - -
    - -
    - -
    -
    -
    +
    +
    \ No newline at end of file diff --git a/www/views/includes/containers/tasks/log/log-found.inc.php b/www/views/includes/containers/tasks/log/log-found.inc.php new file mode 100644 index 000000000..33ffc11dd --- /dev/null +++ b/www/views/includes/containers/tasks/log/log-found.inc.php @@ -0,0 +1,164 @@ + + +
    +
    +

    TASK #

    + + +

    View parent task

    + +
    + + +

    Stop

    + +
    + +
    > +
    + 0) { + $statusDetails[] = $subTasksSummary['failed'] . ' failed'; + } + if ($subTasksSummary['ongoing'] > 0) { + $statusDetails[] = $subTasksSummary['ongoing'] . ' in progress'; + } + + $statusLabel = !empty($statusDetails) ? 'Sub-tasks status (' . implode(', ', $statusDetails) . ')' : 'Sub-tasks status'; + } ?> + +
    + + +
    +
    +

    + '; + } ?> +
    +

    +
    +
    + +
    + +
    +

    +

    Action

    +
    +
    + + +
    + +
    +

    format('d-m-Y') ?>

    +

    +
    +
    + +
    + +
    +

    +

    Total duration

    +
    +
    + +
    + + +
    + +
    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    \ No newline at end of file diff --git a/www/views/includes/containers/tasks/log/log-not-found.inc.php b/www/views/includes/containers/tasks/log/log-not-found.inc.php new file mode 100644 index 000000000..11a8e9590 --- /dev/null +++ b/www/views/includes/containers/tasks/log/log-not-found.inc.php @@ -0,0 +1,15 @@ +
    +

    +
    + +
    +
    + +
    +

    Error

    +

    +
    +
    +
    + +

    Go back to the tasks list

    \ No newline at end of file diff --git a/www/views/includes/containers/tasks/log/step.inc.php b/www/views/includes/containers/tasks/log/step.inc.php index e5f9bb56a..27a2fe630 100644 --- a/www/views/includes/containers/tasks/log/step.inc.php +++ b/www/views/includes/containers/tasks/log/step.inc.php @@ -1,6 +1,5 @@
    -
    +

    diff --git a/www/views/includes/containers/tasks/tasks.inc.php b/www/views/includes/containers/tasks/tasks.inc.php new file mode 100644 index 000000000..6b218ad02 --- /dev/null +++ b/www/views/includes/containers/tasks/tasks.inc.php @@ -0,0 +1,59 @@ + + +
    +

    TASKS

    + +
    +
    + +
    +

    +

    Total tasks

    +
    +
    + +
    + +
    +

    +

    Running tasks

    +
    +
    + +
    + +
    +

    +

    Scheduled tasks

    +
    +
    +
    + + +
    +

    No task yet!

    +

    Start by creating a new repository or scheduling a task.

    +
    + 0) : ?> +
    + +
    + +
    diff --git a/www/views/includes/footer.inc.php b/www/views/includes/footer.inc.php index 2bfc0a110..1dfed0bf8 100644 --- a/www/views/includes/footer.inc.php +++ b/www/views/includes/footer.inc.php @@ -1,5 +1,5 @@ @@ -51,6 +49,8 @@ + + @@ -69,6 +69,8 @@ const mytable = new Table(); const mypanel = new Panel(); const mycookie = new Cookie(); + const myrepopermission = new RepoPermission(); + const mytaskpermission = new TaskPermission(); const myalert = new Alert(); const myconfirmbox = new ConfirmBox(); const mycheckbox = new Checkbox(); @@ -82,9 +84,7 @@ REPOSITORY getPackageType() == 'rpm') { echo '' . $repoController->getName() . ' ❯ ' . $repoController->getReleasever() . ''; diff --git a/www/views/includes/forms/tasks/delete.inc.php b/www/views/includes/forms/tasks/delete.inc.php index 34620fa8f..2acc6efa9 100644 --- a/www/views/includes/forms/tasks/delete.inc.php +++ b/www/views/includes/forms/tasks/delete.inc.php @@ -1,5 +1,3 @@ -
    DELETE
    -

    The repository snapshot to delete.

    getPackageType() == 'rpm') { echo '' . $repoController->getName() . ' ❯ ' . $repoController->getReleasever() . '' . $repoController->getDateFormatted() . ''; diff --git a/www/views/includes/forms/tasks/duplicate.inc.php b/www/views/includes/forms/tasks/duplicate.inc.php index 01da6af14..40f6967d6 100644 --- a/www/views/includes/forms/tasks/duplicate.inc.php +++ b/www/views/includes/forms/tasks/duplicate.inc.php @@ -1,5 +1,5 @@ -
    DUPLICATE
    -

    The repository snapshot to be duplicated.

    +

    @@ -21,7 +21,7 @@

    POINT AN ENVIRONMENT

    Select one or multiple environments to point to the new repository snapshot.

    - -
    -
    DESCRIPTION
    - -
    +
    DESCRIPTION
    +

    Optional. A description for this repository.

    + + +
    TAGS
    +

    Optional. Add tags to the repository. Tags can be used to filter repositories.

    + -
    DESCRIPTION
    - - diff --git a/www/views/includes/forms/tasks/rebuild.inc.php b/www/views/includes/forms/tasks/rebuild.inc.php index 547e2983c..22cebde8c 100644 --- a/www/views/includes/forms/tasks/rebuild.inc.php +++ b/www/views/includes/forms/tasks/rebuild.inc.php @@ -1,6 +1,3 @@ -
    REBUILD METADATA
    -

    The repository snapshot to rebuild metadata for.

    -

    RENAME -

    The repository to be renamed.

    -

    The new name of the repository.

    - - - - +use Controllers\Utils\Random; -
    +$showScheduleIntro = $scheduleForm['show-intro'] ?? true; +$showScheduleToggle = $scheduleForm['show-toggle'] ?? true; ?> + +
    TASK SCHEDULING
    -

    Don't want to execute the task immediately? Schedule it!

    + +

    Don't want to execute the task immediately? Schedule it!

    + SCHEDULE IT - + if ($showScheduleToggle) : ?> +
    SCHEDULE IT
    + + + + -
    +
    SCHEDULE TYPE
    UPDATE -

    Task will create a new snapshot of the repository.

    -

    "> + @@ -28,19 +29,18 @@ "run.css", - "browse" => "browse.css", - "stat" => "stats-hosts.css", - "stats" => "stats-hosts.css", - "hosts" => "stats-hosts.css", - "host" => "stats-hosts.css", - "settings" => "settings.css", - "cves" => "cve.css", - "cve" => "cve.css" + 'tasks' => 'tasks', + 'task' => 'tasks.css', + 'snapshot' => 'snapshot.css', + 'stat' => 'stats-hosts.css', + 'stats' => 'stats-hosts.css', + 'hosts' => 'stats-hosts.css', + 'host' => 'stats-hosts.css', + 'settings' => 'settings.css', + 'cves' => 'cve.css', + 'cve' => 'cve.css' ]; foreach ($additionalCss as $uri => $css) { @@ -70,23 +70,21 @@ diff --git a/www/views/includes/host/package/event-packages-details.inc.php b/www/views/includes/host/package/event-packages-details.inc.php index 85f3cea90..cbfdfdb47 100644 --- a/www/views/includes/host/package/event-packages-details.inc.php +++ b/www/views/includes/host/package/event-packages-details.inc.php @@ -30,7 +30,7 @@

    -

    +

    VERSION
    -

    +

    diff --git a/www/views/includes/panels/general/notification.inc.php b/www/views/includes/panels/general/notification.inc.php index 9b8706b65..fb2ac04a8 100644 --- a/www/views/includes/panels/general/notification.inc.php +++ b/www/views/includes/panels/general/notification.inc.php @@ -1,17 +1,19 @@ Nothing for now!

    '; +if (NOTIFICATION == 0) : ?> +
    +

    Nothing for now!

    +

    You have no notifications at the moment. Repomanager will notify you when an update is available or if there are any important messages to read.

    +
    + -
    +
    -
    +
    @@ -19,6 +21,7 @@

    Major release

    + @@ -26,6 +29,8 @@

    Changelog

    + +

    Update instructions

    @@ -56,26 +61,21 @@ -
    +
    -
    +

    -
    - -

    Mark as read

    -
    +
    diff --git a/www/views/includes/panels/general/userspace.inc.php b/www/views/includes/panels/general/userspace.inc.php index 8ce46696e..41699ec7f 100644 --- a/www/views/includes/panels/general/userspace.inc.php +++ b/www/views/includes/panels/general/userspace.inc.php @@ -10,15 +10,10 @@

    The API key is used to authenticate for most of the API endpoints. Keep it secret.

    -
    - +
    +
    -
    LOGOUT FROM REPOMANAGER
    - - - -
    PERSONAL INFORMATIONS
    @@ -60,8 +55,15 @@
    + +
    LOGOUT
    + + + + + -
    TASKS
    +
    TASKS
    - +
    HOSTS
    diff --git a/www/views/includes/panels/hosts/groups/list.inc.php b/www/views/includes/panels/hosts/groups/list.inc.php index 3ce61eff8..d890cdfee 100644 --- a/www/views/includes/panels/hosts/groups/list.inc.php +++ b/www/views/includes/panels/hosts/groups/list.inc.php @@ -2,34 +2,28 @@
    CREATE A NEW GROUP
    -
    - - + + +
    -
    - -
    CURRENT GROUPS
    +
    CURRENT GROUPS
    getHostsMembers($group['Id']); $hostsNotIn = $mygroup->getHostsNotMembers(); $hostsInCount = count($hostsIn); ?> -
    +

    host 1 ? 's' : '' ?>

    @@ -48,18 +42,14 @@
    HOSTS
    - + + CURRENT PROFILES'; + echo '
    CURRENT PROFILES
    '; foreach ($profiles as $profile) : $packageExclude = explode(',', $profile['Package_exclude']); diff --git a/www/views/includes/panels/hosts/settings.inc.php b/www/views/includes/panels/hosts/settings.inc.php index c90dcd990..8e996fa69 100644 --- a/www/views/includes/panels/hosts/settings.inc.php +++ b/www/views/includes/panels/hosts/settings.inc.php @@ -16,6 +16,14 @@
    +
    +

    Host is not compliant when a security update is available:

    + +
    +

    Host is not compliant when a reboot is required:

    diff --git a/www/views/includes/tables/host/history.inc.php b/www/views/includes/tables/host/history.inc.php index 347c14dbc..f36dc6a03 100644 --- a/www/views/includes/tables/host/history.inc.php +++ b/www/views/includes/tables/host/history.inc.php @@ -1,77 +1,80 @@
    - -
    -

    Nothing for now!

    -

    The host did not send any history yet.

    -
    +
    +
    +

    Nothing for now!

    +

    The host did not send any history yet.

    +
    + $packageState) : ?> -
    -
    -

    format('d-m-Y') ?>

    -
    + if (!empty($reloadableTableContent)) : + foreach ($reloadableTableContent as $date => $packageState) : ?> +
    +
    + +

    format('d-m-Y') ?>

    +
    -
    - $packages) : - if (empty($packages)) { - continue; - } +
    + $packages) : + if (empty($packages)) { + continue; + } - if ($state == 'installed') { - $title = 'INSTALLED'; - $icon = 'package-installed.svg'; - } + if ($state == 'installed') { + $title = 'INSTALLED'; + $icon = 'package-installed.svg'; + } - if ($state == 'reinstalled') { - $title = 'REINSTALLED'; - $icon = 'package-installed.svg'; - } + if ($state == 'reinstalled') { + $title = 'REINSTALLED'; + $icon = 'package-installed.svg'; + } - if ($state == 'dep-installed') { - $title = 'DEP. INSTALLED'; - $icon = 'package-installed.svg'; - } + if ($state == 'dep-installed') { + $title = 'DEP. INSTALLED'; + $icon = 'package-installed.svg'; + } - if ($state == 'upgraded') { - $title = 'UPDATED'; - $icon = 'package-updated.svg'; - } + if ($state == 'upgraded') { + $title = 'UPDATED'; + $icon = 'package-updated.svg'; + } - if ($state == 'downgraded') { - $title = 'DOWNGRADED'; - $icon = 'package-updated.svg'; - } + if ($state == 'downgraded') { + $title = 'DOWNGRADED'; + $icon = 'package-updated.svg'; + } - if ($state == 'removed') { - $title = 'UNINSTALLED'; - $icon = 'package-removed.svg'; - } + if ($state == 'removed') { + $title = 'UNINSTALLED'; + $icon = 'package-removed.svg'; + } - if ($state == 'purged') { - $title = 'PURGED'; - $icon = 'package-removed.svg'; - } + if ($state == 'purged') { + $title = 'PURGED'; + $icon = 'package-removed.svg'; + } - $count = count($packages); + $count = count($packages); - include(ROOT . '/views/includes/labels/label-icon-tr.inc.php'); - endforeach ?> + include(ROOT . '/views/includes/labels/label-icon-tr.inc.php'); + endforeach ?> +
    -
    - + unset($date, $packageState, $state, $packages, $title, $icon, $count); ?> -
    - -
    - +
    + +
    + +
    diff --git a/www/views/includes/tables/host/installed-packages.inc.php b/www/views/includes/tables/host/installed-packages.inc.php new file mode 100644 index 000000000..42617a919 --- /dev/null +++ b/www/views/includes/tables/host/installed-packages.inc.php @@ -0,0 +1,40 @@ +
    +

    + package 1 ? 's' : '' ?> inventored +

    + + +

    + +
    + +
    +
    + + +
    +

    + ' . $item['Name'] . ' (uninstalled)'; + } else { + echo $item['Name']; + } ?> +

    +

    +
    +
    +
    + +
    + +
    + +
    + +
    diff --git a/www/views/includes/tables/host/requests.inc.php b/www/views/includes/tables/host/requests.inc.php index c81eb882c..ffa569a5a 100644 --- a/www/views/includes/tables/host/requests.inc.php +++ b/www/views/includes/tables/host/requests.inc.php @@ -10,418 +10,429 @@ +
    + = 1 and $failedCount >= 1) { - $requestStatus = 'Partial success'; - $requestStatusIcon = 'warning.svg'; - } + // Retrieve full-upgrade value + if (!empty($requestData['update-params']['full-upgrade'])) { + $fullUpgrade = \Controllers\Utils\Convert::toBool($requestData['update-params']['full-upgrade']); + } - // If there was no packages updated AND packages failed - if ($successCount == 0 and $failedCount >= 1) { - $requestStatus = 'Failed'; - $requestStatusIcon = 'error.svg'; - } + // Retrieve keep-config-files value + if (!empty($requestData['update-params']['keep-config-files'])) { + $keepConfigFiles = \Controllers\Utils\Convert::toBool($requestData['update-params']['keep-config-files']); + } + } - // Build a short info message - $responseDetails .= $successCount . ' package(s) updated, ' . $failedCount . ' failed'; + /** + * Request info + */ + $requestInfo = $item['Info']; - // Retrieve the list of packages updated - $successPackages = $responseJson['update']['success']['packages']; + /** + * Response data + */ + if (!empty($item['Response_json'])) { + $responseJson = json_decode($item['Response_json'], true); + } - // Retrieve the list of packages failed - $failedPackages = $responseJson['update']['failed']['packages']; - } + /** + * Request status + */ + if ($item['Status'] == 'new') { + $requestStatus = 'Pending'; + $requestStatusIcon = 'pending.svg'; + } + if ($item['Status'] == 'sent') { + $requestStatus = 'Sent'; + $requestStatusIcon = 'pending.svg'; + } + if ($item['Status'] == 'running') { + $requestStatus = 'Running'; + $requestStatusIcon = 'loading.svg'; + } + if ($item['Status'] == 'canceled') { + $requestStatus = 'Canceled'; + $requestStatusIcon = 'warning-red.svg'; + } + if ($item['Status'] == 'failed') { + $requestStatus = 'Failed'; + $requestStatusIcon = 'error.svg'; + } + if ($item['Status'] == 'completed') { + $requestStatus = 'Completed'; + $requestStatusIcon = 'check.svg'; } - } - if ($request == 'request-all-packages-update') { - $requestString = 'Requested to update all packages'; - $requestTitle = 'Requested the host to update all of its packages'; + /** + * Request title + */ + if ($request == 'request-general-infos') { + $requestString = 'Requested general informations'; + $requestTitle = 'Requested the host to send its general informations'; + } - if (!empty($responseJson)) { - /** - * If there was no packages to update - */ - if ($responseJson['update']['status'] == 'nothing-to-do') { - $responseDetails = 'No packages to update'; - } + if ($request == 'request-packages-infos') { + $requestString = 'Requested packages informations'; + $requestTitle = 'Requested the host to send its packages informations'; + } - /** - * If there was packages to update, retrieve the number of packages updated - */ - if ($responseJson['update']['status'] == 'done' or $responseJson['update']['status'] == 'failed') { - $successCount = $responseJson['update']['success']['count']; - $failedCount = $responseJson['update']['failed']['count']; - - // If the update was successful - if ($responseJson['update']['status'] == 'done') { - $requestStatus = 'Successful'; - $requestStatusIcon = 'check.svg'; - } + if ($request == 'request-packages-update') { + $requestString = 'Request to update a list of package(s)'; + $requestTitle = 'Requested the host to install a list of package(s)'; - // If the update failed - if ($responseJson['update']['status'] == 'failed') { - $requestStatus = 'Failed with errors'; - $requestStatusIcon = 'error.svg'; + if (!empty($responseJson)) { + if (!empty($requestJson['packages'])) { + $requestDetails = count($requestJson['packages']) . ' package(s) to install'; } - // If there was packages updated AND packages failed - if ($successCount >= 1 and $failedCount >= 1) { - $requestStatus = 'Partial success'; - $requestStatusIcon = 'warning.svg'; + /** + * If there was no packages to update + */ + if ($responseJson['update']['status'] == 'nothing-to-do') { + $responseDetails = 'No packages to update'; } - // If there was no packages updated AND packages failed - if ($successCount == 0 and $failedCount >= 1) { - $requestStatus = 'Failed'; - $requestStatusIcon = 'error.svg'; + /** + * If there was packages to update, retrieve the number of packages updated + */ + if ($responseJson['update']['status'] == 'done' or $responseJson['update']['status'] == 'failed') { + $successCount = $responseJson['update']['success']['count']; + $failedCount = $responseJson['update']['failed']['count']; + + // If the update was successful + if ($responseJson['update']['status'] == 'done') { + $requestStatus = 'Successful'; + $requestStatusIcon = 'check.svg'; + } + + // If the update failed + if ($responseJson['update']['status'] == 'failed') { + $requestStatus = 'Failed with errors'; + $requestStatusIcon = 'error.svg'; + } + + // If there was packages updated AND packages failed + if ($successCount >= 1 and $failedCount >= 1) { + $requestStatus = 'Partial success'; + $requestStatusIcon = 'warning.svg'; + } + + // If there was no packages updated AND packages failed + if ($successCount == 0 and $failedCount >= 1) { + $requestStatus = 'Failed'; + $requestStatusIcon = 'error.svg'; + } + + // Build a short info message + $responseDetails .= $successCount . ' package(s) updated, ' . $failedCount . ' failed'; + + // Retrieve the list of packages updated + $successPackages = $responseJson['update']['success']['packages']; + + // Retrieve the list of packages failed + $failedPackages = $responseJson['update']['failed']['packages']; } - - // Build a short info message - $responseDetails .= $successCount . ' package(s) updated, ' . $failedCount . ' failed'; - - // Retrieve the list of packages updated - $successPackages = $responseJson['update']['success']['packages']; - - // Retrieve the list of packages failed - $failedPackages = $responseJson['update']['failed']['packages']; } } - } - if ($request == 'request-packages-update' or $request == 'request-all-packages-update') { - $class .= ' request-show-more-info-btn pointer'; - } + if ($request == 'request-all-packages-update') { + $requestString = 'Requested to update all packages'; + $requestTitle = 'Requested the host to update all of its packages'; - // If the request was a profile update or a disconnect, skip it - if ($request == 'update-profile' or $request == 'disconnect') { - continue; - } ?> - -
    -
    - '; - } else { - echo ' '; + if (!empty($responseJson)) { + /** + * If there was no packages to update + */ + if ($responseJson['update']['status'] == 'nothing-to-do') { + $responseDetails = 'No packages to update'; } - } ?> -
    - -
    -

    format('d-m-Y') ?>

    -

    -

    -
    -

    - -

    - = 1 and $failedCount >= 1) { + $requestStatus = 'Partial success'; + $requestStatusIcon = 'warning.svg'; + } + + // If there was no packages updated AND packages failed + if ($successCount == 0 and $failedCount >= 1) { + $requestStatus = 'Failed'; + $requestStatusIcon = 'error.svg'; + } + + // Build a short info message + $responseDetails .= $successCount . ' package(s) updated, ' . $failedCount . ' failed'; + + // Retrieve the list of packages updated + $successPackages = $responseJson['update']['success']['packages']; + + // Retrieve the list of packages failed + $failedPackages = $responseJson['update']['failed']['packages']; } + } + } - if (!empty($responseDetails)) { - echo ' - ' . $responseDetails; - } ?> -

    -
    + /** + * Add accent color based on status + */ + if ($item['Status'] == 'completed' or $requestStatus == 'Successful') { + $class .= ' request-accent-green'; + } elseif ($item['Status'] == 'failed' or $item['Status'] == 'canceled' or $requestStatus == 'Failed' or $requestStatus == 'Failed with errors') { + $class .= ' request-accent-red'; + } elseif ($requestStatus == 'Partial success') { + $class .= ' request-accent-yellow'; + } -
    - '; - } + if ($request == 'request-packages-update' or $request == 'request-all-packages-update') { + $class .= ' request-show-more-info-btn pointer'; + } - if (file_exists(WS_REQUESTS_LOGS_DIR . '/request-' . $item['Id'] . '.log')) : ?> - - - if ($item['Status'] == 'new') : ?> - +
    +
    -
    -
    + if (!empty($requestStatusIcon)) { + if (str_ends_with($requestStatusIcon, '.svg')) { + echo ''; + } else { + echo ' '; + } + } ?> -
    - -
    -
    DRY RUN
    - Enabled

    '; - } else { - echo '

    Disabled

    '; - } - } ?> +

    format('d-m-Y') ?>

    +

    +
    -
    -
    IGNORE EXCLUSIONS
    +
    +

    Enabled

    '; - } else { - echo '

    Disabled

    '; - } + echo $requestStatus; + + if (!empty($requestInfo)) { + echo ' - ' . $requestInfo; + } + + if (!empty($responseDetails)) { + echo ' - ' . $responseDetails; } ?> -
    +

    +
    -
    -
    FULL UPGRADE
    +
    + '; + } + + if (file_exists(WS_REQUESTS_LOGS_DIR . '/request-' . $item['Id'] . '.log')) : ?> + Enabled

    '; - } else { - echo '

    Disabled

    '; - } - } ?> -
    + endif; -
    -
    KEEP CONFIG FILES
    + if ($item['Status'] == 'new') : ?> + Enabled

    '; - } else { - echo '

    Disabled

    '; - } - } ?> -
    + endif ?>
    -
    - - /** - * If there was packages updated - */ - if (!empty($successPackages)) : - $icon = 'update-yellow.svg'; - $title = count($successPackages) . ' package' . (count($successPackages) > 1 ? 's' : '') . ' updated'; - include(ROOT . '/views/includes/labels/label-icon-tr.inc.php'); ?> +
    + +
    +
    +
    DRY RUN
    + Enabled

    '; + } else { + echo '

    Disabled

    '; + } + } ?> +
    -
    - $details) : ?> -
    - -

    +
    +
    IGNORE EXCLUSIONS
    + Enabled

    '; + } else { + echo '

    Disabled

    '; + } + } ?>
    -

    +
    +
    FULL UPGRADE
    + Enabled

    '; + } else { + echo '

    Disabled

    '; + } + } ?> +
    -

    +

    +
    KEEP CONFIG FILES
    - - -

    + if (isset($keepConfigFiles)) { + if ($keepConfigFiles) { + echo '

    Enabled

    '; + } else { + echo '

    Disabled

    '; + } + } ?> +
    +
    +
    + 1 ? 's' : '') . ' updated'; + include(ROOT . '/views/includes/labels/label-icon-tr.inc.php'); ?> + +
    -
    - $details) : ?> +
    + +

    +
    - /** - * If there was packages failed - */ - if (!empty($failedPackages)) : - $icon = 'warning-red.svg'; - $title = count($failedPackages) . ' package' . (count($failedPackages) > 1 ? 's' : '') . ' failed'; - include(ROOT . '/views/includes/labels/label-icon-tr.inc.php'); ?> +

    -
    +

    + + + +

    + +
    $details) : ?> -
    - -

    -
    + endif; -

    + /** + * If there was packages failed + */ + if (!empty($failedPackages)) : + $icon = 'warning-red.svg'; + $title = count($failedPackages) . ' package' . (count($failedPackages) > 1 ? 's' : '') . ' failed'; + include(ROOT . '/views/includes/labels/label-icon-tr.inc.php'); ?> -

    - - - -

    +
    -
    - -
    - + // Print each package with its version and log + foreach ($failedPackages as $package => $details) : ?> +
    + +

    +
    + +

    + +

    + + + +

    + +
    + +
    + +
    diff --git a/www/views/includes/tables/repos/sources/gpgkeys.inc.php b/www/views/includes/tables/repos/sources/gpgkeys.inc.php index 96b84698d..a7653ebc3 100644 --- a/www/views/includes/tables/repos/sources/gpgkeys.inc.php +++ b/www/views/includes/tables/repos/sources/gpgkeys.inc.php @@ -12,7 +12,7 @@ -
    +

    diff --git a/www/views/includes/tables/repos/sources/list.inc.php b/www/views/includes/tables/repos/sources/list.inc.php index d27f7f505..bf4dd1fa6 100644 --- a/www/views/includes/tables/repos/sources/list.inc.php +++ b/www/views/includes/tables/repos/sources/list.inc.php @@ -18,7 +18,7 @@

    No source repository configured yet.

    Import a predefined list to get started quickly, or add a source repository manually.

    - +
    @@ -48,6 +48,8 @@ // Case it is a deb source repository if ($type == 'deb') { + $accent = 'red'; + if (isset($definition['distributions'])) { $distAndComponent = $definition['distributions']; } @@ -55,6 +57,8 @@ // Case it is a rpm source repository if ($type == 'rpm') { + $accent = 'blue'; + if (isset($definition['releasever'])) { $releasevers = $definition['releasever']; } @@ -71,17 +75,7 @@ $sslCaCertificate = $definition['ssl-authentication']['ca-certificate']; } ?> -
    -
    - rpm'; - } - if ($type == 'deb') { - echo ' deb'; - } ?> -
    - +

    +
    -
    -
    +
    +
    -
    - + // Print Select all checkbox for scheduled and queued tasks if there are more than 1 task + if (in_array($taskTableType, ['scheduled', 'queued']) and (count($reloadableTableContent) > 1)) : ?> +
    + Select all +
    - - -
    -
    -
    - + getSubTasksSummaries(array_column($reloadableTableContent, 'Id')); + + foreach ($reloadableTableContent as $item) : + $taskAccent = ''; + + // Retrieve task parameters + try { + $taskRawParams = json_decode($item['Raw_params'], true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + echo '

    Error decoding task #' . $item['Id'] . ' parameters: ' . $e->getMessage() . '

    '; + continue; + } + + $subTasksSummary = $subTasksSummaries[$item['Id']] ?? []; + $itemStatus = !empty($subTasksSummary) ? $subTasksSummary['status'] : $item['Status']; + + // Determine status accent color + if ($itemStatus == 'done') { + $taskAccent = 'accent-green'; + } elseif ($itemStatus == 'error' or $itemStatus == 'stopped') { + $taskAccent = 'accent-red'; + } elseif ($itemStatus == 'running') { + $taskAccent = 'accent-cyan'; + } elseif ($itemStatus == 'queued') { + $taskAccent = 'accent-yellow'; + } elseif ($itemStatus == 'scheduled') { + $taskAccent = 'accent-orange'; + } + + // Determine action title and icon + $icon = 'plus'; + $actionTitle = Task::generateLiteralAction($taskRawParams['action']); + + if ($taskRawParams['action'] == 'create') { + $icon = 'plus'; + if (!isset($taskRawParams['repo-type'])) { + $actionTitle = 'New repository'; + } else { + $actionTitle = $taskRawParams['repo-type'] == 'local' ? 'New local repository' : 'New mirror repository'; + } + } + if ($taskRawParams['action'] == 'update') { + $icon = 'update'; + } + if ($taskRawParams['action'] == 'rebuild') { + $icon = 'update'; + } + if ($taskRawParams['action'] == 'rename') { + $icon = 'edit'; + } + if ($taskRawParams['action'] == 'env') { + $icon = 'link'; + } + if ($taskRawParams['action'] == 'duplicate') { + $icon = 'duplicate'; + } + if ($taskRawParams['action'] == 'delete') { + $icon = 'delete'; + } + if ($taskRawParams['action'] == 'removeEnv') { + $icon = 'delete'; + } + if ($itemStatus == 'running') { + $icon = 'loading'; + } + + // Determine click behavior class + $actionBtn = in_array($item['Status'], ['scheduled', 'queued', 'disabled']) ? 'task-item-selectable' : ''; ?> + +
    + > +
    + + + - if ($taskRawParams['action'] == 'update') { - $icon = 'update'; - $actionTitle = 'Update repository'; - } + - if ($taskRawParams['action'] == 'rebuild') { - $icon = 'update'; - $actionTitle = 'Rebuild metadata'; - } +
    + +

    format('d-m-Y') ?>

    + +
    + Task # + Pending +
    + +
    + + format('d-m-Y') . ' ' . $taskRawParams['schedule']['schedule-time'] . ':00'; + } + if ($taskRawParams['schedule']['schedule-type'] == 'recurring') { + $scheduleTime = ' at ' . htmlspecialchars($taskRawParams['schedule']['schedule-time'] ?? '', ENT_QUOTES, 'UTF-8') . ':00'; + + if ($taskRawParams['schedule']['schedule-frequency'] == 'hourly') { + echo 'Hourly'; + } + if ($taskRawParams['schedule']['schedule-frequency'] == 'daily') { + echo 'Daily' . $scheduleTime; + } + if ($taskRawParams['schedule']['schedule-frequency'] == 'weekly') { + echo 'Weekly' . $scheduleTime; + } + if ($taskRawParams['schedule']['schedule-frequency'] == 'monthly') { + echo 'Monthly' . $scheduleTime; + } + if ($taskRawParams['schedule']['schedule-frequency'] == 'cron') { + echo 'Cron: ' . htmlspecialchars($taskRawParams['schedule']['schedule-cron'] ?? '', ENT_QUOTES, 'UTF-8'); + } + } ?> + + + Past schedule'; + } + } - if ($taskRawParams['action'] == 'env') { - $icon = 'link'; - $actionTitle = 'Point an environment'; - } + // If the task is disabled, print a label + if ($item['Status'] == 'disabled') : ?> + Disabled + '; + } ?> +
    + - if ($taskRawParams['action'] == 'duplicate') { - $icon = 'duplicate'; - $actionTitle = 'Duplicate repository'; - } + +
    +
    +
    - if ($taskRawParams['action'] == 'delete') { - $icon = 'delete'; - $actionTitle = 'Delete repository'; - } +
    + ' . $subTasksSummary['success'] . '/' . $subTasksSummary['total'] . ''; } ?> - -
    + getRepo($item['Id']); ?> -
    ' . DateTime::createFromFormat('Y-m-d', $item['Date'])->format('d-m-Y') . ' ' . $item['Time'] . ''; - endif; - - /** - * If task is scheduled - */ - if ($item['Type'] == 'scheduled') : - if (!empty($item['Date']) and !empty($item['Time'])) { - $class = 'margin-top-5'; - } else { - $class = ''; - } ?> + if (in_array($taskRawParams['action'], ['env', 'removeEnv'])) { + if (is_string($taskRawParams['env'])) { + echo Label::envtag($taskRawParams['env']); + } - - '; - echo DateTime::createFromFormat('Y-m-d', $taskRawParams['schedule']['schedule-date'])->format('d-m-Y') . ' ' . $taskRawParams['schedule']['schedule-time'] . ':00'; + if (is_array($taskRawParams['env'])) { + foreach ($taskRawParams['env'] as $env) { + echo Label::envtag($env); } - - /** - * Case it is a recurring scheduled task - */ - if ($taskRawParams['schedule']['schedule-type'] == 'recurring') { - if ($taskRawParams['schedule']['schedule-frequency'] == 'hourly') { - echo 'Hourly scheduled task'; - } - - if ($taskRawParams['schedule']['schedule-frequency'] == 'daily') { - echo 'Daily scheduled task'; - } - - if ($taskRawParams['schedule']['schedule-frequency'] == 'weekly') { - echo 'Weekly scheduled task'; - } - - if ($taskRawParams['schedule']['schedule-frequency'] == 'monthly') { - echo 'Monthly scheduled task'; - } - - if ($taskRawParams['schedule']['schedule-frequency'] == 'cron') { - echo 'Cron scheduled task'; - } - } ?> - - - - - - + } + } ?>
    -
    - -
    - - getRepo($item['Id']); ?> - - + '; + // } + + if (($item['Status'] == 'error' or $item['Status'] == 'stopped') and !empty($item['Id'])) { + if (TaskPermission::allowedAction('relaunch')) { + echo ''; } } - } ?> -
    -
    - '; - } - - // Print relaunch button if task has failed - if (($item['Status'] == 'error' or $item['Status'] == 'stopped') and !empty($item['Id'])) { - if (IS_ADMIN or in_array('relaunch', USER_PERMISSIONS['tasks']['allowed-actions'])) { - echo ''; + if ($item['Status'] == 'queued') { + echo ''; } - } - if ($item['Status'] == 'queued') { - echo ''; - } - - if ($item['Status'] == 'running') { - if (IS_ADMIN or in_array('stop', USER_PERMISSIONS['tasks']['allowed-actions'])) { - echo ''; - } - echo ''; - } + if ($item['Status'] == 'running') { + if (TaskPermission::allowedAction('stop')) { + echo ''; + } + } ?> +
    +
    - if ($item['Status'] == 'done') { - echo ''; - } + +
    +
    +
    +
    SCHEDULE TYPE
    + +

    Single execution

    + +

    Recurring execution

    + +
    - if ($item['Status'] == 'error') { - echo ''; - } +
    + +
    SCHEDULE DATE
    +

    format('d-m-Y') . ' ' . $taskRawParams['schedule']['schedule-time'] . ':00' ?>

    + +
    SCHEDULE FREQUENCY
    +

    + +

    + +
    +
    - if ($item['Status'] == 'stopped') { - echo ''; - } + +
    DUPLICATE TO
    +

    + '; - } - } ?> -
    -
    + if ($taskRawParams['action'] == 'rename') : ?> +
    RENAME TO
    +

    + - -
    -
    -
    -
    SCHEDULE TYPE
    +
    -

    Single execution

    + if (!empty($taskRawParams['arch'])) : ?> +
    +
    ARCHITECTURE
    +
    + ' . Label::white($architecture) . '

    '; + } ?> +
    +
    -

    Recurring execution

    + if (!empty($taskRawParams['env'])) : ?> +
    +
    ENVIRONMENT
    +
    + ' . Label::envtag($env) . '

    '; + } ?> +
    +
    -
    +
    -
    SCHEDULE DATE
    -

    format('d-m-Y') . ' ' . $taskRawParams['schedule']['schedule-time'] . ':00' ?>

    + if (!empty($taskRawParams['gpg-check'])) : ?> +
    +
    CHECK GPG SIGNATURES
    +
    + '; + echo 'Enabled'; + } else { + echo ''; + echo 'Disabled'; + } ?> +
    +
    -
    SCHEDULE FREQUENCY
    -

    - - -

    + if (!empty($taskRawParams['gpg-sign'])) : ?> +
    +
    SIGN WITH GPG
    +
    + '; + echo 'Enabled'; + } else { + echo ''; + echo 'Disabled'; + } ?> +
    +
    -
    - - -
    DUPLICATE TO
    -

    - -
    RENAME TO
    -

    - - -
    - -
    -
    ARCHITECTURE
    -
    - ' . $architecture . '
    '; - } ?> -
    -
    - +
    -
    ENVIRONMENT
    -
    - ' . \Controllers\Utils\Generate\Html\Label::envtag($env) . '
    '; - } ?> -
    -
    - -
    - -
    - -
    -
    CHECK GPG SIGNATURES
    +
    NOTIFY ON TASK ERROR
    '; echo 'Enabled'; } else { @@ -346,15 +381,12 @@ } ?>
    -
    -
    SIGN WITH GPG
    +
    NOTIFY ON TASK SUCCESS
    '; echo 'Enabled'; } else { @@ -363,80 +395,49 @@ } ?>
    - -
    - -
    -
    -
    NOTIFY ON TASK ERROR
    -
    - '; - echo 'Enabled'; - } else { - echo ''; - echo 'Disabled'; - } ?> -
    -
    -
    NOTIFY ON TASK SUCCESS
    -
    - '; - echo 'Enabled'; - } else { - echo ''; - echo 'Disabled'; - } ?> +
    +
    +
    SEND A REMINDER
    +

    + '; + } else { + echo $reminder . ' days before
    '; + } + } + } ?> +

    -
    -
    -
    -
    -
    SEND A REMINDER
    -

    - '; - } else { - echo $reminder . ' days before
    '; +

    +
    CONTACT
    +

    + '; } - } - } ?> -

    -
    - -
    -
    CONTACT
    -

    - '; - } - } ?> -

    + } ?> +

    +
    -
    - + +
    - +


    diff --git a/www/views/layout.html.php b/www/views/layout.html.php index 6471302aa..823783083 100644 --- a/www/views/layout.html.php +++ b/www/views/layout.html.php @@ -3,28 +3,33 @@ -
    +
    + + - + +
    +
    + -
    - -
    + - + +
    -
    + +
    + + +
    +
    \ No newline at end of file diff --git a/www/views/templates/tasks/create.inc.php b/www/views/templates/tasks/create.inc.php index 4f65d6cb3..9148b94c9 100644 --- a/www/views/templates/tasks/create.inc.php +++ b/www/views/templates/tasks/create.inc.php @@ -1,51 +1,25 @@ -
    -
    -

    - getType() == 'mirror') { - echo strtoupper($repoController->getPackageType()) . ' MIRROR REPOSITORY'; - } - if ($repoController->getType() == 'local') { - echo 'LOCAL ' . strtoupper($repoController->getPackageType()) . ' REPOSITORY'; - } ?> -

    + -
    -

    format('d-m-Y') . ' ' . $taskInfo['Time'] ?>

    -
    -

    Task #

    - '; - } ?> -
    -
    -
    -
    - -
    +
    REPOSITORY
    -

    - - getPackageType() == 'deb') { - echo $repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection(); - } + getPackageType() == 'deb') { + echo Label::white($repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection()); + } - if ($repoController->getPackageType() == 'rpm') { - echo $repoController->getName(); - } ?> - -

    + if ($repoController->getPackageType() == 'rpm') { + echo Label::white($repoController->getName()); + } ?>
    getType() == 'mirror') : ?>
    SOURCE REPOSITORY
    -

    getSource() ?>

    +

    getSource()) ?>

    @@ -70,7 +44,7 @@
    getArch() as $arch) { - echo '' . $arch . ''; + echo Label::white($arch); } ?>
    @@ -84,7 +58,7 @@
    @@ -101,7 +75,7 @@
    getAdvancedParams()['packages']['include'] as $package) { - echo '' . $package . ''; + echo Label::white($package); } ?>
    @@ -114,7 +88,7 @@
    getAdvancedParams()['packages']['exclude'] as $package) { - echo '' . $package . ''; + echo Label::white($package); } ?>
    @@ -131,7 +105,7 @@
    ' . $package . ''; + echo Label::white($package); } ?>
    @@ -144,7 +118,7 @@
    ' . $package . ''; + echo Label::white($package); } ?>
    @@ -180,7 +154,7 @@ } $displayName = $metadataMap[$metadata] ?? $metadata; - echo '' . $displayName . ''; + echo Label::white($displayName); } ?>
    @@ -247,6 +221,22 @@ getTags())) : ?> +
    +
    TAGS
    +
    + getTags() as $tag) { + echo Label::white($tag); + } ?> +
    +
    + +
    + +
    + getGroup())) : ?>
    ADD TO GROUP
    diff --git a/www/views/templates/tasks/delete.inc.php b/www/views/templates/tasks/delete.inc.php index be76fa47b..e77e247d0 100644 --- a/www/views/templates/tasks/delete.inc.php +++ b/www/views/templates/tasks/delete.inc.php @@ -1,40 +1,22 @@ -
    -
    -

    DELETE REPOSITORY SNAPSHOT

    + -
    -

    format('d-m-Y') . ' ' . $taskInfo['Time'] ?>

    -
    -

    Task #

    - '; - } ?> -
    -
    -
    -
    - -
    +
    REPOSITORY
    -

    - - getPackageType() == 'deb') { - echo $repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection(); - } - if ($repoController->getPackageType() == 'rpm') { - echo $repoController->getName() . ' ❯ ' . $repoController->getReleasever(); - } ?> - -

    + getPackageType() == 'deb') { + echo Label::white($repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection()); + } + if ($repoController->getPackageType() == 'rpm') { + echo Label::white($repoController->getName() . ' ❯ ' . $repoController->getReleasever()); + } ?>
    DATE
    -

    getDateFormatted() ?>

    + getDateFormatted()) ?>
    diff --git a/www/views/templates/tasks/duplicate.inc.php b/www/views/templates/tasks/duplicate.inc.php index a05ae03d9..3917e2cd2 100644 --- a/www/views/templates/tasks/duplicate.inc.php +++ b/www/views/templates/tasks/duplicate.inc.php @@ -1,59 +1,35 @@ -
    -
    -

    DUPLICATE REPOSITORY SNAPSHOT

    + -
    -

    format('d-m-Y') . ' ' . $taskInfo['Time'] ?>

    -
    -

    Task #

    - '; - } ?> -
    -
    -
    -
    - -
    +
    REPOSITORY
    -

    - - getPackageType() == 'rpm') { - echo $repoController->getName() . ' ❯ ' . $repoController->getReleasever(); - } - if ($repoController->getPackageType() == 'deb') { - echo $repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection(); - } ?> - -

    + getPackageType() == 'rpm') { + echo Label::white($repoController->getName() . ' ❯ ' . $repoController->getReleasever()); + } + if ($repoController->getPackageType() == 'deb') { + echo Label::white($repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection()); + } ?>
    SNAPSHOT
    -

    - getDateFormatted() ?> -

    + getDateFormatted()) ?>
    DUPLICATE TO
    -

    - - getPackageType() == 'rpm') { - echo $rawParams['name'] . ' ❯ ' . $repoController->getReleasever(); - } - if ($repoController->getPackageType() == 'deb') { - echo $rawParams['name'] . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection(); - } ?> - -

    + getPackageType() == 'rpm') { + echo Label::white($rawParams['name'] . ' ❯ ' . $repoController->getReleasever()); + } + if ($repoController->getPackageType() == 'deb') { + echo Label::white($rawParams['name'] . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection()); + } ?>
    -
    + endif; -
    - getDescription())) : ?> + if (!empty($rawParams['description'])) : ?>
    DESCRIPTION
    -

    getDescription() ?>

    +

    +
    + +
    +
    TAGS
    +
    + +
    -
    -

    POINT AN ENVIRONMENT

    + -
    -

    format('d-m-Y') . ' ' . $taskInfo['Time'] ?>

    -
    -

    Task #

    - '; - } ?> -
    -
    -
    -
    - -
    +
    REPOSITORY
    -

    - - getPackageType() == 'rpm') { - echo $repoController->getName() . ' ❯ ' . $repoController->getReleasever(); - } - if ($repoController->getPackageType() == 'deb') { - echo $repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection(); - } ?> - -

    + getPackageType() == 'rpm') { + echo Label::white($repoController->getName() . ' ❯ ' . $repoController->getReleasever()); + } + if ($repoController->getPackageType() == 'deb') { + echo Label::white($repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection()); + } ?>
    SNAPSHOT
    -

    - getDateFormatted() ?> -

    + getDateFormatted()) ?>
    @@ -46,20 +26,9 @@
    - -
    - getDescription())) : ?> -
    -
    DESCRIPTION
    -

    getDescription() ?>

    -
    - -
    diff --git a/www/views/templates/tasks/rebuild.inc.php b/www/views/templates/tasks/rebuild.inc.php index 133809ba8..93f98fac8 100644 --- a/www/views/templates/tasks/rebuild.inc.php +++ b/www/views/templates/tasks/rebuild.inc.php @@ -1,42 +1,22 @@ -
    -
    -

    REBUILD REPOSITORY METADATA

    + -
    -

    format('d-m-Y') . ' ' . $taskInfo['Time'] ?>

    -
    -

    Task #

    - '; - } ?> -
    -
    -
    -
    - -
    +
    REPOSITORY
    -

    - - getPackageType() == 'deb') { - echo $repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection(); - } - if ($repoController->getPackageType() == 'rpm') { - echo $repoController->getName() . ' ❯ ' . $repoController->getReleasever(); - } ?> - -

    + getPackageType() == 'deb') { + echo Label::white($repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection()); + } + if ($repoController->getPackageType() == 'rpm') { + echo Label::white($repoController->getName() . ' ❯ ' . $repoController->getReleasever()); + } ?>
    SNAPSHOT
    -

    - getDateFormatted() ?> -

    + getDateFormatted()) ?>
    @@ -48,7 +28,7 @@
    getArch() as $arch) { - echo '' . $arch . ''; + echo Label::white($arch); } ?>
    diff --git a/www/views/templates/tasks/removeEnv.inc.php b/www/views/templates/tasks/removeEnv.inc.php index 4a0f9edd0..033e64b69 100644 --- a/www/views/templates/tasks/removeEnv.inc.php +++ b/www/views/templates/tasks/removeEnv.inc.php @@ -1,40 +1,22 @@ -
    -
    -

    REMOVE ENVIRONMENT

    + -
    -

    format('d-m-Y') . ' ' . $taskInfo['Time'] ?>

    -
    -

    Task #

    - '; - } ?> -
    -
    -
    -
    - -
    +
    REPOSITORY
    -

    - - getPackageType() == 'deb') { - echo $repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection(); - } - if ($repoController->getPackageType() == 'rpm') { - echo $repoController->getName() . ' ❯ ' . $repoController->getReleasever(); - } ?> - -

    + getPackageType() == 'deb') { + echo Label::white($repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection()); + } + if ($repoController->getPackageType() == 'rpm') { + echo Label::white($repoController->getName() . ' ❯ ' . $repoController->getReleasever()); + } ?>
    ENVIRONNEMENT
    -

    +
    diff --git a/www/views/templates/tasks/rename.inc.php b/www/views/templates/tasks/rename.inc.php index 6add4eb43..5dbe9943b 100644 --- a/www/views/templates/tasks/rename.inc.php +++ b/www/views/templates/tasks/rename.inc.php @@ -1,50 +1,28 @@ -
    -
    -

    RENAME REPOSITORY

    + -
    -

    format('d-m-Y') . ' ' . $taskInfo['Time'] ?>

    -
    -

    Task #

    - '; - } ?> -
    -
    -
    -
    - -
    +
    REPOSITORY
    -

    - - getPackageType() == 'rpm') { - echo $rawParams['old-name'] . ' ❯ ' . $repoController->getReleasever(); - } - if ($repoController->getPackageType() == 'deb') { - echo $rawParams['old-name'] . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection(); - } ?> - -

    + getPackageType() == 'rpm') { + echo Label::white($rawParams['old-name'] . ' ❯ ' . $repoController->getReleasever()); + } + if ($repoController->getPackageType() == 'deb') { + echo Label::white($rawParams['old-name'] . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection()); + } ?>
    RENAME TO
    -

    - - getPackageType() == 'rpm') { - echo $rawParams['name'] . ' ❯ ' . $repoController->getReleasever(); - } - if ($repoController->getPackageType() == 'deb') { - echo $rawParams['name'] . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection(); - } ?> - -

    + getPackageType() == 'rpm') { + echo Label::white($rawParams['name'] . ' ❯ ' . $repoController->getReleasever()); + } + if ($repoController->getPackageType() == 'deb') { + echo Label::white($rawParams['name'] . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection()); + } ?>
    \ No newline at end of file diff --git a/www/views/templates/tasks/update.inc.php b/www/views/templates/tasks/update.inc.php index 5aede9a3a..9983fa826 100644 --- a/www/views/templates/tasks/update.inc.php +++ b/www/views/templates/tasks/update.inc.php @@ -1,40 +1,22 @@ -
    -
    -

    UPDATE REPOSITORY

    + -
    -

    format('d-m-Y') . ' ' . $taskInfo['Time'] ?>

    -
    -

    Task #

    - '; - } ?> -
    -
    -
    -
    - -
    +
    REPOSITORY
    -

    - - getPackageType() == 'deb') { - echo $repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection(); - } - if ($repoController->getPackageType() == 'rpm') { - echo $repoController->getName(); - } ?> - -

    + getPackageType() == 'deb') { + echo Label::white($repoController->getName() . ' ❯ ' . $repoController->getDist() . ' ❯ ' . $repoController->getSection()); + } + if ($repoController->getPackageType() == 'rpm') { + echo Label::white($repoController->getName()); + } ?>
    SOURCE REPOSITORY
    -

    getSource() ?>

    +

    getSource()) ?>

    @@ -57,7 +39,7 @@
    ' . $arch . ''; + echo Label::white($arch); } ?>
    @@ -70,7 +52,7 @@
    @@ -86,7 +68,7 @@
    ' . $package . ''; + echo Label::white($package); } ?>
    @@ -99,7 +81,7 @@
    ' . $package . ''; + echo Label::white($package); } ?>
    @@ -135,7 +117,7 @@ } $displayName = $metadataMap[$metadata] ?? $metadata; - echo '' . $displayName . ''; + echo Label::white($displayName); } ?>