Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion .github/workflows/sbom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
pull_request:
branches: [main, development]

permissions:
contents: write
pull-requests: write

jobs:
sbom:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -79,7 +83,18 @@ jobs:
- name: npm audit
run: npm audit --audit-level=critical

- name: Commit SBOM
# Protected branches (main, development, beta) reject direct pushes per
# the org ruleset, so route SBOM updates through a PR instead. On any
# other branch (feature/**, bugfix/**, hotfix/**) commit + push direct
# since they aren't protected. Skip entirely on pull_request events —
# the SBOM is already validated by the steps above; merging the PR will
# re-trigger this workflow on the target branch.
- name: Commit SBOM (unprotected branches)
if: |
github.event_name == 'push' &&
github.ref_name != 'main' &&
github.ref_name != 'development' &&
github.ref_name != 'beta'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
Expand All @@ -91,6 +106,26 @@ jobs:
git push
fi

- name: Open PR with SBOM update (protected branches)
if: |
github.event_name == 'push' &&
(github.ref_name == 'main' || github.ref_name == 'development' || github.ref_name == 'beta')
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore: update SBOM"
title: "chore: update SBOM (auto-generated)"
body: |
Auto-generated SBOM regeneration after a push to `${{ github.ref_name }}`.

Source run: [${{ github.workflow }} #${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).

Merge to keep `sbom.cdx.json` in sync with the locked dependency tree on `${{ github.ref_name }}`.
branch: chore/sbom-update-${{ github.ref_name }}
base: ${{ github.ref_name }}
add-paths: sbom.cdx.json
delete-branch: true

- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"phpmetrics": "./vendor/bin/phpmetrics --report-html=phpmetrics lib/",
"phpmetrics:violations": "./vendor/bin/phpmetrics --violations-xml=phpmetrics/violations.xml lib/",
"psalm": "./vendor/bin/psalm --threads=1 --no-cache || echo 'Psalm not installed, skipping...'",
"phpstan": "./vendor/bin/phpstan analyse --memory-limit=1G || echo 'PHPStan not installed, skipping...'",
"phpstan": "./vendor/bin/phpstan analyse --memory-limit=1G",
"test": "phpunit --configuration phpunit.xml",
"test:unit": "./vendor/bin/phpunit --colors=always || echo 'Tests require Nextcloud environment, skipping...'",
"test:all": "./vendor/bin/phpunit --colors=always || echo 'Tests require Nextcloud environment, skipping...'",
Expand Down
Binary file added docs/screenshots/admin-settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/customize-panel-dashboards.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/customize-panel-widgets.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/template-create-modal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions lib/Controller/AdminController.php
Original file line number Diff line number Diff line change
Expand Up @@ -355,9 +355,10 @@ public function updateGroupOrder(mixed $groups=null): JSONResponse

try {
$this->settingsService->setGroupOrder(groupIds: $groups);
} catch (InvalidArgumentException $e) {
} catch (InvalidArgumentException) {
// ADR-005: do not leak raw exception messages.
return new JSONResponse(
data: ['error' => $e->getMessage()],
data: ['error' => 'Invalid groups payload'],
statusCode: Http::STATUS_BAD_REQUEST
);
}
Expand Down
20 changes: 12 additions & 8 deletions lib/Controller/DashboardApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -432,9 +432,10 @@ public function getGroup(
groupId: $groupId,
uuid: $uuid
);
} catch (DoesNotExistException $e) {
} catch (DoesNotExistException) {
// ADR-005: do not leak raw exception messages to clients.
return new JSONResponse(
data: ['error' => $e->getMessage()],
data: ['error' => 'Dashboard not found'],
statusCode: Http::STATUS_NOT_FOUND
);
}
Expand Down Expand Up @@ -496,9 +497,10 @@ public function updateGroup(
return ResponseHelper::success(
data: ['dashboard' => $dashboard->jsonSerialize()]
);
} catch (DoesNotExistException $e) {
} catch (DoesNotExistException) {
// ADR-005: do not leak raw exception messages to clients.
return new JSONResponse(
data: ['error' => $e->getMessage()],
data: ['error' => 'Dashboard not found'],
statusCode: Http::STATUS_NOT_FOUND
);
} catch (\Exception $e) {
Expand Down Expand Up @@ -543,9 +545,10 @@ public function deleteGroup(
);

return ResponseHelper::success(data: ['status' => 'ok']);
} catch (DoesNotExistException $e) {
} catch (DoesNotExistException) {
// ADR-005: do not leak raw exception messages to clients.
return new JSONResponse(
data: ['error' => $e->getMessage()],
data: ['error' => 'Dashboard not found'],
statusCode: Http::STATUS_NOT_FOUND
);
} catch (\Exception $e) {
Expand Down Expand Up @@ -608,9 +611,10 @@ public function setGroupDefault(
'uuid' => $uuid,
]
);
} catch (DoesNotExistException $e) {
} catch (DoesNotExistException) {
// ADR-005: do not leak raw exception messages to clients.
return new JSONResponse(
data: ['error' => $e->getMessage()],
data: ['error' => 'Dashboard not found'],
statusCode: Http::STATUS_NOT_FOUND
);
} catch (\Exception $e) {
Expand Down
35 changes: 21 additions & 14 deletions lib/Controller/DashboardShareApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
{
if ($this->userId === null) {
return new DataResponse(
data: ['error' => 'Not logged in'],

Check failure on line 75 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array<string, string> given.

Check failure on line 75 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array<string, string> given.
statusCode: Http::STATUS_UNAUTHORIZED
);
}
Expand All @@ -86,15 +86,16 @@
callback: static fn($s) => $s->jsonSerialize(),
array: $shares
);
return new DataResponse(data: $serialized);

Check failure on line 89 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array<array> given.

Check failure on line 89 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array<array> given.
} catch (DoesNotExistException) {
return new DataResponse(
data: ['error' => 'Dashboard not found'],

Check failure on line 92 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array<string, string> given.

Check failure on line 92 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array<string, string> given.
statusCode: Http::STATUS_NOT_FOUND
);
} catch (Exception $e) {
} catch (Exception) {
// ADR-005: do not leak raw exception messages to clients.
return new DataResponse(
data: ['error' => $e->getMessage()],
data: ['error' => 'Forbidden'],

Check failure on line 98 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array<string, string> given.

Check failure on line 98 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array<string, string> given.
statusCode: Http::STATUS_FORBIDDEN
);
}//end try
Expand All @@ -119,7 +120,7 @@
): DataResponse {
if ($this->userId === null) {
return new DataResponse(
data: ['error' => 'Not logged in'],

Check failure on line 123 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array<string, string> given.

Check failure on line 123 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array<string, string> given.
statusCode: Http::STATUS_UNAUTHORIZED
);
}
Expand All @@ -133,22 +134,24 @@
callerId: $this->userId
);
return new DataResponse(
data: $share->jsonSerialize(),

Check failure on line 137 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array given.

Check failure on line 137 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array given.
statusCode: Http::STATUS_CREATED
);
} catch (InvalidArgumentException $e) {
} catch (InvalidArgumentException) {
// ADR-005: do not leak raw exception messages to clients.
return new DataResponse(
data: ['error' => $e->getMessage()],
data: ['error' => 'Invalid request'],

Check failure on line 143 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array<string, string> given.

Check failure on line 143 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array<string, string> given.
statusCode: Http::STATUS_BAD_REQUEST
);
} catch (DoesNotExistException) {
return new DataResponse(
data: ['error' => 'Dashboard not found'],

Check failure on line 148 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array<string, string> given.

Check failure on line 148 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array<string, string> given.
statusCode: Http::STATUS_NOT_FOUND
);
} catch (Exception $e) {
} catch (Exception) {
// ADR-005: do not leak raw exception messages to clients.
return new DataResponse(
data: ['error' => $e->getMessage()],
data: ['error' => 'Forbidden'],

Check failure on line 154 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array<string, string> given.

Check failure on line 154 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array<string, string> given.
statusCode: Http::STATUS_FORBIDDEN
);
}//end try
Expand All @@ -166,7 +169,7 @@
{
if ($this->userId === null) {
return new DataResponse(
data: ['error' => 'Not logged in'],

Check failure on line 172 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array<string, string> given.

Check failure on line 172 in lib/Controller/DashboardShareApiController.php

View workflow job for this annotation

GitHub Actions / quality / PHP Quality (phpstan)

Parameter $data of class OCP\AppFramework\Http\DataResponse constructor expects T of OCP\AppFramework\Http\DataResponseType, array<string, string> given.
statusCode: Http::STATUS_UNAUTHORIZED
);
}
Expand All @@ -182,9 +185,10 @@
data: ['error' => 'Share not found'],
statusCode: Http::STATUS_NOT_FOUND
);
} catch (Exception $e) {
} catch (Exception) {
// ADR-005: do not leak raw exception messages to clients.
return new DataResponse(
data: ['error' => $e->getMessage()],
data: ['error' => 'Forbidden'],
statusCode: Http::STATUS_FORBIDDEN
);
}//end try
Expand Down Expand Up @@ -223,19 +227,21 @@
array: $newShares
);
return new DataResponse(data: $serialized);
} catch (InvalidArgumentException $e) {
} catch (InvalidArgumentException) {
// ADR-005: do not leak raw exception messages to clients.
return new DataResponse(
data: ['error' => $e->getMessage()],
data: ['error' => 'Invalid request'],
statusCode: Http::STATUS_BAD_REQUEST
);
} catch (DoesNotExistException) {
return new DataResponse(
data: ['error' => 'Dashboard not found'],
statusCode: Http::STATUS_NOT_FOUND
);
} catch (Exception $e) {
} catch (Exception) {
// ADR-005: do not leak raw exception messages to clients.
return new DataResponse(
data: ['error' => $e->getMessage()],
data: ['error' => 'Forbidden'],
statusCode: Http::STATUS_FORBIDDEN
);
}//end try
Expand Down Expand Up @@ -269,9 +275,10 @@
callerId: $this->userId
);
return new DataResponse(data: ['deleted' => $count]);
} catch (InvalidArgumentException $e) {
} catch (InvalidArgumentException) {
// ADR-005: do not leak raw exception messages to clients.
return new DataResponse(
data: ['error' => $e->getMessage()],
data: ['error' => 'Invalid request'],
statusCode: Http::STATUS_BAD_REQUEST
);
}
Expand Down
36 changes: 19 additions & 17 deletions lib/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,18 @@ class PageController extends Controller
/**
* Constructor
*
* @param IRequest $request The request.
* @param IInitialState $initialState Nextcloud initial-state service.
* @param IDashboardManager $dashboardManager Dashboard widget manager.
* @param IUserSession $userSession Current user session.
* @param IGroupManager $groupManager Group membership lookup.
* @param AdminSettingsService $adminSettings MyDash admin settings.
* @param DashboardService $dashboardService Dashboard service (active
* resolver — REQ-DASH-018).
* @param AdminTemplateService $templateService Template service (primary
* group resolver —
* REQ-TMPL-012).
* @param IRequest $request The request.
* @param IInitialState $initialState Nextcloud initial-state service.
* @param IDashboardManager $dashboardManager Dashboard widget manager.
* @param IUserSession $userSession Current user session.
* @param IGroupManager $groupManager Group membership lookup.
* @param AdminSettingsService $adminSettings MyDash admin settings.
* @param DashboardService $dashboardService Dashboard service (active
* resolver —
* REQ-DASH-018).
* @param AdminTemplateService $templateService Template service (primary
* group resolver —
* REQ-TMPL-012).
*/
public function __construct(
IRequest $request,
Expand Down Expand Up @@ -88,18 +89,19 @@ public function index(): TemplateResponse
// Load all widget scripts so legacy widgets can register their callbacks.
$widgets = $this->loadWidgetScripts();

$user = $this->userSession->getUser();
$isAdmin = false;
$userId = null;
$user = $this->userSession->getUser();
$isAdmin = false;
$userId = null;
if ($user !== null) {
$userId = $user->getUID();
$isAdmin = $this->groupManager->isAdmin(userId: $userId);
}

// Resolve the primary group via the canonical REQ-TMPL-012 authority.
$primaryGroup = ($userId !== null)
? $this->templateService->resolvePrimaryGroup(userId: $userId)
: Dashboard::DEFAULT_GROUP_ID;
$primaryGroup = Dashboard::DEFAULT_GROUP_ID;
if ($userId !== null) {
$primaryGroup = $this->templateService->resolvePrimaryGroup(userId: $userId);
}

$primaryGroupName = $primaryGroup;
if ($primaryGroup !== 'default'
Expand Down
12 changes: 6 additions & 6 deletions lib/Controller/ResponseHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ public static function forbidden(
* exception is recorded in the server log; the client only ever sees
* the generic `$message` (defaulting to "Operation failed").
*
* @param \Exception $exception The exception.
* @param int $statusCode The HTTP status code.
* @param LoggerInterface|null $logger When provided, the exception is
* logged at ERROR level before
* the response is returned.
* @param string $message Generic client-facing message.
* @param \Exception $exception The exception.
* @param int $statusCode The HTTP status code.
* @param LoggerInterface|null $logger When provided, the exception is
* logged at ERROR level before
* the response is returned.
* @param string $message Generic client-facing message.
*
* @return JSONResponse The error response.
*/
Expand Down
4 changes: 2 additions & 2 deletions lib/Db/AdminSettingMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,13 @@ public function setSetting(string $key, mixed $value): AdminSetting
try {
$setting = $this->findByKey(key: $key);
$setting->setValueEncoded($value);
$setting->setUpdatedAt(new DateTime());
$setting->setUpdatedAt((new DateTime())->format(format: 'Y-m-d H:i:s'));
return $this->update(entity: $setting);
} catch (DoesNotExistException) {
$setting = new AdminSetting();
$setting->setSettingKey($key);
$setting->setValueEncoded($value);
$setting->setUpdatedAt(new DateTime());
$setting->setUpdatedAt((new DateTime())->format(format: 'Y-m-d H:i:s'));
return $this->insert(entity: $setting);
}
}//end setSetting()
Expand Down
26 changes: 13 additions & 13 deletions lib/Notification/Notifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,15 +139,15 @@ public function prepare(
*
* Subject parameters: [sharerUserId, dashboardName, permissionLevel].
*
* @param INotification $notification The notification.
* @param \OCP\L10N\IL10N $l The L10N instance.
* @param string $url The deep-link URL.
* @param INotification $notification The notification.
* @param \OCP\IL10N $l The L10N instance.
* @param string $url The deep-link URL.
*
* @return INotification The prepared notification.
*/
private function prepareDashboardShared(
INotification $notification,
\OCP\L10N\IL10N $l,
\OCP\IL10N $l,
string $url
): INotification {
$params = $notification->getSubjectParameters();
Expand Down Expand Up @@ -175,7 +175,7 @@ private function prepareDashboardShared(
message: $levelLabel,
parameters: []
);
$notification->setParsedMessage(subject: $levelLabel);
$notification->setParsedMessage(message: $levelLabel);

$notification->setLink(link: $url);

Expand All @@ -187,15 +187,15 @@ private function prepareDashboardShared(
*
* Subject parameters: [dashboardName].
*
* @param INotification $notification The notification.
* @param \OCP\L10N\IL10N $l The L10N instance.
* @param string $url The deep-link URL.
* @param INotification $notification The notification.
* @param \OCP\IL10N $l The L10N instance.
* @param string $url The deep-link URL.
*
* @return INotification The prepared notification.
*/
private function prepareOwnershipTransferred(
INotification $notification,
\OCP\L10N\IL10N $l,
\OCP\IL10N $l,
string $url
): INotification {
$params = $notification->getSubjectParameters();
Expand All @@ -217,7 +217,7 @@ private function prepareOwnershipTransferred(
message: $message,
parameters: []
);
$notification->setParsedMessage(subject: $message);
$notification->setParsedMessage(message: $message);

$notification->setLink(link: $url);

Expand Down Expand Up @@ -261,13 +261,13 @@ private function buildDashboardUrl(string $objectId): string
/**
* Return the human-readable label for a permission level.
*
* @param \OCP\L10N\IL10N $l The L10N instance.
* @param string $level The permission level identifier.
* @param \OCP\IL10N $l The L10N instance.
* @param string $level The permission level identifier.
*
* @return string The translated label.
*/
private function permissionLabel(
\OCP\L10N\IL10N $l,
\OCP\IL10N $l,
string $level
): string {
return match ($level) {
Expand Down
Loading
Loading