Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Added

- Add translations management through Transifex
- Add "Allow specific languages" form access policy
- Add "GLPI system information" and "GLPI core or plugins concerned" question types

### Fixed
Expand Down
196 changes: 196 additions & 0 deletions src/Model/AccessControl/LanguageAccessControl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
<?php

/**
* -------------------------------------------------------------------------
* advancedforms plugin for GLPI
* -------------------------------------------------------------------------
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
* -------------------------------------------------------------------------
* @copyright Copyright (C) 2025 by the advancedforms plugin team.
* @license MIT https://opensource.org/licenses/mit-license.php
* @link https://github.com/pluginsGLPI/advancedforms
* -------------------------------------------------------------------------
*/

namespace GlpiPlugin\Advancedforms\Model\AccessControl;

use Dropdown;
use Glpi\Application\View\TemplateRenderer;
use Glpi\DBAL\JsonFieldInterface;
use Glpi\Form\AccessControl\AccessVote;
use Glpi\Form\AccessControl\ControlType\ControlTypeInterface;
use Glpi\Form\AccessControl\FormAccessControl;
use Glpi\Form\AccessControl\FormAccessParameters;
use Glpi\Form\Export\Context\DatabaseMapper;
use Glpi\Form\Export\Serializer\DynamicExportDataField;
use Glpi\Form\Form;
use Glpi\Locale\LanguageRegistry;
use InvalidArgumentException;
use Override;

final class LanguageAccessControl implements ControlTypeInterface
{
public const INPUT_NAME = '_advancedforms_languages';

#[Override]
public function getLabel(): string
{
return __('Allow specific languages', 'advancedforms');
}

#[Override]
public function getIcon(): string
{
return 'ti ti-language';
}

#[Override]
public function getConfig(): JsonFieldInterface
{
return new LanguageAccessControlConfig();
}

#[Override]
public function getWarnings(Form $form): array
{
foreach ($form->getAccessControls() as $control) {
if (
$control->fields['is_active']
&& $control->getStrategy() instanceof self
&& $this->getValidatedConfig($control->getConfig())->getLanguages() === []
) {
return [__('No language is selected in the "Allow specific languages" policy: it denies access to everyone.', 'advancedforms')];
}
}

return [];
}

#[Override]
public function renderConfigForm(FormAccessControl $access_control): string
{
$twig = TemplateRenderer::getInstance();
return $twig->render('@advancedforms/access_control/language.html.twig', [
'access_control' => $access_control,
'config' => $this->getValidatedConfig($access_control->getConfig()),
'languages' => Dropdown::getLanguages(),
'input_name' => self::INPUT_NAME,
'label' => $this->getLabel(),
]);
}

#[Override]
public function getWeight(): int
{
// After core policies (AllowList: 10, DirectAccess: 20).
return 30;
}

/** @param array<string, mixed> $input */
#[Override]
public function createConfigFromUserInput(array $input): LanguageAccessControlConfig
{
$values = $input[self::INPUT_NAME] ?? [];
// An empty multi-select is posted as an empty string.
if (!is_array($values)) {
$values = [];
}

// Trust boundary: keep only known GLPI language codes.
$languages = array_values(array_unique(array_filter(
$values,
fn($value): bool => is_string($value) && LanguageRegistry::has($value),
)));

return new LanguageAccessControlConfig($languages);
}

#[Override]
public function canAnswer(
Form $form,
JsonFieldInterface $config,
FormAccessParameters $parameters,
): AccessVote {
$config = $this->getValidatedConfig($config);

// Deny (not Abstain) so the language restricts every other policy:
// "All users" + this policy means "all users using a listed language".
$language = $_SESSION['glpilanguage'] ?? null;
if (!in_array($language, $config->getLanguages(), true)) {
return AccessVote::Deny;
}

// Next to other policies, only restrict: granting would let in users
// they refuse (e.g. outside the allowed group, or without the token).
if ($this->hasOtherActivePolicies($form)) {
return AccessVote::Abstain;
}

// Never grant anonymous users: login stays the job of other policies.
return $parameters->isAuthenticated() ? AccessVote::Grant : AccessVote::Abstain;
}

#[Override]
public function allowUnauthenticated(JsonFieldInterface $config): bool
{
return false;
}

#[Override]
public function exportDynamicConfig(JsonFieldInterface $config): DynamicExportDataField
{
// Language codes are not database ids: no requirement to map on import.
return new DynamicExportDataField($config->jsonSerialize(), []);
}

/**
* @param array<string, mixed> $config
* @return array<string, mixed>
*/
#[Override]
public static function prepareDynamicConfigDataForImport(
array $config,
DatabaseMapper $mapper,
): array {
return $config;
}

private function hasOtherActivePolicies(Form $form): bool
{
foreach ($form->getAccessControls() as $control) {
if ($control->fields['is_active'] && $control->fields['strategy'] !== self::class) {
return true;
}
}

return false;
}

private function getValidatedConfig(JsonFieldInterface $config): LanguageAccessControlConfig
{
if (!$config instanceof LanguageAccessControlConfig) {
throw new InvalidArgumentException('Invalid config class');
}

return $config;
}
}
68 changes: 68 additions & 0 deletions src/Model/AccessControl/LanguageAccessControlConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

