refactor: align addressbook module to MintyPHP\Module\AddressBook namespace
Migrates addressbook service classes from core namespace (MintyPHP\Service\AddressBook\*) to proper module namespace (MintyPHP\Module\AddressBook\Service\*), consistent with the bookmarks module pattern. Moved to module: - AddressBookService → modules/addressbook/lib/Module/AddressBook/Service/ - AddressBookServicesFactory → modules/addressbook/lib/Module/AddressBook/Service/ - AddressBookServiceTest → modules/addressbook/tests/Module/AddressBook/Service/ - Pages, templates, CSS, JS all in module directory All module PHP classes now live under MintyPHP\Module\<Name>\*, enforced by ModuleStructureContractTest::testModuleClassesUseModuleNamespace. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,40 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace MintyPHP\Module\AddressBook\Providers;
|
|
||||||
|
|
||||||
use MintyPHP\App\AppContainer;
|
|
||||||
use MintyPHP\App\Module\Contracts\LayoutContextProvider;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides address book data for the layout navigation context.
|
|
||||||
*
|
|
||||||
* Contributes the 'addressBook' key to $layoutNav with URL, active filters,
|
|
||||||
* and people groups from the session.
|
|
||||||
*/
|
|
||||||
final class AddressBookLayoutProvider implements LayoutContextProvider
|
|
||||||
{
|
|
||||||
public function provide(array $session, AppContainer $container): array
|
|
||||||
{
|
|
||||||
// Read query params for active filter state
|
|
||||||
$query = [];
|
|
||||||
try {
|
|
||||||
$query = requestInput()->queryAll();
|
|
||||||
} catch (\Throwable) {
|
|
||||||
// fail-open: may not be available in CLI context
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'addressBook' => [
|
|
||||||
'url' => lurl('address-book'),
|
|
||||||
'activeSearch' => trim((string) ($query['search'] ?? '')),
|
|
||||||
'activeTenants' => appNormalizeStringList($query['tenants'] ?? ''),
|
|
||||||
'activeDepartments' => appNormalizePositiveIntList($query['departments'] ?? ''),
|
|
||||||
'activeRoles' => appNormalizePositiveIntList($query['roles'] ?? ''),
|
|
||||||
'activeCustomFilters' => appNormalizeAddressBookCustomFilterQuery($query),
|
|
||||||
'peopleGroups' => is_array($session['available_departments_by_tenant'] ?? null)
|
|
||||||
? $session['available_departments_by_tenant']
|
|
||||||
: [],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace MintyPHP\Module\AddressBook\Providers;
|
|
||||||
|
|
||||||
use MintyPHP\App\AppContainer;
|
|
||||||
use MintyPHP\App\Module\Contracts\SessionProvider;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Populates session with department-by-tenant hierarchy data used by the
|
|
||||||
* address book aside panel.
|
|
||||||
*
|
|
||||||
* In V1 this data is already populated by the core auth flow (loadTenantDataIntoSession).
|
|
||||||
* This provider is a placeholder that will take over population once the core
|
|
||||||
* address book hardcodings are fully removed.
|
|
||||||
*/
|
|
||||||
final class AddressBookSessionProvider implements SessionProvider
|
|
||||||
{
|
|
||||||
public function populate(array $user, AppContainer $container): void
|
|
||||||
{
|
|
||||||
// V1: available_departments_by_tenant is still populated by core's
|
|
||||||
// AuthService::loadTenantDataIntoSession(). This provider is registered
|
|
||||||
// so the contract is satisfied and the session hook is ready for V2
|
|
||||||
// when the core delegation is fully removed.
|
|
||||||
}
|
|
||||||
|
|
||||||
public function clear(): void
|
|
||||||
{
|
|
||||||
unset($_SESSION['available_departments_by_tenant']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MintyPHP\Module\AddressBook;
|
||||||
|
|
||||||
|
use MintyPHP\Service\Access\AuthorizationDecision;
|
||||||
|
use MintyPHP\Service\Access\AuthorizationPolicyInterface;
|
||||||
|
use MintyPHP\Service\Access\PermissionService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorization policy for address book abilities.
|
||||||
|
*
|
||||||
|
* Owns the 'addressbook.view' ability and delegates to the
|
||||||
|
* 'address_book.view' permission in the Core PermissionService.
|
||||||
|
*/
|
||||||
|
final class AddressBookAuthorizationPolicy implements AuthorizationPolicyInterface
|
||||||
|
{
|
||||||
|
public const ABILITY_VIEW = 'addressbook.view';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly PermissionService $permissionService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supports(string $ability): bool
|
||||||
|
{
|
||||||
|
return $ability === self::ABILITY_VIEW;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authorize(string $ability, array $context = []): AuthorizationDecision
|
||||||
|
{
|
||||||
|
$actorUserId = (int) ($context['actor_user_id'] ?? 0);
|
||||||
|
if ($actorUserId <= 0) {
|
||||||
|
return AuthorizationDecision::deny(403, 'forbidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->permissionService->userHas($actorUserId, PermissionService::ADDRESS_BOOK_VIEW)) {
|
||||||
|
return AuthorizationDecision::deny(403, 'forbidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthorizationDecision::allow();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MintyPHP\Module\AddressBook;
|
||||||
|
|
||||||
|
use MintyPHP\App\AppContainer;
|
||||||
|
use MintyPHP\App\Container\ContainerRegistrar;
|
||||||
|
use MintyPHP\Module\AddressBook\Service\AddressBookService;
|
||||||
|
use MintyPHP\Module\AddressBook\Service\AddressBookServicesFactory;
|
||||||
|
use MintyPHP\Service\CustomField\CustomFieldServicesFactory;
|
||||||
|
use MintyPHP\Service\Directory\DirectoryServicesFactory;
|
||||||
|
use MintyPHP\Service\Tenant\TenantScopeService;
|
||||||
|
use MintyPHP\Service\User\UserServicesFactory;
|
||||||
|
|
||||||
|
final class AddressBookContainerRegistrar implements ContainerRegistrar
|
||||||
|
{
|
||||||
|
public function register(AppContainer $container): void
|
||||||
|
{
|
||||||
|
$container->set(AddressBookServicesFactory::class, static fn (AppContainer $c): AddressBookServicesFactory => new AddressBookServicesFactory(
|
||||||
|
$c->get(UserServicesFactory::class),
|
||||||
|
$c->get(DirectoryServicesFactory::class),
|
||||||
|
$c->get(CustomFieldServicesFactory::class),
|
||||||
|
$c->get(TenantScopeService::class)
|
||||||
|
));
|
||||||
|
|
||||||
|
$container->set(AddressBookService::class, static fn (AppContainer $c): AddressBookService => $c
|
||||||
|
->get(AddressBookServicesFactory::class)
|
||||||
|
->createAddressBookService());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MintyPHP\Module\AddressBook\Providers;
|
||||||
|
|
||||||
|
use MintyPHP\App\AppContainer;
|
||||||
|
use MintyPHP\App\Module\Contracts\LayoutContextProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides address book data for the layout navigation context.
|
||||||
|
*
|
||||||
|
* Contributes the 'addressbook.nav' key to $layoutNav with URL, active filters,
|
||||||
|
* and people groups from the session.
|
||||||
|
*/
|
||||||
|
final class AddressBookLayoutProvider implements LayoutContextProvider
|
||||||
|
{
|
||||||
|
public function provide(array $session, AppContainer $container): array
|
||||||
|
{
|
||||||
|
// Read query params for active filter state
|
||||||
|
$query = [];
|
||||||
|
try {
|
||||||
|
$query = requestInput()->queryAll();
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// fail-open: may not be available in CLI context
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'addressbook.nav' => [
|
||||||
|
'url' => lurl('address-book'),
|
||||||
|
'activeSearch' => trim((string) ($query['search'] ?? '')),
|
||||||
|
'activeTenants' => appNormalizeStringList($query['tenants'] ?? ''),
|
||||||
|
'activeDepartments' => appNormalizePositiveIntList($query['departments'] ?? ''),
|
||||||
|
'activeRoles' => appNormalizePositiveIntList($query['roles'] ?? ''),
|
||||||
|
'activeCustomFilters' => self::normalizeCustomFilterQuery($query),
|
||||||
|
'peopleGroups' => is_array($session['available_departments_by_tenant'] ?? null)
|
||||||
|
? $session['available_departments_by_tenant']
|
||||||
|
: [],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize address-book custom field query keys/values.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $rawQuery
|
||||||
|
* @return array<string, string|list<int>>
|
||||||
|
*/
|
||||||
|
public static function normalizeCustomFilterQuery(array $rawQuery): array
|
||||||
|
{
|
||||||
|
$normalized = [];
|
||||||
|
foreach ($rawQuery as $rawKey => $rawValue) {
|
||||||
|
$key = strtolower(trim((string) $rawKey));
|
||||||
|
if ($key === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (preg_match('/^cf_[a-f0-9-]{36}$/', $key)) {
|
||||||
|
$value = trim((string) $rawValue);
|
||||||
|
if ($value !== '') {
|
||||||
|
$normalized[$key] = $value;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (preg_match('/^cfm_[a-f0-9-]{36}$/', $key)) {
|
||||||
|
$ids = appNormalizePositiveIntList($rawValue);
|
||||||
|
if ($ids) {
|
||||||
|
$normalized[$key] = $ids;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (preg_match('/^cfd_[a-f0-9-]{36}_(from|to)$/', $key)) {
|
||||||
|
$value = trim((string) $rawValue);
|
||||||
|
$dt = \DateTimeImmutable::createFromFormat('Y-m-d', $value);
|
||||||
|
if ($dt && $dt->format('Y-m-d') === $value) {
|
||||||
|
$normalized[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ksort($normalized, SORT_STRING);
|
||||||
|
return $normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MintyPHP\Module\AddressBook\Providers;
|
||||||
|
|
||||||
|
use MintyPHP\App\AppContainer;
|
||||||
|
use MintyPHP\App\Module\Contracts\SessionProvider;
|
||||||
|
use MintyPHP\Http\SessionStoreInterface;
|
||||||
|
use MintyPHP\Service\User\UserTenantContextService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates session with department-by-tenant hierarchy data used by the
|
||||||
|
* address book aside panel.
|
||||||
|
*/
|
||||||
|
final class AddressBookSessionProvider implements SessionProvider
|
||||||
|
{
|
||||||
|
public function populate(array $user, AppContainer $container): void
|
||||||
|
{
|
||||||
|
$userId = (int) ($user['id'] ?? 0);
|
||||||
|
if ($userId <= 0) {
|
||||||
|
unset($_SESSION['available_departments_by_tenant']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenantContext = $container->get(UserTenantContextService::class);
|
||||||
|
$sessionStore = $container->get(SessionStoreInterface::class);
|
||||||
|
if (!$tenantContext instanceof UserTenantContextService || !$sessionStore instanceof SessionStoreInterface) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionStore->set(
|
||||||
|
'available_departments_by_tenant',
|
||||||
|
$tenantContext->getAvailableDepartmentsByTenant($userId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clear(): void
|
||||||
|
{
|
||||||
|
unset($_SESSION['available_departments_by_tenant']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace MintyPHP\Service\AddressBook;
|
namespace MintyPHP\Module\AddressBook\Service;
|
||||||
|
|
||||||
use MintyPHP\Repository\CustomField\TenantCustomFieldOptionRepository;
|
use MintyPHP\Repository\CustomField\TenantCustomFieldOptionRepository;
|
||||||
use MintyPHP\Service\Access\RoleService;
|
use MintyPHP\Service\Access\RoleService;
|
||||||
@@ -61,13 +61,11 @@ class AddressBookService
|
|||||||
|
|
||||||
$roles = $this->roleService->listActive();
|
$roles = $this->roleService->listActive();
|
||||||
|
|
||||||
$customFieldFilterSpec = $this->customFieldValueService->extractAddressBookFilterSpec($query, $currentUserId);
|
$customFieldFilterSpec = $this->customFieldValueService->extractCustomFieldFilterSpec($query, $currentUserId);
|
||||||
$customFieldFilterDefinitions = $this->normalizeDefinitions(
|
$customFieldFilterDefinitions = $this->normalizeDefinitions(
|
||||||
$customFieldFilterSpec['definitions'] ?? []
|
$customFieldFilterSpec['definitions']
|
||||||
);
|
);
|
||||||
$customFieldFilterQuery = is_array($customFieldFilterSpec['query'] ?? null)
|
$customFieldFilterQuery = $customFieldFilterSpec['query'];
|
||||||
? $customFieldFilterSpec['query']
|
|
||||||
: [];
|
|
||||||
|
|
||||||
$customFieldDefinitionIds = [];
|
$customFieldDefinitionIds = [];
|
||||||
foreach ($customFieldFilterDefinitions as $definition) {
|
foreach ($customFieldFilterDefinitions as $definition) {
|
||||||
@@ -161,7 +159,7 @@ class AddressBookService
|
|||||||
$tenants = $query['tenants'] ?? '';
|
$tenants = $query['tenants'] ?? '';
|
||||||
$departments = $query['departments'] ?? '';
|
$departments = $query['departments'] ?? '';
|
||||||
$roles = $query['roles'] ?? '';
|
$roles = $query['roles'] ?? '';
|
||||||
$customFieldFilterSpec = $this->customFieldValueService->extractAddressBookFilterSpec($query, $currentUserId);
|
$customFieldFilterSpec = $this->customFieldValueService->extractCustomFieldFilterSpec($query, $currentUserId);
|
||||||
|
|
||||||
$result = $this->userAccountService->listPaged([
|
$result = $this->userAccountService->listPaged([
|
||||||
'limit' => $limit,
|
'limit' => $limit,
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace MintyPHP\Service\AddressBook;
|
namespace MintyPHP\Module\AddressBook\Service;
|
||||||
|
|
||||||
use MintyPHP\Service\CustomField\CustomFieldServicesFactory;
|
use MintyPHP\Service\CustomField\CustomFieldServicesFactory;
|
||||||
use MintyPHP\Service\Directory\DirectoryServicesFactory;
|
use MintyPHP\Service\Directory\DirectoryServicesFactory;
|
||||||
@@ -3,13 +3,8 @@
|
|||||||
/**
|
/**
|
||||||
* Address Book module manifest.
|
* Address Book module manifest.
|
||||||
*
|
*
|
||||||
* Contributes the "People" aside tab/panel, global search section,
|
* Contributes routes, sidebar slots, search entry, user-edit aside action,
|
||||||
* layout context data, and session lifecycle.
|
* layout context data, and session lifecycle.
|
||||||
*
|
|
||||||
* Permissions (address_book.view) remain in Core PermissionService because
|
|
||||||
* they gate general user-visibility in UserAuthorizationPolicy, not just
|
|
||||||
* address-book access. Asset groups remain in config/assets.php because the
|
|
||||||
* asset pipeline does not yet consume ModuleRegistry::getAssetGroups().
|
|
||||||
*/
|
*/
|
||||||
return [
|
return [
|
||||||
'id' => 'addressbook',
|
'id' => 'addressbook',
|
||||||
@@ -17,9 +12,18 @@ return [
|
|||||||
'enabled_by_default' => false,
|
'enabled_by_default' => false,
|
||||||
'load_order' => 10,
|
'load_order' => 10,
|
||||||
|
|
||||||
'routes' => [],
|
'routes' => [
|
||||||
|
// Keep canonical route stable without forcing a /index target to avoid Router redirect loops.
|
||||||
|
['path' => 'address-book', 'target' => 'address-book'],
|
||||||
|
['path' => 'address-book/data', 'target' => 'address-book/data'],
|
||||||
|
// Legacy aliases for existing bookmarks/links.
|
||||||
|
['path' => 'addressbook', 'target' => 'address-book'],
|
||||||
|
['path' => 'adressbook', 'target' => 'address-book'],
|
||||||
|
],
|
||||||
'public_paths' => [],
|
'public_paths' => [],
|
||||||
'container_registrars' => [],
|
'container_registrars' => [
|
||||||
|
\MintyPHP\Module\AddressBook\AddressBookContainerRegistrar::class,
|
||||||
|
],
|
||||||
|
|
||||||
'ui_slots' => [
|
'ui_slots' => [
|
||||||
'aside.tab_panel' => [
|
'aside.tab_panel' => [
|
||||||
@@ -32,15 +36,47 @@ return [
|
|||||||
'panel_template' => __DIR__ . '/templates/aside-people-panel.phtml',
|
'panel_template' => __DIR__ . '/templates/aside-people-panel.phtml',
|
||||||
'details_storage' => 'aside-people-tenant',
|
'details_storage' => 'aside-people-tenant',
|
||||||
'details_open_active' => true,
|
'details_open_active' => true,
|
||||||
|
'order' => 110,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'search.resource_item' => [
|
||||||
|
[
|
||||||
|
'key' => 'address-book',
|
||||||
|
'label' => 'Address book',
|
||||||
|
'base_url' => 'address-book',
|
||||||
|
'permission' => 'can_view_address_book',
|
||||||
|
'order' => 110,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'user.edit.aside_action' => [
|
||||||
|
[
|
||||||
|
'key' => 'address-book-view-user',
|
||||||
|
'type' => 'link',
|
||||||
|
'label' => 'View in address book',
|
||||||
|
'href' => 'address-book/view/{user_uuid}',
|
||||||
|
'permission' => 'can_view_address_book',
|
||||||
|
'order' => 110,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'authorization_policies' => [
|
||||||
|
\MintyPHP\Module\AddressBook\AddressBookAuthorizationPolicy::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
'layout_capabilities' => [
|
||||||
|
'can_view_address_book' => 'addressbook.view',
|
||||||
],
|
],
|
||||||
|
|
||||||
'search_resources' => [
|
'search_resources' => [
|
||||||
\MintyPHP\Module\AddressBook\Providers\AddressBookSearchProvider::class,
|
\MintyPHP\Module\AddressBook\Providers\AddressBookSearchProvider::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
'asset_groups' => [],
|
'asset_groups' => [
|
||||||
|
'address-book' => [
|
||||||
|
'modules/addressbook/css/pages/address-book-view.css',
|
||||||
|
],
|
||||||
|
],
|
||||||
'scheduler_jobs' => [],
|
'scheduler_jobs' => [],
|
||||||
|
|
||||||
'layout_context_providers' => [
|
'layout_context_providers' => [
|
||||||
@@ -51,7 +87,14 @@ return [
|
|||||||
\MintyPHP\Module\AddressBook\Providers\AddressBookSessionProvider::class,
|
\MintyPHP\Module\AddressBook\Providers\AddressBookSessionProvider::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
'permissions' => [],
|
'permissions' => [
|
||||||
|
[
|
||||||
|
'key' => 'address_book.view',
|
||||||
|
'description' => 'Can view address book',
|
||||||
|
'active' => true,
|
||||||
|
'is_system' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
'migrations_path' => 'db/updates',
|
'migrations_path' => 'db/updates',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use MintyPHP\Http\SessionStoreInterface;
|
use MintyPHP\Http\SessionStoreInterface;
|
||||||
use MintyPHP\Service\Access\OperationsAuthorizationPolicy;
|
use MintyPHP\Module\AddressBook\AddressBookAuthorizationPolicy;
|
||||||
use MintyPHP\Service\AddressBook\AddressBookServicesFactory;
|
use MintyPHP\Module\AddressBook\Service\AddressBookServicesFactory;
|
||||||
use MintyPHP\Support\Guard;
|
use MintyPHP\Support\Guard;
|
||||||
|
|
||||||
$session = app(SessionStoreInterface::class)->all();
|
$session = app(SessionStoreInterface::class)->all();
|
||||||
Guard::requireLogin();
|
Guard::requireLogin();
|
||||||
Guard::requireAbilityOrForbidden(OperationsAuthorizationPolicy::ABILITY_ADDRESS_BOOK_VIEW);
|
Guard::requireAbilityOrForbidden(AddressBookAuthorizationPolicy::ABILITY_VIEW);
|
||||||
gridRequireGetRequest();
|
gridRequireGetRequest();
|
||||||
|
|
||||||
$query = requestInput()->queryAll();
|
$query = requestInput()->queryAll();
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
use MintyPHP\Buffer;
|
use MintyPHP\Buffer;
|
||||||
use MintyPHP\Http\SessionStoreInterface;
|
use MintyPHP\Http\SessionStoreInterface;
|
||||||
use MintyPHP\Service\AddressBook\AddressBookServicesFactory;
|
use MintyPHP\Module\AddressBook\Service\AddressBookServicesFactory;
|
||||||
use MintyPHP\Session;
|
use MintyPHP\Session;
|
||||||
use MintyPHP\Support\Guard;
|
use MintyPHP\Support\Guard;
|
||||||
|
|
||||||
$session = app(SessionStoreInterface::class)->all();
|
$session = app(SessionStoreInterface::class)->all();
|
||||||
Guard::requireLogin();
|
Guard::requireLogin();
|
||||||
Guard::requireAbilityOrForbidden(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_ADDRESS_BOOK_VIEW);
|
Guard::requireAbilityOrForbidden(\MintyPHP\Module\AddressBook\AddressBookAuthorizationPolicy::ABILITY_VIEW);
|
||||||
|
|
||||||
$filterSchema = require __DIR__ . '/filter-schema.php';
|
$filterSchema = require __DIR__ . '/filter-schema.php';
|
||||||
$query = requestInput()->queryAll();
|
$query = requestInput()->queryAll();
|
||||||
@@ -239,4 +239,4 @@ $pageConfig = [
|
|||||||
?>
|
?>
|
||||||
<script src="<?php e(assetVersion('vendor/gridjs/gridjs.umd.js')); ?>"></script>
|
<script src="<?php e(assetVersion('vendor/gridjs/gridjs.umd.js')); ?>"></script>
|
||||||
<script type="application/json" id="page-config-address-book-index"><?php gridJsonForJs($pageConfig); ?></script>
|
<script type="application/json" id="page-config-address-book-index"><?php gridJsonForJs($pageConfig); ?></script>
|
||||||
<script type="module" src="<?php e(assetVersion('js/pages/address-book-index.js')); ?>"></script>
|
<script type="module" src="<?php e(assetVersion('modules/addressbook/js/pages/address-book-index.js')); ?>"></script>
|
||||||
@@ -8,7 +8,7 @@ use MintyPHP\Support\Guard;
|
|||||||
|
|
||||||
$session = app(SessionStoreInterface::class)->all();
|
$session = app(SessionStoreInterface::class)->all();
|
||||||
Guard::requireLogin();
|
Guard::requireLogin();
|
||||||
Guard::requireAbilityOrForbidden(\MintyPHP\Service\Access\OperationsAuthorizationPolicy::ABILITY_ADDRESS_BOOK_VIEW);
|
Guard::requireAbilityOrForbidden(\MintyPHP\Module\AddressBook\AddressBookAuthorizationPolicy::ABILITY_VIEW);
|
||||||
|
|
||||||
$currentUserId = (int) ($session['user']['id'] ?? 0);
|
$currentUserId = (int) ($session['user']['id'] ?? 0);
|
||||||
$uuid = trim((string) ($id ?? ''));
|
$uuid = trim((string) ($id ?? ''));
|
||||||
@@ -16,7 +16,7 @@ if ($uuid === '') {
|
|||||||
Router::redirect('address-book');
|
Router::redirect('address-book');
|
||||||
}
|
}
|
||||||
|
|
||||||
$addressBookService = app(\MintyPHP\Service\AddressBook\AddressBookService::class);
|
$addressBookService = app(\MintyPHP\Module\AddressBook\Service\AddressBookService::class);
|
||||||
$viewContext = $addressBookService->buildViewContext($currentUserId, $uuid);
|
$viewContext = $addressBookService->buildViewContext($currentUserId, $uuid);
|
||||||
$status = (string) ($viewContext['status'] ?? '');
|
$status = (string) ($viewContext['status'] ?? '');
|
||||||
if ($status === 'not_found') {
|
if ($status === 'not_found') {
|
||||||
@@ -91,7 +91,7 @@ if ($profileDescription !== '') {
|
|||||||
</div>
|
</div>
|
||||||
<div class="app-profile-body-container">
|
<div class="app-profile-body-container">
|
||||||
<div class="app-profile-body">
|
<div class="app-profile-body">
|
||||||
<div class="app-tabs" data-tabs data-tabs-param="tab" data-tabs-storage-key="address-book-profile">
|
<div class="app-tabs" data-tabs data-app-component="tabs" data-tabs-param="tab" data-tabs-storage-key="address-book-profile">
|
||||||
<div class="app-tabs-nav">
|
<div class="app-tabs-nav">
|
||||||
<?php if ($hasContact): ?>
|
<?php if ($hasContact): ?>
|
||||||
<button type="button" data-tab="contact" data-tab-default><small><?php e(t('Contact')); ?></small></button>
|
<button type="button" data-tab="contact" data-tab-default><small><?php e(t('Contact')); ?></small></button>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
*
|
*
|
||||||
* Available in scope via include: $layoutNav, $layoutAuth, $viewAuth
|
* Available in scope via include: $layoutNav, $layoutAuth, $viewAuth
|
||||||
*/
|
*/
|
||||||
$addressBook = is_array($layoutNav['addressBook'] ?? null) ? $layoutNav['addressBook'] : [];
|
$addressBook = is_array($layoutNav['addressbook.nav'] ?? null) ? $layoutNav['addressbook.nav'] : [];
|
||||||
$addressBookUrl = trim((string) ($addressBook['url'] ?? lurl('address-book')));
|
$addressBookUrl = trim((string) ($addressBook['url'] ?? lurl('address-book')));
|
||||||
$activeAddressSearch = trim((string) ($addressBook['activeSearch'] ?? ''));
|
$activeAddressSearch = trim((string) ($addressBook['activeSearch'] ?? ''));
|
||||||
$activeAddressTenants = is_array($addressBook['activeTenants'] ?? null) ? $addressBook['activeTenants'] : [];
|
$activeAddressTenants = is_array($addressBook['activeTenants'] ?? null) ? $addressBook['activeTenants'] : [];
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace MintyPHP\Tests\Service\AddressBook;
|
namespace MintyPHP\Tests\Module\AddressBook\Service;
|
||||||
|
|
||||||
use MintyPHP\Service\Access\RoleService;
|
use MintyPHP\Service\Access\RoleService;
|
||||||
use MintyPHP\Service\AddressBook\AddressBookService;
|
use MintyPHP\Module\AddressBook\Service\AddressBookService;
|
||||||
use MintyPHP\Service\CustomField\UserCustomFieldValueService;
|
use MintyPHP\Service\CustomField\UserCustomFieldValueService;
|
||||||
use MintyPHP\Service\Org\DepartmentService;
|
use MintyPHP\Service\Org\DepartmentService;
|
||||||
use MintyPHP\Service\Tenant\TenantScopeService;
|
use MintyPHP\Service\Tenant\TenantScopeService;
|
||||||
@@ -59,7 +59,7 @@ class AddressBookServiceTest extends TestCase
|
|||||||
|
|
||||||
$customFieldGateway
|
$customFieldGateway
|
||||||
->expects($this->once())
|
->expects($this->once())
|
||||||
->method('extractAddressBookFilterSpec')
|
->method('extractCustomFieldFilterSpec')
|
||||||
->with([], 7)
|
->with([], 7)
|
||||||
->willReturn([
|
->willReturn([
|
||||||
'definitions' => [],
|
'definitions' => [],
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { initMultiSelect } from '../components/app-multiselect-init.js';
|
import { initMultiSelect } from '/js/components/app-multiselect-init.js';
|
||||||
import { initMultiSelectCascade } from '../components/app-multiselect-cascade.js';
|
import { initMultiSelectCascade } from '/js/components/app-multiselect-cascade.js';
|
||||||
import { initStandardListPage } from '../core/app-grid-factory.js';
|
import { initStandardListPage } from '/js/core/app-grid-factory.js';
|
||||||
import { readPageConfig } from '../core/app-page-config.js';
|
import { readPageConfig } from '/js/core/app-page-config.js';
|
||||||
import { warnOnce } from '../core/app-dom.js';
|
import { warnOnce } from '/js/core/app-dom.js';
|
||||||
import { getAppBase, escapeHtml, buildBadgeList, gridFilterMultiCsv, gridFilterText } from './app-list-utils.js';
|
import { getAppBase, escapeHtml, buildBadgeList, gridFilterMultiCsv, gridFilterText } from '/js/pages/app-list-utils.js';
|
||||||
|
|
||||||
const config = readPageConfig('address-book-index');
|
const config = readPageConfig('address-book-index');
|
||||||
if (config) {
|
if (config) {
|
||||||
@@ -14,9 +14,7 @@ use FilesystemIterator;
|
|||||||
* Intentionally allowed in Core (not flagged):
|
* Intentionally allowed in Core (not flagged):
|
||||||
* - PermissionService::ADDRESS_BOOK_VIEW (general people-visibility gate)
|
* - PermissionService::ADDRESS_BOOK_VIEW (general people-visibility gate)
|
||||||
* - OperationsAuthorizationPolicy / UiCapabilityMap / UserAuthorizationPolicy (authorization)
|
* - OperationsAuthorizationPolicy / UiCapabilityMap / UserAuthorizationPolicy (authorization)
|
||||||
* - AddressBookService / AddressBookServicesFactory (business logic, V1 shared-db)
|
* - modules/addressbook/* (module-owned runtime, pages/assets/providers)
|
||||||
* - pages/address-book/* (page files, not hardcoded integrations)
|
|
||||||
* - config/assets.php (asset pipeline not yet modular)
|
|
||||||
* - i18n translation files
|
* - i18n translation files
|
||||||
* - Module directory (lib/Module/, modules/)
|
* - Module directory (lib/Module/, modules/)
|
||||||
* - Test files
|
* - Test files
|
||||||
@@ -90,10 +88,7 @@ final class NoAddressBookHardcodingTest extends TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aside panel template must not have hardcoded People panel.
|
* Aside panel template must not have hardcoded People panel/search entries.
|
||||||
*
|
|
||||||
* Note: The search panel legitimately contains data-search-key="address-book"
|
|
||||||
* for the global search results — that is NOT a hardcoded People panel.
|
|
||||||
*/
|
*/
|
||||||
public function testAsideHasNoHardcodedPeoplePanel(): void
|
public function testAsideHasNoHardcodedPeoplePanel(): void
|
||||||
{
|
{
|
||||||
@@ -116,6 +111,20 @@ final class NoAddressBookHardcodingTest extends TestCase
|
|||||||
$content,
|
$content,
|
||||||
'Aside still defines $peopleGroups as a standalone variable'
|
'Aside still defines $peopleGroups as a standalone variable'
|
||||||
);
|
);
|
||||||
|
self::assertStringNotContainsString(
|
||||||
|
'data-search-key="address-book"',
|
||||||
|
$content,
|
||||||
|
'Aside still has static address-book search item markup'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCoreNoLongerContainsAddressBookServiceDirectory(): void
|
||||||
|
{
|
||||||
|
$coreServiceDir = dirname(__DIR__, 2) . '/lib/Service/AddressBook';
|
||||||
|
self::assertDirectoryDoesNotExist(
|
||||||
|
$coreServiceDir,
|
||||||
|
'Addressbook service classes should live in modules/addressbook/lib, not in Core lib/Service'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
87
tests/Unit/Module/AddressBookAuthorizationPolicyTest.php
Normal file
87
tests/Unit/Module/AddressBookAuthorizationPolicyTest.php
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace MintyPHP\Tests\Unit\Module;
|
||||||
|
|
||||||
|
use MintyPHP\Module\AddressBook\AddressBookAuthorizationPolicy;
|
||||||
|
use MintyPHP\Service\Access\AuthorizationPolicyInterface;
|
||||||
|
use MintyPHP\Service\Access\PermissionService;
|
||||||
|
use MintyPHP\Tests\Service\Access\AuthorizationPolicyTestSupport;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \MintyPHP\Module\AddressBook\AddressBookAuthorizationPolicy
|
||||||
|
*/
|
||||||
|
final class AddressBookAuthorizationPolicyTest extends TestCase
|
||||||
|
{
|
||||||
|
use AuthorizationPolicyTestSupport;
|
||||||
|
|
||||||
|
public function testImplementsInterface(): void
|
||||||
|
{
|
||||||
|
$policy = new AddressBookAuthorizationPolicy($this->createMock(PermissionService::class));
|
||||||
|
self::assertInstanceOf(AuthorizationPolicyInterface::class, $policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSupportsOwnAbility(): void
|
||||||
|
{
|
||||||
|
$policy = new AddressBookAuthorizationPolicy($this->createMock(PermissionService::class));
|
||||||
|
self::assertTrue($policy->supports(AddressBookAuthorizationPolicy::ABILITY_VIEW));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDoesNotSupportOtherAbilities(): void
|
||||||
|
{
|
||||||
|
$policy = new AddressBookAuthorizationPolicy($this->createMock(PermissionService::class));
|
||||||
|
self::assertFalse($policy->supports('ops.admin.jobs.view'));
|
||||||
|
self::assertFalse($policy->supports(''));
|
||||||
|
self::assertFalse($policy->supports('ops.address_book.view')); // old ability string
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAllowedWithPermission(): void
|
||||||
|
{
|
||||||
|
$permissionService = $this->permissionGatewayAllowing([
|
||||||
|
5 => [PermissionService::ADDRESS_BOOK_VIEW],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$policy = new AddressBookAuthorizationPolicy($permissionService);
|
||||||
|
$decision = $policy->authorize(AddressBookAuthorizationPolicy::ABILITY_VIEW, [
|
||||||
|
'actor_user_id' => 5,
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertTrue($decision->isAllowed());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeniedWithoutPermission(): void
|
||||||
|
{
|
||||||
|
$permissionService = $this->permissionGatewayAllowing([5 => []]);
|
||||||
|
|
||||||
|
$policy = new AddressBookAuthorizationPolicy($permissionService);
|
||||||
|
$decision = $policy->authorize(AddressBookAuthorizationPolicy::ABILITY_VIEW, [
|
||||||
|
'actor_user_id' => 5,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertForbiddenDecision($decision);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeniedWithZeroActorId(): void
|
||||||
|
{
|
||||||
|
$permissionService = $this->createMock(PermissionService::class);
|
||||||
|
$permissionService->expects($this->never())->method('userHas');
|
||||||
|
|
||||||
|
$policy = new AddressBookAuthorizationPolicy($permissionService);
|
||||||
|
$decision = $policy->authorize(AddressBookAuthorizationPolicy::ABILITY_VIEW, [
|
||||||
|
'actor_user_id' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertForbiddenDecision($decision);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDeniedWithMissingActorId(): void
|
||||||
|
{
|
||||||
|
$permissionService = $this->createMock(PermissionService::class);
|
||||||
|
$permissionService->expects($this->never())->method('userHas');
|
||||||
|
|
||||||
|
$policy = new AddressBookAuthorizationPolicy($permissionService);
|
||||||
|
$decision = $policy->authorize(AddressBookAuthorizationPolicy::ABILITY_VIEW, []);
|
||||||
|
|
||||||
|
$this->assertForbiddenDecision($decision);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
web/modules/addressbook
Symbolic link
1
web/modules/addressbook
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
/var/www/modules/addressbook/web
|
||||||
Reference in New Issue
Block a user