diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..d47544e --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +gdpr.extension.support \ No newline at end of file diff --git a/Classes/Controller/AdministrationController.php b/Classes/Controller/AdministrationController.php new file mode 100644 index 0000000..619e7d9 --- /dev/null +++ b/Classes/Controller/AdministrationController.php @@ -0,0 +1,272 @@ +recordRepository = GeneralUtility::makeInstance(RecordRepository::class); + + if ($this->request->getControllerActionName() !== 'moduleNotEnabled' && (int)$this->getBackendUser()->user['gdpr_module_enable'] === 0) { + $this->redirect('moduleNotEnabled'); + } + } + + public function initializeView(ViewInterface $view) + { + $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); + $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Modal'); + $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/DateTimePicker'); + $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/jquery.clearable'); + $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Tooltip'); + + $dateFormat = ($GLOBALS['TYPO3_CONF_VARS']['SYS']['USdateFormat'] ? ['MM-DD-YYYY', 'HH:mm MM-DD-YYYY'] : ['DD-MM-YYYY', 'HH:mm DD-MM-YYYY']); + $pageRenderer->addInlineSetting('DateTimePicker', 'DateFormat', $dateFormat); + + $buttonBar = $this->view->getModuleTemplate()->getDocHeaderComponent()->getButtonBar(); + + $uriBuilder = $this->objectManager->get(UriBuilder::class); + $uriBuilder->setRequest($this->request); + + $buttonList = [ + [ + 'action' => 'index', + 'icon' => 'actions-system-list-open', + 'position' => ButtonBar::BUTTON_POSITION_LEFT, + 'group' => 1 + ], + [ + 'action' => 'search', + 'icon' => 'actions-search', + 'position' => ButtonBar::BUTTON_POSITION_LEFT, + 'group' => 1 + ], + [ + 'action' => 'formOverview', + 'icon' => 'ext-gdpr-form-overview', + 'position' => ButtonBar::BUTTON_POSITION_LEFT, + 'group' => 1 + ], + [ + 'action' => 'log', + 'icon' => 'actions-document-open-read-only', + 'position' => ButtonBar::BUTTON_POSITION_LEFT, + 'group' => 2 + ], + [ + 'action' => 'configuration', + 'icon' => 'actions-system-extension-configure', + 'position' => ButtonBar::BUTTON_POSITION_RIGHT, + 'group' => 1 + ], + [ + 'action' => 'help', + 'icon' => 'actions-system-help-open', + 'position' => ButtonBar::BUTTON_POSITION_RIGHT, + 'group' => 2 + ], + ]; + + $iconFactory = GeneralUtility::makeInstance(IconFactory::class); + + foreach ($buttonList as $buttonDefinition) { + $button = $buttonBar->makeLinkButton() + ->setIcon($iconFactory->getIcon($buttonDefinition['icon'], Icon::SIZE_SMALL)) + ->setTitle($buttonDefinition['icon']) + ->setHref($uriBuilder + ->reset() + ->setRequest($this->request)->uriFor($buttonDefinition['action'], [], 'Administration')); + $buttonBar->addButton($button, $buttonDefinition['position'], $buttonDefinition['group']); + } + } + + public function indexAction() + { + $tables = TableInformation::getAllEnabledTables(); + + $collectedRows = []; + foreach ($tables as $table) { + $collectedRows[$table]['statistics'] = $this->recordRepository->getStatisticOfTable($table); + $collectedRows[$table]['rows'] = $this->recordRepository->getRestrictedRows($table); + $collectedRows[$table]['meta'] = Table::getInstance($table); + } + + $this->view->assignMultiple([ + 'tables' => $tables, + 'restrictedData' => $collectedRows + ]); + } + + /** + * @param string $table + * @param int $id + */ + public function deleteAction(string $table, int $id) + { + $this->addFlashMessage('deleted'); + $this->addFlashMessage('Only demo mode'); + $this->forward('index'); + } + + /** + * @param string $table + * @param int $id + */ + public function reenableAction(string $table, int $id) + { + $this->addFlashMessage('reenabled'); + $this->addFlashMessage('Only demo mode'); + $this->forward('index'); + + } + + /** + * @param string $table + * @param int $id + */ + public function disableAction(string $table, int $id) + { + $this->addFlashMessage('disabled'); + $this->addFlashMessage('Only demo mode'); + $this->forward('index'); + + } + + /** + * @param string $table + * @param int $id + */ + public function randomizeAction(string $table, int $id) + { + $this->addFlashMessage(sprintf('The record with id %d from table "%s" has been randomized', $id, $table)); + $this->addFlashMessage('Only demo mode'); + $this->forward('index'); + } + + /** + * @param \GeorgRinger\Gdpr\Domain\Model\Dto\Search $search + */ + public function searchAction(Search $search = null) + { + $this->addFlashMessage('Only demo mode'); + $searchPerformed = false; + if ($search === null) { + $search = $this->objectManager->get(Search::class); + } else { + $searchPerformed = true; + } + + $this->view->assignMultiple([ + 'search' => $search, + 'searchPerformed' => $searchPerformed, + ]); + } + + /** + * @param \GeorgRinger\Gdpr\Domain\Model\Dto\LogFilter $filter + */ + public function logAction(LogFilter $filter = null) + { + $this->addFlashMessage('Only demo mode'); + if ($filter === null) { + $filter = $this->objectManager->get(LogFilter::class); + } + + $allTableNames = []; + foreach (TableInformation::getAllEnabledTables() as $tableName) { + $allTableNames[$tableName] = $tableName; + } + + $this->view->assignMultiple([ + 'allTableNames' => $allTableNames, + 'filter' => $filter, + ]); + } + + public function formOverviewAction() + { + $formRepository = GeneralUtility::makeInstance(FormRepository::class); + $this->view->assignMultiple([ + 'forms' => $formRepository->getAllForms(), + ]); + } + + /** + * @param string $type type + * @param int $formId form + * @param int $status status + */ + public function formStatusUpdateAction(string $type, int $formId, int $status) + { + $this->addFlashMessage(sprintf('The form of content element %s has been updated ', $formId)); + $this->addFlashMessage('Only demo mode'); + + $this->forward('formOverview'); + } + + + public function configurationAction() + { + $allTables = TableInformation::getAllEnabledTables(); + + $information = []; + foreach ($allTables as $tableName) { + $information[$tableName] = Table::getInstance($tableName); + } + + $this->view->assignMultiple([ + 'tables' => $information + ]); + } + + /** + * View which shows information if current user got no access + */ + public function moduleNotEnabledAction() + { + + } + + public function helpAction() + { + + } + + /** + * @return BackendUserAuthentication + */ + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Database/Query/Restriction/GdprOnlyRestriction.php b/Classes/Database/Query/Restriction/GdprOnlyRestriction.php new file mode 100644 index 0000000..2eb0955 --- /dev/null +++ b/Classes/Database/Query/Restriction/GdprOnlyRestriction.php @@ -0,0 +1,56 @@ + $tableName) { + if (TableInformation::isTableEnabled($tableName)) { + $table = Table::getInstance($tableName); + $restrictionFieldName = $table->getGdprRestrictionField() ?? null; + if (!empty($restrictionFieldName)) { + $constraints[] = $expressionBuilder->eq( + $tableAlias . '.' . $restrictionFieldName, + 1 + ); + } + } + } + return $expressionBuilder->andX(...$constraints); + } +} diff --git a/Classes/Database/Query/Restriction/GdprRandomizedRestriction.php b/Classes/Database/Query/Restriction/GdprRandomizedRestriction.php new file mode 100644 index 0000000..9a568c2 --- /dev/null +++ b/Classes/Database/Query/Restriction/GdprRandomizedRestriction.php @@ -0,0 +1,56 @@ + $tableName) { + if (TableInformation::isTableEnabled($tableName)) { + $table = Table::getInstance($tableName); + $restrictionFieldName = $table->getGdprRandomizedField() ?? null; + if (!empty($restrictionFieldName)) { + $constraints[] = $expressionBuilder->eq( + $tableAlias . '.' . $restrictionFieldName, + 0 + ); + } + } + } + return $expressionBuilder->andX(...$constraints); + } +} diff --git a/Classes/Database/Query/Restriction/GdprRestriction.php b/Classes/Database/Query/Restriction/GdprRestriction.php new file mode 100644 index 0000000..f8b2b3e --- /dev/null +++ b/Classes/Database/Query/Restriction/GdprRestriction.php @@ -0,0 +1,56 @@ + $tableName) { + if (TableInformation::isTableEnabled($tableName)) { + $table = Table::getInstance($tableName); + $restrictionFieldName = $table->getGdprRestrictionField() ?? null; + if (!empty($restrictionFieldName)) { + $constraints[] = $expressionBuilder->eq( + $tableAlias . '.' . $restrictionFieldName, + 0 + ); + } + } + } + return $expressionBuilder->andX(...$constraints); + } +} diff --git a/Classes/Domain/Model/Dto/ExtensionConfiguration.php b/Classes/Domain/Model/Dto/ExtensionConfiguration.php new file mode 100644 index 0000000..97c5771 --- /dev/null +++ b/Classes/Domain/Model/Dto/ExtensionConfiguration.php @@ -0,0 +1,48 @@ + false]); + if (!empty($settings)) { + $this->randomizerLocale = $settings['randomizerLocale']; + $this->overloadMediaRenderer = isset($settings['overloadMediaRenderer']) ? (bool)$settings['overloadMediaRenderer'] : true; + } + } + + /** + * @return string + */ + public function getRandomizerLocale(): string + { + return $this->randomizerLocale; + } + + /** + * @return bool + */ + public function getOverloadMediaRenderer(): bool + { + return $this->overloadMediaRenderer; + } + + public static function getInstance(): ExtensionConfiguration + { + return GeneralUtility::makeInstance(__CLASS__); + } + +} diff --git a/Classes/Domain/Model/Dto/LogFilter.php b/Classes/Domain/Model/Dto/LogFilter.php new file mode 100644 index 0000000..e65fbd8 --- /dev/null +++ b/Classes/Domain/Model/Dto/LogFilter.php @@ -0,0 +1,109 @@ +tableName; + } + + /** + * @param string $tableName + */ + public function setTableName(string $tableName) + { + $this->tableName = $tableName; + } + + /** + * @return int + */ + public function getStatus(): int + { + return $this->status; + } + + /** + * @param int $status + */ + public function setStatus($status = 0) + { + $this->status = (int)$status; + } + + /** + * @return string + */ + public function getDateFrom(): string + { + return $this->dateFrom; + } + + /** + * @param string $dateFrom + */ + public function setDateFrom(string $dateFrom) + { + $this->dateFrom = $dateFrom; + } + + /** + * @return string + */ + public function getDateTo(): string + { + return $this->dateTo; + } + + /** + * @param string $dateTo + */ + public function setDateTo(string $dateTo) + { + $this->dateTo = $dateTo; + } + + /** + * @return int + */ + public function getLimit(): int + { + return MathUtility::forceIntegerInRange($this->limit, 10, self::MAX_LIMIT, self::DEFAULT_LIMIT); + } + + /** + * @param int $limit + */ + public function setLimit($limit) + { + $this->limit = (int)$limit; + } + +} diff --git a/Classes/Domain/Model/Dto/Search.php b/Classes/Domain/Model/Dto/Search.php new file mode 100644 index 0000000..56847bb --- /dev/null +++ b/Classes/Domain/Model/Dto/Search.php @@ -0,0 +1,48 @@ +searchWord; + } + + /** + * @param string $searchWord + */ + public function setSearchWord(string $searchWord) + { + $this->searchWord = $searchWord; + } + + /** + * @return bool + */ + public function isSensitiveOnly(): bool + { + return (bool)$this->sensitiveOnly; + } + + /** + * @param bool $sensitiveOnly + */ + public function setSensitiveOnly(bool $sensitiveOnly) + { + $this->sensitiveOnly = $sensitiveOnly; + } + + +} diff --git a/Classes/Domain/Model/Dto/Table.php b/Classes/Domain/Model/Dto/Table.php new file mode 100644 index 0000000..01a513e --- /dev/null +++ b/Classes/Domain/Model/Dto/Table.php @@ -0,0 +1,155 @@ +tableName = $tableName; + $this->title = $tcaCtrl['title']; + $this->titleField = $tcaCtrl['label']; + $this->deletedField = isset($tcaCtrl['delete']) ? $tcaCtrl['delete'] : ''; + $this->titleLabel = $GLOBALS['TCA'][$tableName]['columns'][$this->titleField]['label']; + $this->gdprRestrictionField = $tcaCtrl['gdpr']['restriction_field'] ?: ''; + $this->gdprRandomizedField = $tcaCtrl['gdpr']['randomized_field'] ?: ''; + $this->gdprRandomizeMapping = $tcaCtrl['gdpr']['randomize_mapping'] ?: []; + $this->gdprRandomizedDateField = $tcaCtrl['gdpr']['randomize_datefield'] ?: ''; + $this->gdprExpirePeriod = $tcaCtrl['gdpr']['randomize_expirePeriod'] ?: 365; + } + + /** + * @param string $tableName + * @return Table + */ + public static function getInstance(string $tableName): self + { + return GeneralUtility::makeInstance(__CLASS__, $tableName); + } + + /** + * @return string + */ + public function getTableName(): string + { + return $this->tableName; + } + + /** + * @return string + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * @return string + */ + public function getTitleField(): string + { + return $this->titleField; + } + + /** + * @return string + */ + public function getDeletedField(): string + { + return $this->deletedField; + } + + /** + * @return string + */ + public function getTitleLabel(): string + { + return $this->titleLabel; + } + + /** + * @return string + */ + public function getGdprRestrictionField(): string + { + return $this->gdprRestrictionField; + } + + /** + * @return string + */ + public function getGdprRandomizedField(): string + { + return $this->gdprRandomizedField; + } + + /** + * @return array + */ + public function getGdprRandomizeMapping(): array + { + return $this->gdprRandomizeMapping; + } + + /** + * @return string + */ + public function getGdprRandomizedDateField(): string + { + return $this->gdprRandomizedDateField; + } + + /** + * @return int + */ + public function getGdprExpirePeriod(): int + { + return $this->gdprExpirePeriod; + } + + public function randomizationEnabled(): bool + { + return !empty($this->gdprRandomizedField) && !empty($this->gdprRandomizeMapping); + } + +} diff --git a/Classes/Domain/Repository/BaseRepository.php b/Classes/Domain/Repository/BaseRepository.php new file mode 100644 index 0000000..7dab465 --- /dev/null +++ b/Classes/Domain/Repository/BaseRepository.php @@ -0,0 +1,24 @@ +getConnectionForTable($table); + } + + protected function getQueryBuilder(string $table): QueryBuilder + { + return $this->getConnection($table)->createQueryBuilder(); + } + +} diff --git a/Classes/Domain/Repository/FormRepository.php b/Classes/Domain/Repository/FormRepository.php new file mode 100644 index 0000000..3167e07 --- /dev/null +++ b/Classes/Domain/Repository/FormRepository.php @@ -0,0 +1,135 @@ +uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + $this->registry = GeneralUtility::makeInstance(Registry::class); + } + + public function getAllForms() + { + $data = [ + 'ext-form' => $this->getFormForms(), + 'ext-powermail' => $this->getPowermailForms(), +// 'ext-formhandler' => $this->getFormhandlerForms() + ]; + + return $data; + } + + protected function getPowermailForms() + { + $queryBuilder = $this->getQueryBuilder('tt_content'); + $rows = $queryBuilder + ->select('*') + ->from('tt_content') + ->where( + $queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('list', \PDO::PARAM_STR)), + $queryBuilder->expr()->eq('list_type', $queryBuilder->createNamedParameter('powermail_pi1', \PDO::PARAM_STR)) + ) + ->execute() + ->fetchAll(); + + $rows = $this->enhanceRows('powermail', $rows); + return $rows; + } + + protected function getFormhandlerForms() + { + return []; + } + + protected function getFormForms() + { + $queryBuilder = $this->getQueryBuilder('tt_content'); + $rows = $queryBuilder + ->select('*') + ->from('tt_content') + ->where( + $queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter('form_formframework', \PDO::PARAM_STR)) + ) + ->execute() + ->fetchAll(); + + $rows = $this->enhanceRows('form', $rows); + return $rows; + } + + protected function enhanceRows(string $type, array $rows) + { + foreach ($rows as $key => $row) { + $status = $this->registry->get(self::REGISTRY_NAMESPACE, $this->getRegistryKey($type, $row['uid']), false); + $rows[$key]['_meta'] = [ + 'type' => $type, + 'isValidated' => $status, + 'links' => [ + 'editContentElement' => $this->createEditUri('tt_content', $row['uid']) + ], + 'page' => BackendUtility::getRecord('pages', $row['pid']), + 'path' => BackendUtility::getRecordPath($row['pid'], $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW), 1000) + ]; + } + + return $rows; + } + + protected function getRegistryKey(string $type, int $id): string + { + return sprintf('%s-%s', $type, $id); + } + + protected function createEditUri(string $table, int $id): string + { + $urlParameters = [ + 'edit' => [ + $table => [ + $id => 'edit' + ] + ], + 'returnUrl' => GeneralUtility::getIndpEnv('REQUEST_URI') + ]; + return (string)$this->uriBuilder->buildUriFromRoute('record_edit', $urlParameters); + } + + /** + * Returns the current BE user. + * + * @return BackendUserAuthentication + */ + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} diff --git a/Classes/Domain/Repository/LogRepository.php b/Classes/Domain/Repository/LogRepository.php new file mode 100644 index 0000000..5000039 --- /dev/null +++ b/Classes/Domain/Repository/LogRepository.php @@ -0,0 +1,41 @@ +getTimestamp(); + } else { + // try to check strtotime + $timeFromString = strtotime($timeInput); + + if ($timeFromString) { + $timeLimit = $timeFromString; + } + } + } + return $timeLimit; + } + +} diff --git a/Classes/Domain/Repository/RecordRepository.php b/Classes/Domain/Repository/RecordRepository.php new file mode 100644 index 0000000..8d2af15 --- /dev/null +++ b/Classes/Domain/Repository/RecordRepository.php @@ -0,0 +1,71 @@ +logger = GeneralUtility::makeInstance(LogManager::class); + } + + public function getRestrictedRows(string $table): array + { + $tableInformation = Table::getInstance($table); + + $queryBuilder = $this->getQueryBuilder($table); + $queryBuilder->getRestrictions() + ->removeByType(GdprRestriction::class) + ->removeByType(HiddenRestriction::class) + ->add(GeneralUtility::makeInstance(GdprOnlyRestriction::class)); + + return $queryBuilder + ->select('uid', $tableInformation->getTitleField(), $tableInformation->getGdprRestrictionField()) + ->from($table) + ->execute() + ->fetchAll(); + } + + public function getStatisticOfTable(string $table): array + { + $restrictionFieldName = Table::getInstance($table)->getGdprRestrictionField(); + + $queryBuilder = $this->getQueryBuilder($table); + $queryBuilder->getRestrictions() + ->removeAll() + ->removeByType(GdprRestriction::class); + + $rows = $queryBuilder + ->select($restrictionFieldName) + ->selectLiteral('count(' . $restrictionFieldName . ') as count') + ->from($table) + ->groupBy($restrictionFieldName) + ->execute() + ->fetchAll(); + + return [ + 'restricted' => (int)$rows[1]['count'], + 'public' => (int)$rows[0]['count'] + ]; + } + + + + +} diff --git a/Classes/Hooks/DataHandlerHook.php b/Classes/Hooks/DataHandlerHook.php new file mode 100644 index 0000000..3ee7aeb --- /dev/null +++ b/Classes/Hooks/DataHandlerHook.php @@ -0,0 +1,56 @@ +logManager = GeneralUtility::makeInstance(LogManager::class); + } + + /** + * Hooks into TCE Main and watches all record creations and updates. If it + * detects that the new/updated record belongs to a table configured for + * indexing through Solr, we add the record to the index queue. + * + * @param string $status Status of the current operation, 'new' or 'update' + * @param string $table The table the record belongs to + * @param mixed $uid The record's uid, [integer] or [string] (like 'NEW...') + * @param array $fields The record's data + * @param DataHandler $tceMain TYPO3 Core Engine parent object + * @return void + */ + public function processDatamap_afterDatabaseOperations( + $status, + $table, + $uid, + array $fields, + DataHandler $tceMain + ) + { + if (TableInformation::isTableEnabled($table)) { + $tableInformation = Table::getInstance($table); + + $restrictionField = $tableInformation->getGdprRestrictionField(); + if ($restrictionField && isset($fields[$restrictionField])) { + if ($fields[$restrictionField]) { + $this->logManager->log($table, $uid, LogManager::STATUS_RESTRICT); + } else { + $this->logManager->log($table, $uid, LogManager::STATUS_REENABLE); + } + } + } + } +} diff --git a/Classes/Hooks/QueryBuilderHook.php b/Classes/Hooks/QueryBuilderHook.php new file mode 100644 index 0000000..0c422b1 --- /dev/null +++ b/Classes/Hooks/QueryBuilderHook.php @@ -0,0 +1,21 @@ +getRestrictionContainer(); + $fo->add(GeneralUtility::makeInstance(GdprRestriction::class)); + } +} \ No newline at end of file diff --git a/Classes/Log/LogManager.php b/Classes/Log/LogManager.php new file mode 100644 index 0000000..91e071e --- /dev/null +++ b/Classes/Log/LogManager.php @@ -0,0 +1,72 @@ + 5) { + throw new \UnexpectedValueException(sprintf('Value "%s" is invalid, use one of [1,2,3,4]', $status)); + } + $backendUser = $this->getBackendUser()->user; + $fieldValues = [ + 'tstamp' => $GLOBALS['EXEC_TIME'], + 'table_name' => $tableName, + 'record_id' => $id, + 'status' => $status, + ]; + if (is_array($backendUser)) { + $fieldValues['user'] = $backendUser['uid']; + $fieldValues['user_name_text'] = $this->getUsernameText($backendUser); + } else { + // @todo CLI? + $fieldValues['user'] = -1; + $fieldValues['user_name_text'] = 'CLI'; + } + + + GeneralUtility::makeInstance(ConnectionPool::class) + ->getConnectionForTable('tx_gdpr_domain_model_log') + ->insert('tx_gdpr_domain_model_log', $fieldValues); + } + + protected function getUsernameText(array $user): string + { + $data = [ + 'username' => $user['username'], + 'realName' => $user['realName'], + 'email' => $user['email'] + ]; + return json_encode($data); + } + + /** + * @return BackendUserAuthentication + */ + protected function getBackendUser(): BackendUserAuthentication + { + return $GLOBALS['BE_USER']; + } +} \ No newline at end of file diff --git a/Classes/Report/GdprStatusReport.php b/Classes/Report/GdprStatusReport.php new file mode 100644 index 0000000..6b3c4ed --- /dev/null +++ b/Classes/Report/GdprStatusReport.php @@ -0,0 +1,71 @@ + $this->getStatusOfGdpr(), + ]; + return $statuses; + } + + protected function getStatusOfGdpr(): ReportStatus + { + $recordRepository = GeneralUtility::makeInstance(RecordRepository::class); + + $messages = []; + $actionRequired = false; + $status = ReportStatus::OK; + foreach (TableInformation::getAllEnabledTables() as $table) { + $statistic = $recordRepository->getStatisticOfTable($table); + $countAction = $statistic['restricted']; + $countNoAction = $statistic['public']; + $sum = $countAction + $countNoAction; + if ($countAction > 0) { + $status = ReportStatus::WARNING; + $messages[] = sprintf('In Table "%s" are %s rows total, %s need an action!', $table, $sum, $countAction); + } else { + $messages[] = sprintf('In Table "%s" are %s rows total, no action required.', $table, $sum); + } + } + + $message = implode('
', $messages); + + + return GeneralUtility::makeInstance( + ReportStatus::class, + 'GDPR Handling', + 'Some information regarding GDPR related sensible information:', + $message, + $status + ); + } + + /** + * @return LanguageService + */ + protected function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/Classes/Service/TableInformation.php b/Classes/Service/TableInformation.php new file mode 100644 index 0000000..8c431fa --- /dev/null +++ b/Classes/Service/TableInformation.php @@ -0,0 +1,40 @@ + $tca['ctrl']['title'], + 'labelField' => $tca['ctrl']['label'] + ]; + } +} diff --git a/Classes/Service/Tca.php b/Classes/Service/Tca.php new file mode 100644 index 0000000..41ee161 --- /dev/null +++ b/Classes/Service/Tca.php @@ -0,0 +1,86 @@ +tableName = $tableName; + + $GLOBALS['TCA'][$this->tableName]['ctrl']['gdpr'] = [ + 'enabled' => true + ]; + } + + public static function getInstance(string $tableName): Tca + { + return GeneralUtility::makeInstance(self::class, $tableName); + } + + public function addRestriction(string $fieldName) + { + $GLOBALS['TCA'][$this->tableName]['ctrl']['gdpr']['restriction_field'] = $fieldName; + $this->fields[$fieldName] = [ + 'label' => 'LLL:EXT:gdpr/Resources/Private/Language/locallang.xlf:field.restriction_field', + 'exclude' => true, + 'config' => [ + 'type' => 'check', + 'default' => 0 + ] + ]; + + return $this; + } + + public function addRandomization(string $fieldName, array $randomizationOptions) + { + $GLOBALS['TCA'][$this->tableName]['ctrl']['gdpr']['randomized_field'] = $fieldName; + $GLOBALS['TCA'][$this->tableName]['ctrl']['gdpr']['randomize_mapping'] = $randomizationOptions['mapping']; + $GLOBALS['TCA'][$this->tableName]['ctrl']['gdpr']['randomize_datefield'] = $randomizationOptions['dateField']; + $GLOBALS['TCA'][$this->tableName]['ctrl']['gdpr']['randomize_expirePeriod'] = $randomizationOptions['expirePeriod']; + + $this->fields[$fieldName] = [ + 'label' => 'LLL:EXT:gdpr/Resources/Private/Language/locallang.xlf:field.randomized_field', + 'exclude' => true, + 'config' => [ + 'type' => 'check', + 'readOnly' => true, + 'default' => 0 + ] + ]; + return $this; + } + + public function add(string $position) + { + if (!empty($this->fields)) { + ExtensionManagementUtility::addTCAcolumns($this->tableName, $this->fields); + $GLOBALS['TCA'][$this->tableName]['palettes']['paletteGdpr'] = [ + 'showitem' => implode(', ', array_keys($this->fields)), + ]; + + ExtensionManagementUtility::addToAllTCAtypes( + $this->tableName, + '--palette--;LLL:EXT:gdpr/Resources/Private/Language/locallang.xlf:palette;paletteGdpr,', + '', + $position + ); + } + } + +} diff --git a/Classes/Xclass/DefaultRestrictionContainer.php b/Classes/Xclass/DefaultRestrictionContainer.php new file mode 100644 index 0000000..050b355 --- /dev/null +++ b/Classes/Xclass/DefaultRestrictionContainer.php @@ -0,0 +1,30 @@ +addGdprConstraints(); + } + + public function removeAll() + { + parent::removeAll(); + $this->addGdprConstraints(); + + return $this; + } + + protected function addGdprConstraints() + { + $this->add(GeneralUtility::makeInstance(GdprRestriction::class)); + } +} diff --git a/Classes/Xclass/FrontendRestrictionContainer.php b/Classes/Xclass/FrontendRestrictionContainer.php new file mode 100644 index 0000000..68e57cf --- /dev/null +++ b/Classes/Xclass/FrontendRestrictionContainer.php @@ -0,0 +1,30 @@ +addGdprConstraints(); + } + + public function removeAll() + { + parent::removeAll(); + $this->addGdprConstraints(); + + return $this; + } + + protected function addGdprConstraints() + { + $this->add(GeneralUtility::makeInstance(GdprRestriction::class)); + } +} diff --git a/Configuration/Commands.php b/Configuration/Commands.php new file mode 100644 index 0000000..c3f32a9 --- /dev/null +++ b/Configuration/Commands.php @@ -0,0 +1,17 @@ + [ + 'class' => \GeorgRinger\Gdpr\Command\RandomizeCommand::class + ], + 'gdpr:anonymizeIp' => [ + 'class' => \GeorgRinger\Gdpr\Command\AnonymizeIpCommand::class + ] +]; diff --git a/Configuration/TCA/Overrides/be_users.php b/Configuration/TCA/Overrides/be_users.php new file mode 100644 index 0000000..b3f63db --- /dev/null +++ b/Configuration/TCA/Overrides/be_users.php @@ -0,0 +1,41 @@ + [ + 'label' => 'LLL:EXT:gdpr/Resources/Private/Language/locallang.xlf:be_users.gdpr_module_enable', + 'config' => [ + 'type' => 'check', + 'default' => 0 + ] + ] +]; + +if (version_compare(TYPO3_branch, '9.2', '>=')) { + $fields['gdpr_module_enable']['config'] = [ + 'type' => 'check', + 'renderType' => 'checkboxLabeledToggle', + 'items' => [ + [ + 0 => '', + 1 => '', + 'labelChecked' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.enabled', + 'labelUnchecked' => 'LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.disabled' + ], + ], + ]; +} + +\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('be_users', $fields); +\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes( + 'be_users', + 'gdpr_module_enable', + '', + 'before:disableIPlock' +); + +$tca = \GeorgRinger\Gdpr\Service\Tca::getInstance('be_users'); +$tca + ->addRestriction('gdpr_restricted') + ->add('after:disable'); \ No newline at end of file diff --git a/Configuration/TCA/Overrides/fe_users.php b/Configuration/TCA/Overrides/fe_users.php new file mode 100644 index 0000000..5669480 --- /dev/null +++ b/Configuration/TCA/Overrides/fe_users.php @@ -0,0 +1,23 @@ +addRestriction('gdpr_restricted') + ->addRandomization('gdpr_randomized', [ + 'dateField' => 'tstamp', + 'expirePeriod' => 360, + 'mapping' => [ + 'username' => 'userName', + 'email' => 'email', + 'password' => 'password', + 'zip' => 'postcode', + 'address' => 'address', + 'city' => 'city', + 'first_name' => 'firstName', + 'last_name' => 'lastName', + 'telephone' => 'e164PhoneNumber', + 'fax' => 'e164PhoneNumber', + ] + ]) + ->add('after:disable'); \ No newline at end of file diff --git a/Documentation/Thoughts.rst b/Documentation/Thoughts.rst deleted file mode 100644 index 1afdf06..0000000 --- a/Documentation/Thoughts.rst +++ /dev/null @@ -1,47 +0,0 @@ -Thoughts on GDPR API --------------------- - -Goals -^^^^^ - -- Make default TYPO3 core GDPR compliant -- Enable website owners to be GDPR compliant -- Empower developers to create GDPR compliant extensions -- Enable integrators to configure extensions to be GDPR compliant - -Terms/Usages -^^^^^^^^^^^ -- Anonymization can happen: - - On persisting ☐ - - On recurring basis (scheduler) ✔ - - On manual interaction ✔ -- Randomization can happen: - - On recurring basis (scheduler) ✔ - - On manual interaction ✔ -- Hiding data can happen - - On manual interaction ✔ -- Data Removal can happen: - - On recurring basis (scheduler) ✔ - - On manual interaction ✔ -- Data Retrieval can happen: - - On manual interaction ✔ - -Stakeholders -^^^^^^^^^^^^ -- TYPO3 Core - - IP addresses need to be anonymized - - Logs - - Indexed_search - - Give overview over sensitive data & place for interaction (removal, retrieval, ...) - - Persisting “User last logged in” needs to be configurable -- Website owner - - As a website owner I want to be able to remove all personal data collected for a person. - - As a website owner I want to be able to extract all personal data collected for a person. - - As a website owner I want to be able to randomize all personal data collected for a person. - - As a website owner I want my data privacy guideline to be automatically created based on my current API configuration. - - As a website owner I want to know who has access to the data - - As a website owner I want to be able to decide on a per-user basis which backend user can see, access, randomize, export or release personalized data. -- Developers - - As a developer I want to be able to pipe my persistence through a dedicated API that can be configured to follow GDPR rules -- Integrators - - As an integrator I want to be able to configure each API usage to fit my current requirements. diff --git a/Documentation/todos.md b/Documentation/todos.md deleted file mode 100644 index 0f5b13e..0000000 --- a/Documentation/todos.md +++ /dev/null @@ -1,12 +0,0 @@ - -## Todos / Ideas - -- Randomization: - - API for mass randomization, e.g. all records of a page or - - Backend module -- Export relations -- Translations -- Support for TYPO3 CMS 8 - - Think about dropping `$GLOBALS['TYPO3_DB']` -- Tests -- Faker as phar file \ No newline at end of file diff --git a/FAQ.md b/FAQ.md new file mode 100644 index 0000000..8e1b03d --- /dev/null +++ b/FAQ.md @@ -0,0 +1,24 @@ +[<- back](Readme.md) + +# FAQ + +## Why not add the GDPR extension to the core? + +It is a lot easier to maintain an extension. +Releases can happen a lot more often and it is possible to support 8.7 with new features which would not be possible if it would be in the core. + +## What about support for 7.6 or 6.2? + +Certain features like hiding records everywhere is not possible in 7.6 because there is no *Doctrine DBAL*. + +It would be possible to make a separate version for 7.6 or even for 6.2 covering the following features: + +- Randomization +- Form overview + +If you are interested in this version and can help sponsoring it, please contact me + +## What about the GDPR features in the core? + +All GDPR features which have been added to the latest versions of TYPO3 core have there beginning in this extension. +Those have been developed and tested as feature in the extension first and then have been moved to the core. \ No newline at end of file diff --git a/Features/FormOverview.md b/Features/FormOverview.md new file mode 100644 index 0000000..0bfb796 --- /dev/null +++ b/Features/FormOverview.md @@ -0,0 +1,16 @@ +[<- back](../Readme.md) + +# Form overview + +![FormOverview.png](../Resources/Public/Documentation/Screenshots/FormOverview.png) + +Each form must be checked if it complies with the GDPR. This can't be done by the extension but it can help to ease the workflow. + +## Features + +- Listing of all forms, generated by the most popular form extensions `form` and `powermail`: +- Provide a state to know which forms have already been checked and which need to be done. + +## Configuration + +No configuration required. If one of the supported form extensions is installed, all content elements showing forms are rendered. \ No newline at end of file diff --git a/Features/Logs.md b/Features/Logs.md new file mode 100644 index 0000000..edbaa08 --- /dev/null +++ b/Features/Logs.md @@ -0,0 +1,7 @@ +[<- back](../Readme.md) + +# Logs + +See and filter any action of GDPR related actions + +![Logs.png](../Resources/Public/Documentation/Screenshots/Logs.png) \ No newline at end of file diff --git a/Features/PersonalData.md b/Features/PersonalData.md new file mode 100644 index 0000000..d4f7fb0 --- /dev/null +++ b/Features/PersonalData.md @@ -0,0 +1,43 @@ +[<- back](../Readme.md) + +# Working with personal data + +The General Data Protection Regulation requires that people can revoke access to their data and that this data must be removed. + +The extension makes it possible to exclude any record by activating a checkbox. After that, the record won't be accessible and available anymore, no matter if backend or frontend, editor or admin. + +![Record-fields.png](../Resources/Public/Documentation/Screenshots/Record-fields.png) + +A new administration module gives editors the possibility to handle those flagged records and react with one of the following options: + +- Completely remove the record from the database +- Reactivate the record +- Randomize content of the record [see Randomization](Randomization.md) + +![record-randomization.png](../Resources/Public/Documentation/Screenshots/record-randomization.png) + +Every action regarding those flags is logged in a central place. + + +## Configuration + +The following code what is needed to add a custom table to the GDPR extension. +The code must be placed in the file `Configuration/TCA/Overrides/.php` + +```php +'); +$tca + ->addRestriction('gdpr_restricted') // name of the field used for the checkbox to flag records + -add('after:disable'); // positioning of the new field +```` + +## Technical background + +The implementation is based on the `RestrictionContainers` of the TYPO3 core. + +### Drawbacks + +The limitation of the implementation is that only records having a TCA configuration are covered. +Furthermore direct access to the database without using the `QueryBuilder` of TYPO3 will still deliver every record. + diff --git a/Features/Randomization.md b/Features/Randomization.md new file mode 100644 index 0000000..0b5dcef --- /dev/null +++ b/Features/Randomization.md @@ -0,0 +1,82 @@ +[<- back](../Readme.md) + +# Randomization + +Randomization is a good way to remove the private data from a record and still be able to use the rest o the record. + +**Example**: A user wants his orders to be removed. After randomization the private data is removed but it is still possible to generate statistics from the order, e.g. orders from a country. + +## Randomize data + +Records can be randomized by using the `fzaninotto/faker` library. +By providing a mapping per table, is possible to exchange the data with dummy information which looks still ok and can be used in exports. An example would be + +``` +Arrayhttps://bitbucket.org/georgringer/gdpr/blob/master/Readme.mdname] => martens.conny + [email] => gerhild.hartwig@yahoo.de + [password] => 94n3ifyp($+%u# + [zip] => 33781 + [address] => Hans-Jürgen-Sauer-Weg 21 +86788 Oberursel (Taunus) + [city] => Pfungstadt + [first_name] => Miriam + [last_name] => Sander + [telephone] => +8747861395322 + [fax] => +5484337015644 +) +``` + +## Configuration + +The code must be placed in the file `Configuration/TCA/Overrides/.php` + +```php +addRestriction('gdpr_restricted') + ->addRandomization('gdpr_randomized', [ + 'dateField' => 'tstamp', + 'expirePeriod' => 360, + 'mapping' => [ + 'username' => 'userName', + 'email' => 'email', + 'password' => 'password', + 'zip' => 'postcode', + 'address' => 'address', + 'city' => 'city', + 'first_name' => 'firstName', + 'last_name' => 'lastName', + 'telephone' => 'e164PhoneNumber', + 'fax' => 'e164PhoneNumber', + ] + ]) + ->add('after:disable'); +``` + +Randomization uses [Faker](https://github.com/fzaninotto/Faker#formatters) and the mapping accepts any property of faker. + + +## Using a CLI command + +By using a CLI command, all data with a specific age can be randomized: `./web/typo3/sysext/core/bin/typo3 gdpr:randomize` + +Result can look like this + +``` +Randomize data +============== + +Starting with table "be_users" +------------------------------ + + // Randomization skipped as not enabled! + +Starting with table "fe_users" +------------------------------ + + // find all fields where value of field "tstamp" is older than 360 days + + [OK] 3 records randomized + +``` diff --git a/Features/ReportModule.md b/Features/ReportModule.md new file mode 100644 index 0000000..acff94c --- /dev/null +++ b/Features/ReportModule.md @@ -0,0 +1,7 @@ +[<- back](../Readme.md) + +# Report module + +A report shows a short information about potential actions. + +![Report.png](../Resources/Public/Documentation/Screenshots/Report.png) \ No newline at end of file diff --git a/Features/Search.md b/Features/Search.md new file mode 100644 index 0000000..8bb67a9 --- /dev/null +++ b/Features/Search.md @@ -0,0 +1,7 @@ +[<- back](../Readme.md) + +# Search + +A search, similar to the one in the *DB Check* module allows to search within sensitive records. + +![Search](../Resources/Public/Documentation/Screenshots/Search.png) \ No newline at end of file diff --git a/Features/VideoEmbedPrivacy.md b/Features/VideoEmbedPrivacy.md new file mode 100644 index 0000000..9641d2c --- /dev/null +++ b/Features/VideoEmbedPrivacy.md @@ -0,0 +1,54 @@ +[<- back](../Readme.md) + +# Improved privacy for embedded videos + +Before videos from the platforms YouTube and Vimeo are actually loaded, the user is asked for consent. This improves the privacy and as an additional benefit also the loading time. + +## Configuration + +The template used to show the overlay can be configured with an additional setting +of the ViewHelper ``. + +Since the versions `8.7.14` and `7.6.28` the ViewHelper call in `fluid_styled_content` looks like this: + +```html + +``` + +This allows to override the template with the following TypoScript: + +``` +lib.contentElement { + settings { + media { + additionalConfig { + gdpr-vimeo-template = EXT:sitepackage/Resources/Private/Templates/Gdpr/Vimeo.html + gdpr-youtube-template = EXT:sitepackage/Resources/Private/Templates/Gdpr/Youtube.html + } + } + } +} +``` + +### Usage in custom extensions + +If the `` is used in custom extensions, use a configuration like `additionalConfig="{settings.mediaConfiguration}"` and a TS like + +``` +plugin.tx_yourExt { + settings { + mediaConfiguration { + gdpr-vimeo-template = EXT:sitepackage/Resources/Private/Templates/Gdpr/Vimeo.html + gdpr-youtube-template = EXT:sitepackage/Resources/Private/Templates/Gdpr/Youtube.html + } + } +} +``` + +### Templates + +The templates (default can be found at `EXT:gdpr/Resources/Private/Templates/Rendering/` contain +the styling and vanilla JS for exchanging the overlay with the actual video iframe. \ No newline at end of file diff --git a/Readme.md b/Readme.md index 6badc27..21a5936 100644 --- a/Readme.md +++ b/Readme.md @@ -2,155 +2,60 @@ [![License](https://poser.pugx.org/georgringer/gdpr/license)](https://packagist.org/packages/georgringer/gdpr) - -This extensions makes it easier for website owners and agencies to have the site compatible to the GDPR (German "DSGVO"). +This extensions enables you as site owner and extension developer to comply with the GDPR by covering some of the important aspects: + +- Find and randomize or remove privacy related data +- CLI to randomize records after given time +- Logging of any privacy related tasks +- Overview and status of forms provided by form extensions +- Improved privacy for included YouTube & Vimeo videos. + +## Table of Contents + +- [Screenshots](Screenshots.md) +- [Setup](Setup.md) +- Features + - [Acces restrictions for personal data](Features/PersonalData.md) + - [Randomization](Features/Randomization.md) + - [Search](Features/Search.md) + - [Logs](Features/Logs.md) + - [Overview & Status of all forms](Features/FormOverview.md) + - [Improved privacy for embedded videos](Features/VideoEmbedPrivacy.md) + - [ReportModule](Features/ReportModule.md) +- [FAQ](FAQ.md) ## Costs -It costs **€ 1450 excl. taxes** and a special price before 25th of May 2018 of **€ 990 excl. taxes**. Contact me - *Georg Ringer* via [mail](mail@ringer.it), [TYPO3 slack](https://forger.typo3.com/slack) or [twitter](https://twitter.com/georg_ringer). - -Costs are **per** installation - discounts possible, ask me directly. - -## Screenshots - -**Be aware** that this extension does **not** cover every area of the GDPR - especially the frontend part is **not** covered! - -![Overview](Resources/Public/Documentation/Screenshots/Overview.png) -![Record fields](Resources/Public/Documentation/Screenshots/Record-fields.png) -![Search](Resources/Public/Documentation/Screenshots/Search.png) -![Configuration](Resources/Public/Documentation/Screenshots/Configuration.png) -![Help](Resources/Public/Documentation/Screenshots/Help.png) - -## Requirements - -- TYPO3 CMS 8 LTS & 9 LTS - -## Drawbacks - -The limitation of the implementation is that only records having a TCA configuration are covered. -Furthermore direct access to the database without using the `QueryBuilder` of TYPO3 will still deliver every record. - -## Features - -### Hide sensitive data - -The General Data Protection Regulation requires that people can revoke access to their data. -The extension makes it possible to exclude any record by activating a checkbox. After that, the record won't be accessible and available anymore, no matter if backend or frontend, editor or admin. - -A new administration module gives editors the possibility to handle those flagged records and react with one of the following options: - -- Completely remove the record from the database -- Reactivate the record -- Randomize content of the record (see below) - -Every action regarding those flags is logged on a central place. - -As additional security layer, this module must be explicitly enabled for every user - even for administrators. - -### Randomize data - -Records can be randomized by using the `fzaninotto/faker`. By providing a mapping per table, is possible to exchange the data with dummy information which looks still ok and can be used in exports. An example would be - -``` -Array -( - [username] => martens.conny - [email] => gerhild.hartwig@yahoo.de - [password] => 94n3ifyp($+%u# - [zip] => 33781 - [address] => Hans-Jürgen-Sauer-Weg 21 -86788 Oberursel (Taunus) - [city] => Pfungstadt - [first_name] => Miriam - [last_name] => Sander - [telephone] => +8747861395322 - [fax] => +5484337015644 -) -``` - -#### Using a CLI command - -By using a CLI command, all data with a specific age can be randomized: `./web/typo3/sysext/core/bin/typo3 gdpr:randomize` - -Result can look like this - -``` -Randomize data -============== - -Starting with table "be_users" ------------------------------- - - // Randomization skipped as not enabled! - -Starting with table "fe_users" ------------------------------- - - // find all fields where value of field "tstamp" is older than 360 days - - [OK] 3 records randomized - -``` - -### Search - -A search, similar to the one in the *DB Check* module allows to search within sensitive records. - -### Logs - -See and filter any action of GDPR related actions - -### Anonymize IP logging - -IPs inserted into the table `sys_log` and `index_stat_search` are now anonymized. - -#### Anonymize existing data - -By using a CLI command, existing IPs can be anonymized. Example: - -``` -# parameters: -./web/typo3/sysext/core/bin/typo3 gdpr:anonymizeIp sys_log tstamp IP 365 -./web/typo3/sysext/core/bin/typo3 gdpr:anonymizeIp index_stat_search tstamp IP 180 -``` +*The license is a one time fee and includes every update until Mai 2019.* Renewal is possible after that. -### Report for report module +- Personal license for one site: *€ 240 excl. taxes* +- Professional license for one site: *€ 1450 excl. taxes* (*\**) +- Agency license for up to 25 sites: *€ 5000 excl. taxes* (*\**) +- Agency license for unlimited sites: *€ 7500 excl. taxes* (*\**) +- Academic license for universities, research Institutions, and colleges *€ 490 excl. taxes* -A report shows a short information about potential actions. +**(*\**) Important: Get a 30% discount before 25th May 2018!** -## Usage +Contact me - *Georg Ringer* via [mail](mailto:mail@ringer.it), [TYPO3 slack](https://forger.typo3.com/slack) or [twitter](https://twitter.com/georg_ringer). -1. Install the extension -2. Enable the users who are allowed to work with the module in the `be_users` table. -3. By default, the table `fe_users` is defined as the one having sensitive information, therefore there is a checkbox to hide this record. +### Differences free <> paid version -### Add restriction to custom tables +| | Free | Paid | +|:-----------------------------------|:-----|:-----| +| API to hide records | ✓ | ✓ | +| Form overview | ✓ | ✓ | +| Individual form status | | ✓ | +| Randomization | | ✓ | +| Custom icon for randomized records | | ✓ | +| CLI for randomization | | ✓ | +| Log module | | ✓ | +| Search module | | ✓ | +| Reports module | ✓ | ✓ | +| Privacy for YouTube & Vimeo videos | | ✓ | +| Support further development | | ✓ | -Example call how an own record can be added. +### Access to paid version -``` -$tca = \GeorgRinger\Gdpr\Service\Tca::getInstance('fe_users'); -$tca - ->addRestriction('gdpr_restricted') - ->addRandomization('gdpr_randomized', [ - 'dateField' => 'tstamp', - 'expirePeriod' => 360, - 'mapping' => [ - 'username' => 'userName', - 'email' => 'email', - 'password' => 'password', - 'zip' => 'postcode', - 'address' => 'address', - 'city' => 'city', - 'first_name' => 'firstName', - 'last_name' => 'lastName', - 'telephone' => 'e164PhoneNumber', - 'fax' => 'e164PhoneNumber', - ] - ]) - ->add('after:disable'); -``` +Access to a private GitHub repository will be granted. This makes it possible to include the extension in automated deployments. -## Technical background -The implementation is based on the `RestrictionContainers` of the TYPO3 core. diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf new file mode 100644 index 0000000..bccb890 --- /dev/null +++ b/Resources/Private/Language/locallang.xlf @@ -0,0 +1,21 @@ + + + +
+ + + GDPR + + + Enable GDPR module + + + Record is randomized + + + Record must be hidden everywhere + + + + + diff --git a/Resources/Private/Language/locallang_modadministration.xlf b/Resources/Private/Language/locallang_modadministration.xlf new file mode 100644 index 0000000..9e77493 --- /dev/null +++ b/Resources/Private/Language/locallang_modadministration.xlf @@ -0,0 +1,17 @@ + + + +
+ + + Gdpr Administration + + + Administrate for GDPR related content + + + This module provides an easy way to administrate records + + + + diff --git a/Resources/Private/Language/locallang_rendering.xlf b/Resources/Private/Language/locallang_rendering.xlf new file mode 100644 index 0000000..e5955ef --- /dev/null +++ b/Resources/Private/Language/locallang_rendering.xlf @@ -0,0 +1,14 @@ + + + +
+ + + Click to load video! + + + Load the video by clicking here. Only then data will be transferred to the video provider! + + + + diff --git a/Resources/Private/Layouts/Backend/Default.html b/Resources/Private/Layouts/Backend/Default.html new file mode 100644 index 0000000..74127b2 --- /dev/null +++ b/Resources/Private/Layouts/Backend/Default.html @@ -0,0 +1,6 @@ + +
+ + +
+ diff --git a/Resources/Private/Partials/Forms/Listing.html b/Resources/Private/Partials/Forms/Listing.html new file mode 100644 index 0000000..cbc6c80 --- /dev/null +++ b/Resources/Private/Partials/Forms/Listing.html @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + +
uidHeaderPage
+ + + {row.uid} + + {row.header} + + {row._meta.path} + +
+ + + + + + + + + + + + + + + +
+
diff --git a/Resources/Private/Partials/TableListing.html b/Resources/Private/Partials/TableListing.html new file mode 100644 index 0000000..46c47d0 --- /dev/null +++ b/Resources/Private/Partials/TableListing.html @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + +
Status{f:translate(key:meta.titleLabel,default:meta.titleField)}Actions
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
{row.{meta.titleField}} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/Resources/Private/Templates/Administration/Configuration.html b/Resources/Private/Templates/Administration/Configuration.html new file mode 100644 index 0000000..fd221c2 --- /dev/null +++ b/Resources/Private/Templates/Administration/Configuration.html @@ -0,0 +1,43 @@ + + + + + +

