Skip to content

Commit

Permalink
[FEATURE] Introduce translation access features for backend user groups
Browse files Browse the repository at this point in the history
Add new Items custom_options to the be_groups table, access to deepltranslate translate option.

Add unit and functional tests for AccessRegistry and Hook.
Introduce several unit tests for AccessRegistry, AllowedGlossarySyncAccess, and AllowedTranslateAccess.
Also, add functional tests for ButtonBarHook and TranslateHook to ensure correct behavior of glossary synchronization button and related access checks.

Add documentation for backend group access configuration
Included a new Index.rst file under Administration/Access to explain backend group permissions.
Added an image to illustrate backend access rights and updated main documentation index to reference the new page.
  • Loading branch information
NarkNiro committed Oct 11, 2024
1 parent 7ae23b0 commit a68a1d4
Show file tree
Hide file tree
Showing 30 changed files with 771 additions and 187 deletions.
36 changes: 36 additions & 0 deletions Classes/Access/AccessItemInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace WebVision\WvDeepltranslate\Access;

interface AccessItemInterface
{
/**
* Unique access identifier
*
* @return string
*/
public function getIdentifier(): string;

/**
* The title of the access
*
* @return string
*/
public function getTitle(): string;

/**
* A short description about the access
*
* @return string
*/
public function getDescription(): string;

/**
* The icon identifier for this access
*
* @return string
*/
public function getIconIdentifier(): string;
}
45 changes: 45 additions & 0 deletions Classes/Access/AccessRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace WebVision\WvDeepltranslate\Access;

use TYPO3\CMS\Core\SingletonInterface;

final class AccessRegistry implements SingletonInterface
{
/**
* @var AccessItemInterface[]
*/
private static $access = [];

public function addAccess(AccessItemInterface $accessItem): void
{
self::$access[] = $accessItem;
}

/**
* @return AccessItemInterface[]
*/
public function getAllAccess(): array
{
return self::$access;
}

public function getAccess(string $identifier): ?AccessItemInterface
{
foreach (self::$access as $accessItem) {
if ($accessItem->getIdentifier() === $identifier) {
return $accessItem;
}
}

return null;
}

public function hasAccess(string $identifier): bool
{
$object = $this->getAccess($identifier);
return $object !== null;
}
}
30 changes: 30 additions & 0 deletions Classes/Access/AllowedGlossarySyncAccess.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace WebVision\WvDeepltranslate\Access;

final class AllowedGlossarySyncAccess implements AccessItemInterface
{
public const ALLOWED_GLOSSARY_SYNC = 'deepltranslate:allowedGlossarySync';

public function getIdentifier(): string
{
return 'allowedGlossarySync';
}

public function getTitle(): string
{
return 'LLL:EXT:wv_deepltranslate/Resources/Private/Language/locallang.xlf:be_groups.deepltranslate_access.items.allowedGlossarySync.title';
}

public function getDescription(): string
{
return 'LLL:EXT:wv_deepltranslate/Resources/Private/Language/locallang.xlf:be_groups.deepltranslate_access.items.allowedGlossarySync.description';
}

public function getIconIdentifier(): string
{
return 'deepl-logo';
}
}
30 changes: 30 additions & 0 deletions Classes/Access/AllowedTranslateAccess.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace WebVision\WvDeepltranslate\Access;

final class AllowedTranslateAccess implements AccessItemInterface
{
public const ALLOWED_TRANSLATE_OPTION_VALUE = 'deepltranslate:translateAllowed';

public function getIdentifier(): string
{
return 'translateAllowed';
}

public function getTitle(): string
{
return 'LLL:EXT:wv_deepltranslate/Resources/Private/Language/locallang.xlf:be_groups.deepltranslate_access.items.translateAllowed.title';
}

public function getDescription(): string
{
return 'LLL:EXT:wv_deepltranslate/Resources/Private/Language/locallang.xlf:be_groups.deepltranslate_access.items.translateAllowed.description';
}

public function getIconIdentifier(): string
{
return 'deepl-logo';
}
}
7 changes: 6 additions & 1 deletion Classes/Event/Listener/GlossarySyncButtonProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
use WebVision\WvDeepltranslate\Access\AllowedGlossarySyncAccess;

