diff --git a/Setup/Actions.php b/Setup/Actions.php new file mode 100644 index 0000000..cd24023 --- /dev/null +++ b/Setup/Actions.php @@ -0,0 +1,219 @@ + + * @link https://www.oxidmodule.com + */ + +declare(strict_types=1); + +namespace D3\Totp\Setup; + +use Doctrine\DBAL\Driver\Exception as DoctrineDriverException; +use Exception; +use OxidEsales\DoctrineMigrationWrapper\MigrationsBuilder; +use OxidEsales\Eshop\Application\Controller\FrontendController; +use OxidEsales\Eshop\Core\DbMetaDataHandler; +use OxidEsales\Eshop\Core\Registry; +use OxidEsales\Eshop\Core\SeoEncoder; +use OxidEsales\Eshop\Core\Utils; +use OxidEsales\Eshop\Core\UtilsView; +use OxidEsales\EshopCommunity\Internal\Container\ContainerFactory; +use OxidEsales\EshopCommunity\Internal\Framework\Module\Configuration\Bridge\ShopConfigurationDaoBridgeInterface; +use OxidEsales\EshopCommunity\Internal\Framework\Module\Configuration\DataObject\ModuleConfiguration; +use OxidEsales\EshopCommunity\Internal\Framework\Module\Configuration\Exception\ModuleConfigurationNotFoundException; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; +use Psr\Log\LoggerInterface; + +class Actions +{ + public array $seo_de = [ + '2-faktor-authentisierung/' + ]; + public array $seo_en = [ + 'en/2-factor-authentication/' + ]; + public array $stdClassName = [ + 'd3_account_totp' + ]; + + /** + * @throws Exception + */ + public function runModuleMigrations(): void + { + /** @var MigrationsBuilder $migrationsBuilder */ + $migrationsBuilder = oxNew(MigrationsBuilder::class); + $migrations = $migrationsBuilder->build(); + $migrations->execute('migrations:migrate', 'd3totp'); + } + + /** + * Regenerate views for changed tables + * @throws Exception + */ + public function regenerateViews(): void + { + $oDbMetaDataHandler = oxNew(DbMetaDataHandler::class); + $oDbMetaDataHandler->updateViews(); + } + + /** + * clear cache + * @throws Exception + */ + public function clearCache(): void + { + try { + $oUtils = oxNew(Utils::class); + $oUtils->resetTemplateCache($this->getModuleTemplates()); + $oUtils->resetLanguageCache(); + } catch (ContainerExceptionInterface|NotFoundExceptionInterface|ModuleConfigurationNotFoundException $e) { + oxNew(LoggerInterface::class)->error($e->getMessage(), [$this]); + oxNew(UtilsView::class)->addErrorToDisplay($e->getMessage()); + } + } + + /** + * @return array + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + * @throws ModuleConfigurationNotFoundException + */ + protected function getModuleTemplates(): array + { + $container = $this->getDIContainer(); + $shopConfiguration = $container->get(ShopConfigurationDaoBridgeInterface::class)->get(); + $moduleConfiguration = $shopConfiguration->getModuleConfiguration('d3totp'); + + return array_unique(array_merge( + $this->getModuleTemplatesFromTemplates($moduleConfiguration), + $this->getModuleTemplatesFromBlocks($moduleConfiguration) + )); + } + + /** + * @param ModuleConfiguration $moduleConfiguration + * + * @return array + */ + protected function getModuleTemplatesFromTemplates(ModuleConfiguration $moduleConfiguration): array + { + /** @var $template ModuleConfiguration\Template */ + return array_map( + function ($template) { + return $template->getTemplateKey(); + }, + $moduleConfiguration->getTemplates() + ); + } + + /** + * @param ModuleConfiguration $moduleConfiguration + * + * @return array + */ + protected function getModuleTemplatesFromBlocks(ModuleConfiguration $moduleConfiguration): array + { + /** @var $templateBlock ModuleConfiguration\TemplateBlock */ + return array_map( + function ($templateBlock) { + return basename($templateBlock->getShopTemplatePath()); + }, + $moduleConfiguration->getTemplateBlocks() + ); + } + + /** + * @return void + */ + public function seoUrl() + { + try { + if (!$this->hasSeoUrls()) { + $this->createSeoUrls(); + } + } catch (Exception|NotFoundExceptionInterface|DoctrineDriverException|ContainerExceptionInterface $e) { + Registry::getLogger()->error($e->getMessage(), [$this]); + Registry::getUtilsView()->addErrorToDisplay('error wile creating SEO URLs: ' . $e->getMessage()); + } + } + + /** + * @return bool + * @throws Exception + */ + public function hasSeoUrls(): bool + { + foreach ($this->stdClassName as $item) { + foreach ([0, 1] as $lang) { + if (false === $this->hasSeoUrl($item, $lang)) { + return false; + } + } + } + + return true; + } + + protected function hasSeoUrl($item, $langId): bool + { + $seoEncoder = oxNew(SeoEncoder::class); + $seoUrl = $seoEncoder->getStaticUrl( + oxNew(FrontendController::class)->getViewConfig()->getSelfLink() . + "cl=" . $item, + $langId + ); + + return (bool)strlen($seoUrl); + } + + /** + * @return void + */ + public function createSeoUrls() + { + foreach (array_keys($this->stdClassName) as $id) { + $seoEncoder = oxNew(SeoEncoder::class); + $objectid = md5(strtolower(Registry::getConfig()->getShopId() . $this->seo_de[$id])); + if (!$this->hasSeoUrl($this->stdClassName[$id], 0)) { + $seoEncoder->addSeoEntry( + $objectid, + Registry::getConfig()->getShopId(), + 0, + 'index.php?cl=' . $this->stdClassName[$id], + $this->seo_de[$id], + 'static', + false + ); + } + if (!$this->hasSeoUrl($this->stdClassName[$id], 0)) { + $seoEncoder->addSeoEntry( + $objectid, + Registry::getConfig()->getShopId(), + 1, + 'index.php?cl=' . $this->stdClassName[$id], + $this->seo_en[$id], + 'static', + false + ); + } + } + } + + /** + * @return ContainerInterface|null + */ + protected function getDIContainer(): ?ContainerInterface + { + return ContainerFactory::getInstance()->getContainer(); + } +} diff --git a/Setup/Events.php b/Setup/Events.php index fd3713d..f083fb9 100644 --- a/Setup/Events.php +++ b/Setup/Events.php @@ -15,7 +15,6 @@ declare(strict_types=1); namespace D3\Totp\Setup; -use OxidEsales\Eshop\Core\DatabaseProvider; use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; use OxidEsales\Eshop\Core\Exception\DatabaseErrorException; @@ -29,10 +28,11 @@ class Events */ public static function onActivate(): void { - self::addTotpTable(); - self::addTotpBackupCodesTable(); - self::addSeoItem1(); - self::addSeoItem2(); + $actions = oxNew(Actions::class); + $actions->runModuleMigrations(); + $actions->regenerateViews(); + $actions->clearCache(); + $actions->seoUrl(); } /** @@ -41,95 +41,5 @@ class Events public static function onDeactivate() { } - - /** - * @return void - * @throws DatabaseConnectionException - * @throws DatabaseErrorException - */ - public static function addTotpTable(): void - { - $query = "CREATE TABLE IF NOT EXISTS `d3totp` ( - `OXID` CHAR(32) NOT NULL , - `OXUSERID` CHAR(32) NOT NULL , - `USETOTP` TINYINT(1) NOT NULL DEFAULT 0, - `SEED` VARCHAR(256) NOT NULL , - `OXTIMESTAMP` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Timestamp', - PRIMARY KEY (`OXID`) , - UNIQUE KEY `OXUSERID` (`OXUSERID`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci COMMENT='totp setting';"; - - DatabaseProvider::getDb()->execute($query); - } - - /** - * @return void - * @throws DatabaseConnectionException - * @throws DatabaseErrorException - */ - public static function addTotpBackupCodesTable(): void - { - $query = "CREATE TABLE IF NOT EXISTS `d3totp_backupcodes` ( - `OXID` CHAR(32) NOT NULL , - `OXUSERID` CHAR(32) NOT NULL COMMENT 'user id', - `BACKUPCODE` VARCHAR(64) NOT NULL COMMENT 'BackupCode', - `OXTIMESTAMP` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Timestamp', - PRIMARY KEY (`OXID`) , - KEY `OXUSERID` (`OXUSERID`) , - KEY `BACKUPCODE` (`BACKUPCODE`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci COMMENT='totp backup codes';"; - - DatabaseProvider::getDb()->execute($query); - } - - /** - * @return void - * @throws DatabaseConnectionException - * @throws DatabaseErrorException - */ - public static function addSeoItem1(): void - { - if (!DatabaseProvider::getDb()->getOne('SELECT 1 FROM oxseo WHERE oxident = "76282e134ad4e40a3578e121a6cb1f6a"')) { - $query = "INSERT INTO `oxseo` - ( - `OXOBJECTID`, `OXIDENT`, `OXSHOPID`, - `OXLANG`, `OXSTDURL`, `OXSEOURL`, - `OXTYPE`, `OXFIXED`, `OXEXPIRED`, - `OXPARAMS`, `OXTIMESTAMP` - ) VALUES ( - '39f744f17e974988e515558698a29df4', '76282e134ad4e40a3578e121a6cb1f6a', 1, - 1, 'index.php?cl=d3_account_totp', 'en/2-factor-authintication/', - 'static', 0, 0, - '', NOW() - );"; - - DatabaseProvider::getDb()->execute($query); - } - } - - /** - * @return void - * @throws DatabaseConnectionException - * @throws DatabaseErrorException - */ - public static function addSeoItem2(): void - { - if (!DatabaseProvider::getDb()->getOne('SELECT 1 FROM oxseo WHERE oxident = "c1f8b5506e2b5d6ac184dcc5ebdfb591"')) { - $query = "INSERT INTO `oxseo` - ( - `OXOBJECTID`, `OXIDENT`, `OXSHOPID`, - `OXLANG`, `OXSTDURL`, `OXSEOURL`, - `OXTYPE`, `OXFIXED`, `OXEXPIRED`, - `OXPARAMS`, `OXTIMESTAMP` - ) VALUES ( - '39f744f17e974988e515558698a29df4', 'c1f8b5506e2b5d6ac184dcc5ebdfb591', 1, - 0, 'index.php?cl=d3_account_totp', '2-faktor-authentisierung/', - 'static', 0, 0, - '', NOW() - );"; - - DatabaseProvider::getDb()->execute($query); - } - } } // @codeCoverageIgnoreEnd diff --git a/metadata.php b/metadata.php index 4ffc2f4..2faac84 100644 --- a/metadata.php +++ b/metadata.php @@ -42,7 +42,7 @@ use OxidEsales\Eshop\Application\Model as OxidModel; $sMetadataVersion = '2.1'; $sModuleId = 'd3totp'; -$logo = '(D3)'; +$logo = '(D3)'; /** * Module information diff --git a/migrations/README.md b/migrations/README.md new file mode 100644 index 0000000..b6a412c --- /dev/null +++ b/migrations/README.md @@ -0,0 +1,38 @@ +# Arbeiten mit [Doctrine Migrations](https://www.doctrine-project.org/projects/doctrine-migrations/en/3.6/reference/introduction.html) + +Migrations bilden die Veränderung der Datenbankstruktur in programmierter Form ab. Jede Strukturänderung wird in einer einzelnen (jeweils neuen) Migrationsdatei abgelegt, die Teil des Moduls ist. + +Passe die `migrations.yml` an Dein Modul an. + +## Erstellen eines Skeletons für die erste oder zusätzliche Migrationen + +``` +./vendor/bin/oe-eshop-doctrine_migration migrations:generate d3moduleid +``` + +Arbeite die angelegte Datei entsprechend Deinen Anforderungen um. + +## Ausführen der noch nicht ausgeführten Migrations + +Doctrine überwacht selbst, welche Migrationen schon ausgeführt wurden und verhindert damit mehrfache Ausführungen der selben Migration. + +Im OXID-Shop werden Migrations mit folgendem Befehl ausgeführt: + +``` +./vendor/bin/oe-eshop-db_migrate migrations:migrate +``` + +Als Argument kann noch die Suite mitgegeben werden, wenn nur bestimmte Migrations ausgeführt werden sollen. Mögliche Angaben sind: + +- CE - für alle CE-Migrations +- PE - für alle PE-Migrations +- EE - für alle EE-Migrations +- PR - für alle Projekt-Migrations +- ModuleId - für alle Migrations des jeweiligen Moduls +- ohne Angabe - werden die Migrations aller Suiten nacheinander ausgeführt + +## Abweichungen zwischen Doctrine Migrations und dem OXID Migration Wrapper + +In den originalen Doctrine Migrations können keine Suiten angegeben werden. Dafür gibt es die Möglichkeit, die Richtung (up / down) und eine Zielversion anzugeben. + +Bei OXID können Migrations ausschließlich aufwärts (up) und immer fix bis zur aktuellsten Version ausgeführt werden. \ No newline at end of file diff --git a/migrations/data/Version20240905232017.php b/migrations/data/Version20240905232017.php new file mode 100644 index 0000000..11fb951 --- /dev/null +++ b/migrations/data/Version20240905232017.php @@ -0,0 +1,147 @@ +connection->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string'); + + $this->addTotpTable($schema); + $this->addTotpBackupCodesTable($schema); + } + + /** + * @param Schema $schema + * @return void + * @throws SchemaException + */ + public function addTotpTable(Schema $schema): void + { + $table = !$schema->hasTable('d3totp') ? + $schema->createTable('d3totp')->setComment('totp setting') : + $schema->getTable('d3totp'); + + // OXID + if (!$table->hasColumn('OXID')) { + $table->addColumn('OXID', (new StringType())->getName()) + ->setLength(32) + ->setFixed(true) + ->setNotnull(true); + } + + // OXUSERID + if (!$table->hasColumn('OXUSERID')) { + $table->addColumn('OXUSERID', (new StringType())->getName()) + ->setLength(32) + ->setFixed(true) + ->setNotnull(true); + } + + // useTotp + if (!$table->hasColumn('USETOTP')) { + $table->addColumn('USETOTP', (new BooleanType())->getName()) + ->setLength(1) + ->setDefault(0) + ->setNotnull(true); + } + + // Seed + if (!$table->hasColumn('SEED')) { + $table->addColumn('SEED', (new StringType())->getName()) + ->setLength(256) + ->setNotnull(true); + } + + // oxtimestamp + if (!$table->hasColumn('OXTIMESTAMP')) { + $table->addColumn('OXTIMESTAMP', (new DateTimeType())->getName()) + ->setNotnull(true) + ->setDefault('CURRENT_TIMESTAMP'); + } + + $table->hasPrimaryKey() ?:$table->setPrimaryKey(['oxid']); + + if($table->hasIndex('OXUSERID') === false){ + $table->addUniqueIndex(['OXUSERID'], 'OXUSERID'); + } + } + + /** + * @param Schema $schema + * @return void + * @throws SchemaException + */ + public function addTotpBackupCodesTable(Schema $schema): void + { + $table = !$schema->hasTable('d3totp_backupcodes') ? + $schema->createTable('d3totp_backupcodes')->setComment('totp backup codes') : + $schema->getTable('d3totp_backupcodes'); + + // OXID + if (!$table->hasColumn('OXID')) { + $table->addColumn('OXID', (new StringType())->getName()) + ->setLength(32) + ->setFixed(true) + ->setNotnull(true); + } + + // OXUSERID + if (!$table->hasColumn('OXUSERID')) { + $table->addColumn('OXUSERID', (new StringType())->getName()) + ->setLength(32) + ->setFixed(true) + ->setNotnull(true) + ->setComment('User ID'); + } + + // useTotp + if (!$table->hasColumn('BACKUPCODE')) { + $table->addColumn('BACKUPCODE', (new StringType())->getName()) + ->setFixed(false) + ->setLength(64) + ->setNotnull(true); + } + + // oxtimestamp + if (!$table->hasColumn('OXTIMESTAMP')) { + $table->addColumn('OXTIMESTAMP', (new DateTimeType())->getName()) + ->setNotnull(true) + ->setDefault('CURRENT_TIMESTAMP'); + } + + $table->hasPrimaryKey() ?:$table->setPrimaryKey(['oxid']); + + if($table->hasIndex('OXUSERID') === false){ + $table->addIndex(['OXUSERID'], 'OXUSERID'); + } + + if($table->hasIndex('BACKUPCODE') === false){ + $table->addIndex(['BACKUPCODE'], 'BACKUPCODE'); + } + } + + public function down(Schema $schema) : void + { + // this down() migration is auto-generated, please modify it to your needs + } +} diff --git a/migrations/migrations.yml b/migrations/migrations.yml new file mode 100644 index 0000000..a5b19a1 --- /dev/null +++ b/migrations/migrations.yml @@ -0,0 +1,4 @@ +name: D3 Twofactor Onetime Password +migrations_namespace: D3\Totp\Migrations +table_name: d3migrations_totp +migrations_directory: data \ No newline at end of file