Configuration

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{f:translate(key:table.title)} ({tableName})
Restriction field{table.gdprRestrictionField}
Randomization field{table.gdprRandomizedField}
Randomization mapping{table.gdprRandomizeMapping}
Randomization date field{table.gdprRandomizedDateField}
Randomization expire period{table.gdprExpirePeriod}
+
+
+ diff --git a/Resources/Private/Templates/Administration/Delete.html b/Resources/Private/Templates/Administration/Delete.html new file mode 100644 index 0000000..0ed053b --- /dev/null +++ b/Resources/Private/Templates/Administration/Delete.html @@ -0,0 +1 @@ +- empty - diff --git a/Resources/Private/Templates/Administration/FormOverview.html b/Resources/Private/Templates/Administration/FormOverview.html new file mode 100644 index 0000000..819a61a --- /dev/null +++ b/Resources/Private/Templates/Administration/FormOverview.html @@ -0,0 +1,27 @@ + + + + + +

Form overviews

+

Forms are acrusial part regarding privacy. Listing of all forms

+ + + + +
+
{extension}
+ + + + + +
No forms found.
+
+
+
+
+ +
+ diff --git a/Resources/Private/Templates/Administration/FormStatusUpdate.html b/Resources/Private/Templates/Administration/FormStatusUpdate.html new file mode 100644 index 0000000..55db4d2 --- /dev/null +++ b/Resources/Private/Templates/Administration/FormStatusUpdate.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/Private/Templates/Administration/Help.html b/Resources/Private/Templates/Administration/Help.html new file mode 100644 index 0000000..0092e7b --- /dev/null +++ b/Resources/Private/Templates/Administration/Help.html @@ -0,0 +1,155 @@ + + + + + +