final class GlossarySyncButtonProvider
{
Expand Down Expand Up @@ -58,7 +59,11 @@ public function __invoke(ModifyButtonBarEvent $event): void
return;
}

$parameters = $this->buildParamsArrayForListView($id);
if (!$this->getBackendUserAuthentication()->check('custom_options', AllowedGlossarySyncAccess::ALLOWED_GLOSSARY_SYNC)) {
return;
}

$parameters = $this->buildParamsArrayForListView((int)$id);
$title = (string)LocalizationUtility::translate(
'glossary.sync.button.all',
'wv_deepltranslate'
Expand Down
2 changes: 1 addition & 1 deletion Classes/Event/Listener/UsageToolBarEventListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public function __invoke(SystemInformationToolbarCollectorEvent $systemInformati
$usage = $this->usageService->getCurrentUsage();
// @todo Decide to handle empty UsageDetail later and add systeminformation with a default
// (no limit retrieved) instead of simply omitting it here now.
if($usage === null || $usage->character === null) {
if ($usage === null || $usage->character === null) {
return;
}
} catch (ApiKeyNotSetException $exception) {
Expand Down
79 changes: 44 additions & 35 deletions Classes/Hooks/ButtonBarHook.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
use WebVision\WvDeepltranslate\Access\AllowedGlossarySyncAccess;

class ButtonBarHook
{
Expand All @@ -28,46 +30,53 @@ public function getButtons(array $params, ButtonBar $buttonBar): array
$buttons = $params['buttons'];
$queryParams = $GLOBALS['TYPO3_REQUEST']->getQueryParams();

// we're inside a page
if (isset($queryParams['id'])) {
$page = BackendUtility::getRecord(
'pages',
$queryParams['id'],
'uid,module'
);
if (!isset($queryParams['id']) || $queryParams['id'] === '0') {
return $buttons;
}

if (
isset($page['module']) && $page['module'] === 'glossary'
&& $this->getBackendUserAuthentication()
->check('tables_modify', 'tx_wvdeepltranslate_glossaryentry')
) {
$parameters = $this->buildParamsArrayForListView($page['uid']);
$title = (string)LocalizationUtility::translate(
'glossary.sync.button.all',
'wv_deepltranslate'
);
// Style button
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$button = $buttonBar->makeLinkButton();
$button->setIcon($iconFactory->getIcon(
'apps-pagetree-folder-contains-glossary',
Icon::SIZE_SMALL
));
$button->setTitle($title);
$button->setShowLabelText(true);
/** @var array{uid: int, doktype: int, module: string} $page */
$page = BackendUtility::getRecord(
'pages',
$queryParams['id'],
'uid,module,doktype'
);

$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$uri = $uriBuilder->buildUriFromRoute(
'glossaryupdate',
$parameters
);
$button->setHref($uri);
if (
(int)$page['doktype'] !== PageRepository::DOKTYPE_SYSFOLDER
|| $page['module'] !== 'glossary'
) {
return $buttons;
}

// Register Button and position it
$buttons[ButtonBar::BUTTON_POSITION_LEFT][5][] = $button;
}
if (!$this->getBackendUserAuthentication()->check('custom_options', AllowedGlossarySyncAccess::ALLOWED_GLOSSARY_SYNC)) {
return $buttons;
}

$parameters = $this->buildParamsArrayForListView((int)$page['uid']);
$title = (string)LocalizationUtility::translate(
'glossary.sync.button.all',
'wv_deepltranslate'
);
// Style button
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
$button = $buttonBar->makeLinkButton();
$button->setIcon($iconFactory->getIcon(
'apps-pagetree-folder-contains-glossary',
Icon::SIZE_SMALL
));
$button->setTitle($title);
$button->setShowLabelText(true);

$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
$uri = $uriBuilder->buildUriFromRoute(
'glossaryupdate',
$parameters
);
$button->setHref($uri);

// Register Button and position it
$buttons[ButtonBar::BUTTON_POSITION_LEFT][5][] = $button;

return $buttons;
}

Expand Down
3 changes: 3 additions & 0 deletions Classes/Hooks/TCEmainHook.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\DataHandling\DataHandlerCheckModifyAccessListHookInterface;

/**
* ToDo: Rename this class to "AllowedTableForCommandHandler"
*/
class TCEmainHook implements DataHandlerCheckModifyAccessListHookInterface
{
/**
Expand Down
75 changes: 42 additions & 33 deletions Classes/Override/Core11/DatabaseRecordList.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace WebVision\WvDeepltranslate\Override\Core11;

use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use WebVision\WvDeepltranslate\Access\AllowedTranslateAccess;
use WebVision\WvDeepltranslate\Utility\DeeplBackendUtility;

/**
Expand All @@ -23,44 +25,51 @@ public function makeLocalizationPanel($table, $row, array $translations): string
{
$out = parent::makeLocalizationPanel($table, $row, $translations);

if ($out) {
if (!DeeplBackendUtility::isDeeplApiKeySet()) {
return $out;
}
if (!DeeplBackendUtility::isDeeplApiKeySet()) {
return $out;
}

// glossaries should not be auto translated by DeepL
if ($table === 'tx_wvdeepltranslate_glossaryentry') {
return $out;
}
// glossaries should not be auto translated by DeepL
if ($table === 'tx_wvdeepltranslate_glossaryentry') {
return $out;
}

$pageId = (int)($table === 'pages' ? $row['uid'] : $row['pid']);
// All records excluding pages
$possibleTranslations = $this->possibleTranslations;
if ($table === 'pages') {
// Calculate possible translations for pages
$possibleTranslations = array_map(static fn ($siteLanguage) => $siteLanguage->getLanguageId(), $this->languagesAllowedForUser);
$possibleTranslations = array_filter($possibleTranslations, static fn ($languageUid) => $languageUid > 0);
}
$languageInformation = $this->translateTools->getSystemLanguages($pageId);
foreach ($possibleTranslations as $lUid_OnPage) {
if ($this->isEditable($table)
&& !$this->isRecordDeletePlaceholder($row)
&& !isset($translations[$lUid_OnPage])
&& $this->getBackendUserAuthentication()->checkLanguageAccess($lUid_OnPage)
&& DeeplBackendUtility::checkCanBeTranslated($pageId, $lUid_OnPage)
) {
$out .= DeeplBackendUtility::buildTranslateButton(
$table,
$row['uid'],
$lUid_OnPage,
$this->listURL(),
$languageInformation[$lUid_OnPage]['title'],
$languageInformation[$lUid_OnPage]['flagIcon']
);
}
if (!$this->getBackendUserAuthentication()->check('custom_options', AllowedTranslateAccess::ALLOWED_TRANSLATE_OPTION_VALUE)) {
return $out;
}

$pageId = (int)($table === 'pages' ? $row['uid'] : $row['pid']);
// All records excluding pages
$possibleTranslations = $this->possibleTranslations;
if ($table === 'pages') {
// Calculate possible translations for pages
$possibleTranslations = array_map(static fn ($siteLanguage) => $siteLanguage->getLanguageId(), $this->languagesAllowedForUser);
$possibleTranslations = array_filter($possibleTranslations, static fn ($languageUid) => $languageUid > 0);
}
$languageInformation = $this->translateTools->getSystemLanguages($pageId);
foreach ($possibleTranslations as $lUid_OnPage) {
if ($this->isEditable($table)
&& !$this->isRecordDeletePlaceholder($row)
&& !isset($translations[$lUid_OnPage])
&& $this->getBackendUserAuthentication()->checkLanguageAccess($lUid_OnPage)
&& DeeplBackendUtility::checkCanBeTranslated($pageId, $lUid_OnPage)
) {
$out .= DeeplBackendUtility::buildTranslateButton(
$table,
$row['uid'],
$lUid_OnPage,
$this->listURL(),
$languageInformation[$lUid_OnPage]['title'],
$languageInformation[$lUid_OnPage]['flagIcon']
);
}
}

return $out;
}

protected function getBackendUserAuthentication(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
Loading

0 comments on commit a68a1d4

Please sign in to comment.