diff --git a/app/config/bootstrap.php b/app/config/bootstrap.php index 2d3d5f66..2d59fca2 100644 --- a/app/config/bootstrap.php +++ b/app/config/bootstrap.php @@ -241,4 +241,18 @@ /* * Define some constants */ -define('DEF_GLOBAL_SEARCH_LIMIT', 500); \ No newline at end of file +define("DEF_COMANAGE_CO_NAME", "COmanage"); +// Default invitation validity, in minutes (used in various places, should probably be moved elsewhere) +define("DEF_INV_VALIDITY", 1440); + +// Default window for reprovisioning on group validity change +define("DEF_GROUP_SYNC_WINDOW", 1440); + +// Default window for Garbage Collection +define("DEF_GARBAGE_COLLECT_INTERVAL", 1440); + +// Default global search limit +define("DEF_GLOBAL_SEARCH_LIMIT", 500); + +// Default Search block limit +define("DEF_SEARCH_LIMIT", 25); \ No newline at end of file diff --git a/app/config/schema/schema.json b/app/config/schema/schema.json index 8e1f52ca..240687bd 100644 --- a/app/config/schema/schema.json +++ b/app/config/schema/schema.json @@ -874,14 +874,16 @@ "application_states": { "columns": { "id": {}, + "co_id": {}, "person_id": {}, - "tag": { "type": "string", "size": 128 }, + "username": { "type": "string", "size": 512 }, + "tag": { "type": "string", "size": 128 }, "value": { "type": "string", "size": 256 } }, "indexes": { - "application_states_i1": { "columns": [ "person_id" ] } - }, - "changelog": false + "application_states_i1": { "columns": [ "co_id" ] }, + "application_states_i2": { "columns": [ "person_id" ] } + } } }, diff --git a/app/resources/locales/en_US/error.po b/app/resources/locales/en_US/error.po index 53a0a557..871a8b30 100644 --- a/app/resources/locales/en_US/error.po +++ b/app/resources/locales/en_US/error.po @@ -229,9 +229,6 @@ msgstr "Provided value is not an integer" msgid "Jobs.plugin.parameter.invalid" msgstr "Invalid parameter" -msgid "Jobs.plugin.parameter.required" -msgstr "Required parameter not provided" - msgid "Jobs.plugin.parameter.select" msgstr "Provided value is not a valid choice" @@ -289,6 +286,9 @@ msgstr "Notification status {0} is not a valid resolution" msgid "notprov" msgstr "{0} not provided" +msgid "notsupported" +msgstr "Not supported" + msgid "ordr.unique" msgstr "Each {0} must have a unique order" @@ -298,6 +298,9 @@ msgstr "Page number may not be larger than {0}" msgid "pagenum.nan" msgstr "Page number must be an integer" +msgid "Parameter.required" +msgstr "Required parameter '{0}' not provided" + msgid "perm" msgstr "Permission Denied" diff --git a/app/resources/locales/en_US/information.po b/app/resources/locales/en_US/information.po index ad449ff2..10f681d6 100644 --- a/app/resources/locales/en_US/information.po +++ b/app/resources/locales/en_US/information.po @@ -135,6 +135,9 @@ msgstr "This plugin requires no configuration." msgid "plugin.inactive" msgstr "Inactive" +msgid "state.info" +msgstr "{0} successfully {1} to {2}" + msgid "record" msgstr "Record" diff --git a/app/src/Controller/ApiV2Controller.php b/app/src/Controller/ApiV2Controller.php index add5bbb5..a9197be5 100644 --- a/app/src/Controller/ApiV2Controller.php +++ b/app/src/Controller/ApiV2Controller.php @@ -29,6 +29,7 @@ namespace App\Controller; +use Cake\Controller\Controller; use InvalidArgumentException; use Cake\Chronos\Chronos; use Cake\Http\Exception\BadRequestException; @@ -47,7 +48,6 @@ class ApiV2Controller extends AppController { * Perform Cake Controller initialization. * * @since COmanage Registry v5.0.0 - * @param array $config Configuration options passed to constructor */ public function initialize(): void { @@ -100,7 +100,7 @@ public function add() { $results[] = ['id' => $obj->id]; // Trigger provisioning, letting errors bubble up (AR-GMR-5) - if(method_exists($this->modelsName, "requestProvisioning")) { + if(method_exists($this->$modelsName, "requestProvisioning")) { $this->llog('rule', "AR-GMR-5 Requesting provisioning for $modelsName " . $obj->id); $table->requestProvisioning(id: $obj->id, context: ProvisioningContextEnum::Automatic); } @@ -121,7 +121,22 @@ public function add() { // Let the view render $this->render('/Standard/api/v2/json/add-edit'); } - + + /** + * beforeFilter callback. + * + * @param \Cake\Event\EventInterface $event Event. + * @return \Cake\Http\Response|null|void + */ + public function beforeFilter(\Cake\Event\EventInterface $event) + { + parent::beforeFilter($event); + + if ($this->request->is('ajax') && $this->request->is(['post', 'put'])) { + $this->FormProtection->setConfig('validate', false); + } + } + /** * Callback run prior to the request rendering. * diff --git a/app/src/Controller/AppController.php b/app/src/Controller/AppController.php index f9ef73af..c6778ebe 100644 --- a/app/src/Controller/AppController.php +++ b/app/src/Controller/AppController.php @@ -99,7 +99,7 @@ public function initialize(): void { * * In general, we don't need these protections for transactional API calls. */ - $this->loadComponent('Security'); + $this->loadComponent('FormProtection'); // CSRF Protection is enabled via in Middleware via Application.php. } @@ -141,8 +141,15 @@ public function beforeFilter(\Cake\Event\EventInterface $event) { // We need to populate this in beforeFilter (rather than beforeRender) // so it's available to CosController::select $this->populateAvailableCos(); + + // Get Person ID + if($this->RegistryAuth->getAuthenticatedUser() !== null && $this->getCOID() !== null) { + $this->set('vv_person_id', $this->RegistryAuth->getPersonId($this->getCOID())); + } } - + + $this->getAppPrefs(); + return parent::beforeFilter($event); } @@ -154,7 +161,6 @@ public function beforeFilter(\Cake\Event\EventInterface $event) { */ public function beforeRender(\Cake\Event\EventInterface $event) { - // $this->name = Models $modelsName = $this->name; // Views can also inspect the request object to determine the current @@ -169,7 +175,7 @@ public function beforeRender(\Cake\Event\EventInterface $event) { // Provide the user's application roles to the views. $this->set('vv_user_roles', $this->RegistryAuth->getApplicationUserRoles($this->getCOID())); } - + return parent::beforeRender($event); } @@ -368,6 +374,32 @@ protected function primaryLinkLookup(): void } } + /** + * Get a user's Application State + * - postcondition: Application Preferences variable set + * @since COmanage Registry v5.1.0 + */ + protected function getAppPrefs() { + $request = $this->getRequest(); + $session = $request->getSession(); + + $username = $session->read('Auth.external.user'); + $appPrefs = null; + + // Get preferences if we have an Auth.User.co_person_id + if(!empty($username)) { + $ApplicationStates = $this->fetchTable('ApplicationStates'); + $appPrefs = $ApplicationStates->retrieveAll( + username: $username, + // If we have not selected a CO yet, there will be no co_id + coid: $this->getCOID(), + personid: $this->viewBuilder()->getVar('vv_person_id') + ); + } + + $this->set('vv_app_prefs', $appPrefs); + } + /** * Collect information about the Standard Object's Primary Link, if set. * The $vv_primary_link view variable is also set. diff --git a/app/src/Controller/Component/RegistryAuthComponent.php b/app/src/Controller/Component/RegistryAuthComponent.php index e0854862..4c74ddf6 100644 --- a/app/src/Controller/Component/RegistryAuthComponent.php +++ b/app/src/Controller/Component/RegistryAuthComponent.php @@ -597,11 +597,11 @@ public function getAuthenticatedUser(): ?string { } /** - * Obtain permissions suitable for menu rendering, specifically by + * Get permissions suitable for menu rendering, specifically by * templates/element/menuMain.php. * * @since COmanage Registry v5.0.0 - * @param int $coId Current CO ID, if known + * @param int|null $coId Current CO ID, if known * @return array Array of permissions */ @@ -633,6 +633,7 @@ public function getMenuPermissions(?int $coId): array { * @since COmanage Registry v5.0.0 * @param int $coId CO ID * @throws RuntimeException + * @throws RecordNotFoundException */ public function getPersonID(int $coId): ?int { @@ -649,10 +650,11 @@ public function getPersonID(int $coId): ?int { $Identifiers = TableRegistry::getTableLocator()->get('Identifiers'); try { - return $Identifiers->lookupPersonByLogin($coId, $this->authenticatedUser); - } - catch(Cake\Datasource\Exception\RecordNotFoundException $e) { - return null; + $personId = (int)$Identifiers->lookupPersonByLogin($coId, $this->authenticatedUser); + } catch(RecordNotFoundException) { + $personId = null; + } finally { + return $personId; } } diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php index a580dca8..9390fa53 100644 --- a/app/src/Controller/StandardController.php +++ b/app/src/Controller/StandardController.php @@ -29,17 +29,21 @@ namespace App\Controller; +use App\Lib\Enum\ApplicationStateEnum; +use App\Lib\Traits\ApplicationStatesTrait; +use App\Lib\Traits\IndexQueryTrait; use Cake\Database\Expression\QueryExpression; use Cake\ORM\TableRegistry; -use InvalidArgumentException; -use \Cake\Http\Exception\BadRequestException; use Cake\Utility\Inflector; +use InvalidArgumentException; use \App\Lib\Enum\ProvisioningContextEnum; use \App\Lib\Enum\SuspendableStatusEnum; use \App\Lib\Util\{StringUtilities, FunctionUtilities}; +use \Cake\Http\Exception\BadRequestException; class StandardController extends AppController { - use \App\Lib\Traits\IndexQueryTrait; + use IndexQueryTrait; + use ApplicationStatesTrait; // Pagination defaults should be set in each controller public $pagination = []; @@ -628,7 +632,10 @@ public function index() { } // Fetch the data and paginate - $resultSet = $this->paginate($query); + $paginationLimit = $this->getValue(ApplicationStateEnum::PaginationLimit, DEF_SEARCH_LIMIT); + $resultSet = $this->paginate($query, [ + 'limit' => (int)$paginationLimit + ]); // Pass vars to the View $this->set($tableName, $resultSet); diff --git a/app/src/Lib/Enum/ApplicationStateEnum.php b/app/src/Lib/Enum/ApplicationStateEnum.php new file mode 100644 index 00000000..c5fce6a6 --- /dev/null +++ b/app/src/Lib/Enum/ApplicationStateEnum.php @@ -0,0 +1,38 @@ +.
+ * + * @param array $nameParts + * + * @return string + * @since COmanage Registry v5.1.0 + */ + public function constructComplexStateTag(array $nameParts): string + { + $nameParts = (new Collection($nameParts))->map(fn($item) => str_replace(' ', '_', $item))->toArray(); + return implode('.', $nameParts); + } + + /** + * @param string $stateTag + * @param string|int $defaultValue + * + * @return string + * @since COmanage Registry v5.1.0 + */ + public function getValue(string $stateTag, string|int $defaultValue): string + { + $vv_app_prefs = []; + if(method_exists($this, 'getView')) { + // View + $vv_app_prefs = $this->getView()?->get('vv_app_prefs'); + } elseif(method_exists($this, 'viewBuilder')) { + // Controller + $vv_app_prefs = $this->viewBuilder()?->getVar('vv_app_prefs'); + } + + $value = $defaultValue; + if(!empty($vv_app_prefs)) { + $appState = (collection($vv_app_prefs)) + ?->filter(fn ($value, $key) => $value->tag === $stateTag) + ?->first(); + $value = $appState?->value ?? $defaultValue; + } + + return (string)$value; + } + + /** + * @param string $stateTag + * + * @return string + * @since COmanage Registry v5.1.0 + */ + public function getId(string $stateTag): string + { + $vv_app_prefs = []; + if(method_exists($this, 'getView')) { + // View + $vv_app_prefs = $this->getView()?->get('vv_app_prefs'); + } elseif(method_exists($this, 'viewBuilder')) { + // Controller + $vv_app_prefs = $this->viewBuilder()?->getVar('vv_app_prefs'); + } + + $appStateId = ''; + if(!empty($vv_app_prefs)) { + $appState = (collection($vv_app_prefs)) + ?->filter(fn ($value, $key) => $value->tag === $stateTag) + ?->first(); + $appStateId = $appState?->id ?? ''; + } + + return (string)$appStateId; + } +} \ No newline at end of file diff --git a/app/src/Lib/Util/StringUtilities.php b/app/src/Lib/Util/StringUtilities.php index 2e43389d..b752b0fb 100644 --- a/app/src/Lib/Util/StringUtilities.php +++ b/app/src/Lib/Util/StringUtilities.php @@ -48,12 +48,13 @@ public static function classNameToForeignKey(string $className): string { /** * Construct the Column human-readable key * - * @since COmanage Registry v5.0.0 - * @param string $modelsName The name of the Model - * @param string $c The name of the column - * @param DateTimeZone $tz The timezone - * @param boolean $useCustomClMdlLabel Whether to use a custom `Model.column` field entry or rely on the default + * @param string $modelsName The name of the Model + * @param string $c The name of the column + * @param \DateTimeZone|null $tz The timezone + * @param boolean $useCustomClMdlLabel Whether to use a custom `Model.column` field entry or rely on the default + * * @return string Column friendly name + * @since COmanage Registry v5.0.0 */ public static function columnKey( @@ -62,11 +63,17 @@ public static function columnKey( \DateTimeZone $tz=null, bool $useCustomClMdlLabel=false ): string { - if(strpos($c, "_id", strlen($c)-3)) { - $postfix = ""; - if($c == "parent_id") { + if(strpos($c, '_id', strlen($c)-3)) { + $postfix = ''; + if($c === 'parent_id') { // This means we are working with a model that implements a Tree behavior - $postfix = " ({$modelsName})"; + // XXX If i add the parentheses before the name the the Inflecto::Humanize will not + // work as expected. Which means that + // this: parent_(cou) + // will become Parent (cou) and not Parent (Cou) + // because humanize looks for the first character after parentheses + // $postfix = " ({$modelsName})"; + $postfix = " {$modelsName}"; } // Key is of the form field_id, use .ct label instead diff --git a/app/src/Model/Table/ApplicationStatesTable.php b/app/src/Model/Table/ApplicationStatesTable.php index 24d260fa..77cf110a 100644 --- a/app/src/Model/Table/ApplicationStatesTable.php +++ b/app/src/Model/Table/ApplicationStatesTable.php @@ -29,10 +29,15 @@ namespace App\Model\Table; +use Cake\Database\Expression\QueryExpression; +use Cake\ORM\Query; use Cake\ORM\Table; use Cake\Validation\Validator; class ApplicationStatesTable extends Table { + use \App\Lib\Traits\ChangelogBehaviorTrait; + use \App\Lib\Traits\PermissionsTrait; + use \App\Lib\Traits\PrimaryLinkTrait; use \App\Lib\Traits\TableMetaTrait; use \App\Lib\Traits\ValidationTrait; @@ -46,14 +51,34 @@ class ApplicationStatesTable extends Table { public function initialize(array $config): void { // Timestamp behavior handles created/modified updates $this->addBehavior('Log'); + $this->addBehavior('Changelog'); $this->addBehavior('Timestamp'); $this->setTableType(\App\Lib\Enum\TableTypeEnum::Metadata); // Define associations + $this->belongsTo('Cos'); $this->belongsTo('People'); $this->setDisplayField('tag'); + $this->setPrimaryLink(['co_id', 'person_id']); + + + $this->setPermissions([ + // Actions that operate over an entity (ie: require an $id) + 'entity' => [ + 'delete' => true, + 'edit' => true, + 'unfreeze' => true, + 'view' => true + ], + // Actions that operate over a table (ie: do not require an $id) + 'table' => [ + 'add' => true, + 'index' => true, + 'deleted' => true + ], + ]); } /** @@ -74,7 +99,46 @@ public function validationDefault(Validator $validator): Validator { $this->registerStringValidation($validator, $schema, 'tag', true); $this->registerStringValidation($validator, $schema, 'value', false); - +// $validator->add('co_id', [ +// 'content' => ['rule' => 'isInteger'] +// ]); + $this->registerStringValidation($validator, $schema, 'username', false); + $validator->add('username', [ + // Username must have at least one non-space character to avoid + 'content' => ['rule' => ['notBlank'], + 'message' => __d('error', 'input.blank')] + ]); + return $validator; } + + /** + * Retrieve all Application State. + * + * @param string $username + * @param int|null $coid + * @param int|null $personid + * + * @return array List of application states or an empty array + * @since COmanage Registry v5.1.0 + */ + + public function retrieveAll(string $username, ?int $coid, ?int $personid): array { + $subquery = $this->find(); + $subquery = $subquery->where(['username' => $username]); + if ($coid !== null) { + $subquery = $subquery->where(['co_id' => $coid]); + } else { + $subquery = $subquery->where(fn(QueryExpression $exp, Query $query) => $exp->isNull('co_id')); + } + if ($personid !== null) { + $subquery = $subquery->where(['person_id' => $personid]); + } else { + $subquery = $subquery->where(fn(QueryExpression $exp, Query $query) => $exp->isNull('person_id')); + } + $subquery = $subquery->select($this); + $tags = $subquery->toArray(); + + return $tags ?? []; + } } \ No newline at end of file diff --git a/app/src/Model/Table/IdentifiersTable.php b/app/src/Model/Table/IdentifiersTable.php index 5c7dad46..b9579af3 100644 --- a/app/src/Model/Table/IdentifiersTable.php +++ b/app/src/Model/Table/IdentifiersTable.php @@ -205,8 +205,6 @@ public function localAfterSave(\Cake\Event\EventInterface $event, \Cake\Datasour * * @param int $typeId Identifier Type ID * @param string $identifier Identifier - * @param int|null $coId CO Id - * @param bool $login The identifier is login enabled * * @return int Person ID * @since COmanage Registry v5.0.0 diff --git a/app/src/Model/Table/JobsTable.php b/app/src/Model/Table/JobsTable.php index f1272101..5a74f03f 100644 --- a/app/src/Model/Table/JobsTable.php +++ b/app/src/Model/Table/JobsTable.php @@ -676,7 +676,7 @@ protected function validateJobParameters(string $plugin, int $coId, array $param if(isset($pluginParameters[$p]['required']) && $pluginParameters[$p]['required'] && empty($params[$p])) { - $ret[$p] = __d('error', 'Jobs.plugin.parameter.required'); + $ret[$p] = __d('error', 'parameter.required', [$p]); } } diff --git a/app/src/View/Helper/ApplicationStateHelper.php b/app/src/View/Helper/ApplicationStateHelper.php new file mode 100644 index 00000000..3168bd77 --- /dev/null +++ b/app/src/View/Helper/ApplicationStateHelper.php @@ -0,0 +1,38 @@ +Form->label($options['label'] ?? $key); print $this->Form->checkbox($key, [ - 'id' => str_replace("_", "-", $key), + 'id' => str_replace('_', '-', $key), 'class' => 'form-check-input', 'checked' => $query[$key] ?? 0, 'hiddenField' => false, diff --git a/app/templates/element/filter/dateSingle.php b/app/templates/element/filter/dateSingle.php index dc1aa11b..1230fad0 100644 --- a/app/templates/element/filter/dateSingle.php +++ b/app/templates/element/filter/dateSingle.php @@ -36,17 +36,14 @@ declare(strict_types = 1); -use Cake\Utility\Inflector; +use App\Lib\Enum\ApplicationStateEnum; use App\Lib\Enum\DateTypeEnum; +use Cake\Utility\Inflector; // $columns = the passed parameter $indexColumns as found in columns.inc; // provides overrides for labels and sorting. $columns = $vv_indexColumns; - -$wrapperCssClass = 'filter-active'; -if(empty($options['active'])) { - $wrapperCssClass = 'filter-inactive'; -} +$modelsName = $this->name; $label = Inflector::humanize( Inflector::underscore( @@ -54,6 +51,24 @@ ) ); +$propertyName = $this->ApplicationState->constructComplexStateTag( + [ApplicationStateEnum::SearchBlockOptions, $modelsName, $label] +); +$searchBlockOptionsState = $this->ApplicationState->getValue($propertyName, ''); + +// take into consideration the Application State +$wrapperCssClass = 'filter-inactive'; +if ( + ($searchBlockOptionsState === '' + && isset($options['active']) + && filter_var($options['active'], FILTER_VALIDATE_BOOLEAN)) + || + ($searchBlockOptionsState !== '' && filter_var($searchBlockOptionsState, FILTER_VALIDATE_BOOLEAN)) +) { + $wrapperCssClass = 'filter-active'; + $this->set('vv_active_search_filters_count', (int)$vv_active_search_filters_count+1); +} + ?>