Help

+

Some words about GDPR

+ + +

A robust PIA should document the following information: Taken from www.smashingmagazine.com/2018/02/gdpr-for-web-developers/ +

+ +
+ + +
+
+
+ +
+
+

Data collection and retention

+
+
    +
  1. What personal data is processed
  2. +
  3. How is that data collected and retained?
  4. +
  5. Is the data stored locally, on our servers, or both?
  6. +
  7. For how long is data stored, and when is the data deleted?
  8. +
  9. Is the data collection and processing specified, explicit, and legitimate?
  10. +
  11. What is the process for granting consent for the data processing, and is consent + explicit + and + verifiable? +
  12. +
  13. What is the basis of the consent for the data processing?
  14. +
  15. If not based on consent, what is the legal basis for the data processing?
  16. +
  17. Is the data minimized to what is explicitly required?
  18. +
  19. Is the data accurate and kept up to date?
  20. +
  21. How are users informed about the data processing?
  22. +
  23. What controls do users have over the data collection and retention?
  24. +
+
+ +
+
+
+
+
+
+ +
+
+

Technical and security measures

+
+
    +
  1. Is the data encrypted?
  2. +
  3. Is the data anonymized or pseudonymized?
  4. +
  5. Is the data backed up?
  6. +
  7. What are the technical and security measures at the host location?
  8. +