/**
* -------------------------------------------------------------------------
* advancedforms plugin for GLPI
* -------------------------------------------------------------------------
*
* MIT License
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
* -------------------------------------------------------------------------
* @copyright Copyright (C) 2025 by the advancedforms plugin team.
* @license MIT https://opensource.org/licenses/mit-license.php
* @link https://github.com/pluginsGLPI/advancedforms
* -------------------------------------------------------------------------
*/

namespace GlpiPlugin\Advancedforms\Model\AccessControl;

use Glpi\DBAL\JsonFieldInterface;
use Override;

final readonly class LanguageAccessControlConfig implements JsonFieldInterface
{
public const KEY_LANGUAGES = 'languages';

/** @param string[] $languages GLPI language codes (see LanguageRegistry) */
public function __construct(
private array $languages = [],
) {}

/** @param array<string, mixed> $data */
#[Override]
public static function jsonDeserialize(array $data): self
{
$languages = $data[self::KEY_LANGUAGES] ?? [];
return new self(is_array($languages) ? array_values(array_filter($languages, is_string(...))) : []);
}

/** @return array{languages: string[]} */
#[Override]
public function jsonSerialize(): array
{
return [self::KEY_LANGUAGES => $this->languages];
}

/** @return string[] */
public function getLanguages(): array
{
return $this->languages;
}
}
20 changes: 20 additions & 0 deletions src/Service/InitManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@
namespace GlpiPlugin\Advancedforms\Service;

use Config;
use Glpi\Form\AccessControl\FormAccessControlManager;
use Glpi\Form\Destination\FormDestinationManager;
use Glpi\Form\Destination\FormDestinationTicket;
use Glpi\Form\Migration\TypesConversionMapper;
use Glpi\Form\QuestionType\QuestionTypesManager;
use Glpi\Plugin\Hooks;
use Glpi\Toolbox\SingletonTrait;
use GlpiPlugin\Advancedforms\Model\AccessControl\LanguageAccessControl;
use GlpiPlugin\Advancedforms\Model\Config\ConfigTab;
use GlpiPlugin\Advancedforms\Model\Destination\PreReservationField;
use GlpiPlugin\Advancedforms\Model\QuestionType\AdvancedCategory;
Expand All @@ -52,11 +54,15 @@ final class InitManager
{
use SingletonTrait;

/** Manager instance our access policies were registered on (core has no getter to check). */
private ?FormAccessControlManager $policies_registered_on = null;

public function init(): void
{
$this->registerConfiguration();
$this->registerPluginTypes();
$this->registerDestinationFields();
$this->registerAccessControlPolicies();
$this->registerTimelineHooks();
$this->registerNotifications();
$this->registerPurgeHooks();
Expand Down Expand Up @@ -140,6 +146,20 @@ private function registerDestinationFields(): void
);
}

private function registerAccessControlPolicies(): void
{
$manager = FormAccessControlManager::getInstance();

// init() may run multiple times per process; a duplicate would create
// duplicate policy rows on new forms.
if ($this->policies_registered_on === $manager) {
return;
}

$manager->registerPluginAccessControlPolicy(new LanguageAccessControl());
$this->policies_registered_on = $manager;
}

private function registerTimelineHooks(): void
{
global $PLUGIN_HOOKS;
Expand Down
13 changes: 13 additions & 0 deletions src/Service/InstallManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
use Config;
use DBConnection;
use Glpi\DBAL\QueryExpression;
use Glpi\Form\AccessControl\FormAccessControl;
use Glpi\Toolbox\SingletonTrait;
use GlpiPlugin\Advancedforms\Model\AccessControl\LanguageAccessControl;
use GlpiPlugin\Advancedforms\Model\TicketReservationRequest;
use Migration;
use Notification;
Expand Down Expand Up @@ -242,6 +244,7 @@ public function uninstall(): bool
$config->deleteByCriteria(['context' => 'advancedforms']);

$this->uninstallNotifications();
$this->uninstallAccessControlPolicies();

$table = TicketReservationRequest::getTable();
if ($DB->tableExists($table, false)) {
Expand All @@ -251,6 +254,16 @@ public function uninstall(): bool
return true;
}

/** Removes the plugin's access policies saved on forms (core ignores them once the plugin is gone). */
public function uninstallAccessControlPolicies(): void
{
global $DB;

$DB->delete(FormAccessControl::getTable(), [
'strategy' => LanguageAccessControl::class,
]);
}

/** Removes every notification artifact seeded by installNotifications(). */
private function uninstallNotifications(): void
{
Expand Down
Loading
Loading