+
+
+
+
+ +
+
+
+ +
+
+

Personnel

+
+
    +
  1. Who has access to the data?
  2. +
  3. What data protection training have those individuals received?
  4. +
  5. What security measures do those individuals work with?
  6. +
  7. What data breach notification and alert procedures are in place?
  8. +
  9. What procedures are in place for government requests?
  10. +
+
+
+
+
+ +
+
+
+ +
+
+

Subject access rights

+
+
    +
  1. How does the data subject exercise their access rights?
  2. +
  3. How does the data subject exercise their right to data portability?
  4. +
  5. How does the data subject exercise their rights to erasure and the right to be + forgotten? +
  6. +
  7. How does the data subject exercise their right to restrict and object?
  8. +
+
+
+
+
+ +
+
+
+ +
+
+

Legal

+
+
    +
  1. Are the obligations of all data processors, including subcontractors, covered by a + contract? +
  2. +
  3. If the data is transferred outside the European Union, what are the protective measures + and + safeguards? +
  4. +
+
+
+
+
+ +
+
+
+ +
+
+

Risks

+
+
    +
  1. What are the risks to the data subjects if the data is misused, mis-accessed, or + breached? +
  2. +
  3. What are the risks to the data subjects if the data is modified?
  4. +
  5. What are the risks to the data subjects if the data is lost?
  6. +
  7. What are the main sources of risk?
  8. +
  9. What steps have been taken to mitigate those risks?
  10. +
+
+
+
+
+
+ +
+ diff --git a/Resources/Private/Templates/Administration/Index.html b/Resources/Private/Templates/Administration/Index.html new file mode 100644 index 0000000..b2de136 --- /dev/null +++ b/Resources/Private/Templates/Administration/Index.html @@ -0,0 +1,28 @@ + + + + + +

GDPR

+

Overview over all records flagged as holding sensitive data.

+ +
+
{f:translate(key:data.meta.title,default:data.meta.title)} + ({data.statistics.public} / {data.statistics.restricted}) +
+ + + + + +
No flagged records.
+
+
+
+ +
+ +
+ diff --git a/Resources/Private/Templates/Administration/Log.html b/Resources/Private/Templates/Administration/Log.html new file mode 100644 index 0000000..b76c136 --- /dev/null +++ b/Resources/Private/Templates/Administration/Log.html @@ -0,0 +1,131 @@ + + + + + +

Logs

+

List of actions logged regarding GDPR.

+ + + +
+ + +
+ +
+
+ +
+ + +
+
+ + + + +
+
+
+
+ + + + +
+
+
+ + +
+ + +
+ +
+
+ +
+ + +
+ +
+
+ + +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
No results found!
+
+ + + + diff --git a/Resources/Private/Templates/Administration/ModuleNotEnabled.html b/Resources/Private/Templates/Administration/ModuleNotEnabled.html new file mode 100644 index 0000000..c7d0add --- /dev/null +++ b/Resources/Private/Templates/Administration/ModuleNotEnabled.html @@ -0,0 +1,14 @@ + + + + + +

GDPR

+ + This module must be explicitly enabled for your user account! +
- even if your user is already an administrator! +
+ +
+ diff --git a/Resources/Private/Templates/Administration/Randomize.html b/Resources/Private/Templates/Administration/Randomize.html new file mode 100644 index 0000000..0ed053b --- /dev/null +++ b/Resources/Private/Templates/Administration/Randomize.html @@ -0,0 +1 @@ +- empty - diff --git a/Resources/Private/Templates/Administration/Reenable.html b/Resources/Private/Templates/Administration/Reenable.html new file mode 100644 index 0000000..0ed053b --- /dev/null +++ b/Resources/Private/Templates/Administration/Reenable.html @@ -0,0 +1 @@ +- empty - diff --git a/Resources/Private/Templates/Administration/Search.html b/Resources/Private/Templates/Administration/Search.html new file mode 100644 index 0000000..ed9655c --- /dev/null +++ b/Resources/Private/Templates/Administration/Search.html @@ -0,0 +1,63 @@ + + + + + +

Search

+ + + +
+ + +
+ +
+
+ +
+ + +
+ +
+
+ +
+
+ +
+
+
+ + + + + +
+
{f:translate(key:data.meta.title,default:data.meta.title)} + ({data.rows -> f:count()}) +
+ +
+
+
+ +
No results found
+
+ +
+
+ + +
+ diff --git a/Resources/Private/Templates/Rendering/Youtube.html b/Resources/Private/Templates/Rendering/Youtube.html new file mode 100644 index 0000000..f902cf1 --- /dev/null +++ b/Resources/Private/Templates/Rendering/Youtube.html @@ -0,0 +1,37 @@ +
+
+
+ {f:translate(key:'LLL:EXT:gdpr/Resources/Private/Language/locallang_rendering.xlf:overlay.title')} +
+

+ {f:translate(key:'LLL:EXT:gdpr/Resources/Private/Language/locallang_rendering.xlf:overlay.description')}

+
+
+ {html -> f:format.raw()} +
+ + + \ No newline at end of file diff --git a/Resources/Public/Documentation/Screenshots/FormOverview.png b/Resources/Public/Documentation/Screenshots/FormOverview.png new file mode 100644 index 0000000..2aedc0a Binary files /dev/null and b/Resources/Public/Documentation/Screenshots/FormOverview.png differ diff --git a/Resources/Public/Documentation/Screenshots/Logs.png b/Resources/Public/Documentation/Screenshots/Logs.png new file mode 100644 index 0000000..e7d5506 Binary files /dev/null and b/Resources/Public/Documentation/Screenshots/Logs.png differ diff --git a/Resources/Public/Documentation/Screenshots/Report.png b/Resources/Public/Documentation/Screenshots/Report.png new file mode 100644 index 0000000..07b40c2 Binary files /dev/null and b/Resources/Public/Documentation/Screenshots/Report.png differ diff --git a/Resources/Public/Documentation/Screenshots/record-randomization.png b/Resources/Public/Documentation/Screenshots/record-randomization.png new file mode 100644 index 0000000..3160d4b Binary files /dev/null and b/Resources/Public/Documentation/Screenshots/record-randomization.png differ diff --git a/Resources/Public/Icons/Extension.svg b/Resources/Public/Icons/Extension.svg new file mode 100644 index 0000000..465f232 --- /dev/null +++ b/Resources/Public/Icons/Extension.svg @@ -0,0 +1,12 @@ + + + + background + + + + Layer 1 + + + + \ No newline at end of file diff --git a/Resources/Public/Icons/form-overview.svg b/Resources/Public/Icons/form-overview.svg new file mode 100644 index 0000000..6c73ec0 --- /dev/null +++ b/Resources/Public/Icons/form-overview.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Resources/Public/Icons/module.svg b/Resources/Public/Icons/module.svg index bca7a17..465f232 100644 --- a/Resources/Public/Icons/module.svg +++ b/Resources/Public/Icons/module.svg @@ -1 +1,12 @@ - \ No newline at end of file + + + + background + + + + Layer 1 + + + + \ No newline at end of file diff --git a/Screenshots.md b/Screenshots.md new file mode 100644 index 0000000..1641cd7 --- /dev/null +++ b/Screenshots.md @@ -0,0 +1,15 @@ + +[<- back](Readme.md) + +# Screenshots + +![Overview](Resources/Public/Documentation/Screenshots/Overview.png) +![Record fields](Resources/Public/Documentation/Screenshots/Record-fields.png) +![Search](Resources/Public/Documentation/Screenshots/Search.png) +![Configuration](Resources/Public/Documentation/Screenshots/Configuration.png) +![Form overview](Resources/Public/Documentation/Screenshots/FormOverview.png) +![Report module](Resources/Public/Documentation/Screenshots/Report.png) +![Logs](Resources/Public/Documentation/Screenshots/Logs.png) +![Help](Resources/Public/Documentation/Screenshots/Help.png) + +[<- back](Readme.md) \ No newline at end of file diff --git a/Setup.md b/Setup.md new file mode 100644 index 0000000..0649d7e --- /dev/null +++ b/Setup.md @@ -0,0 +1,15 @@ +[<- back](Readme.md) + +# Setup + +## Requirements + +- TYPO3 CMS 8 LTS & 9 LTS + +## Usage + +1. Install the extension +2. Enable the users who are allowed to work with the module in the `be_users` table. +3. By default, the tables `fe_users` & be_users` are defined as the one having sensitive information, therefore there is a checkbox to hide this record. + +[<- back](Readme.md) \ No newline at end of file diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..c741881 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-slate \ No newline at end of file diff --git a/ext_emconf.php b/ext_emconf.php index 7bcf1ec..44a55ee 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -2,7 +2,7 @@ $EM_CONF[$_EXTKEY] = [ 'title' => 'Make TYPO3 compatible to GDPR', - 'description' => '', + 'description' => 'This extensions enables you as site owner and extension developer to comply with the GDPR by covering some of the important aspects', 'category' => 'module', 'author' => 'Georg Ringer', 'author_email' => 'mail@ringer.it', diff --git a/ext_localconf.php b/ext_localconf.php new file mode 100644 index 0000000..395a313 --- /dev/null +++ b/ext_localconf.php @@ -0,0 +1,23 @@ + 'form-overview.svg', +]; +foreach ($icons as $identifier => $path) { + $iconRegistry->registerIcon( + $identifier, + \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class, + ['source' => 'EXT:gdpr/Resources/Public/Icons/' . $path] + ); +} \ No newline at end of file diff --git a/ext_tables.php b/ext_tables.php new file mode 100644 index 0000000..5a6bc1c --- /dev/null +++ b/ext_tables.php @@ -0,0 +1,32 @@ + \GeorgRinger\Gdpr\Xclass\DefaultRestrictionContainer::class +); +$GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][\TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer::class] = array( + 'className' => \GeorgRinger\Gdpr\Xclass\FrontendRestrictionContainer::class +); + + +$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] + = \GeorgRinger\Gdpr\Hooks\DataHandlerHook::class; + +if (TYPO3_MODE === 'BE') { + $isVersion9Up = \TYPO3\CMS\Core\Utility\VersionNumberUtility::convertVersionNumberToInteger(TYPO3_version) >= 9000000; + \TYPO3\CMS\Extbase\Utility\ExtensionUtility::registerModule( + 'GeorgRinger.gdpr', + $isVersion9Up ? 'site' : 'tools', + 'tx_gdpr_m1', + '', + ['Administration' => 'index,help,search,delete,disable,reenable,randomize,moduleNotEnabled,log,configuration,formOverview,formStatusUpdate'], + [ + 'access' => 'user,group', + 'icon' => 'EXT:gdpr/Resources/Public/Icons/module.svg', + 'labels' => 'LLL:EXT:gdpr/Resources/Private/Language/locallang_modadministration.xlf', + ] + ); + + + $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['reports']['tx_reports']['status']['providers']['system'][] = \GeorgRinger\Gdpr\Report\GdprStatusReport::class; + +} diff --git a/ext_tables.sql b/ext_tables.sql new file mode 100644 index 0000000..84fbfcd --- /dev/null +++ b/ext_tables.sql @@ -0,0 +1,29 @@ +# +# Table structure for table 'tx_gdpr_domain_model_log' +# +CREATE TABLE tx_gdpr_domain_model_log ( + uid int(11) NOT NULL auto_increment, + pid int(11) DEFAULT '0' NOT NULL, + tstamp int(11) DEFAULT '0' NOT NULL, + table_name varchar(255) NOT NULL DEFAULT '', + record_id int(11) DEFAULT '0' NOT NULL, + status tinyint(4) DEFAULT '0' NOT NULL, + user int(11) DEFAULT '0' NOT NULL, + user_name_text varchar(255) NOT NULL DEFAULT '', + + PRIMARY KEY (uid), + KEY parent (pid) +); + +# +# Table structure for table 'fe_users' +# +CREATE TABLE fe_users ( + gdpr_restricted tinyint(4) DEFAULT '0' NOT NULL, + gdpr_randomized tinyint(4) DEFAULT '0' NOT NULL +); + +CREATE TABLE be_users ( + gdpr_restricted tinyint(4) DEFAULT '0' NOT NULL, + gdpr_module_enable tinyint(4) DEFAULT '0' NOT NULL +); \ No newline at end of file diff --git a/notes.md b/notes.md deleted file mode 100644 index 042fe4e..0000000 --- a/notes.md +++ /dev/null @@ -1,11 +0,0 @@ -- the site owner must make a list of the personal datas he has stored in the platform, why, and for how long (no longer than necessary). He must also secure this data and allow users to access and delete it. - -When the duration of a personal data storing is over, it is necessary to: - -- remove it -- or archive it (i. e. outside the website) -- or anonymize it - -https://www.cmswire.com/digital-experience/is-your-cms-gdpr-ready/ - -http://ec.europa.eu/justice/smedataprotect/index_en.htm \ No newline at end of file
TableRecordDateStatusUser
{row.table_name}{row.record_id} + @{row.tstamp} + + + delete + randomize + reenable + restrict + ip anonymize + + {row.user}