diff --git a/.gitignore b/.gitignore index 70316a2746..feefe82b03 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /config/database.ini /logs/sql.log /logs/application.log +/db/ /files/ /modules/ /themes/ @@ -17,4 +18,4 @@ .sass-cache .DS_Store /application/language/debug.* -/static/ \ No newline at end of file +/static/ diff --git a/application/Module.php b/application/Module.php index d96fdba439..bec3704944 100644 --- a/application/Module.php +++ b/application/Module.php @@ -11,6 +11,7 @@ use Omeka\Entity\Item; use Omeka\Entity\Media; use Omeka\Module\AbstractModule; +use Omeka\Service\ConnectionFactory; use Laminas\EventManager\Event as ZendEvent; use Laminas\EventManager\SharedEventManagerInterface; use Laminas\Form\Element; @@ -735,7 +736,14 @@ public function searchFulltext(ZendEvent $event) } $qb = $event->getParam('queryBuilder'); - $match = 'MATCH(omeka_fulltext_search.title, omeka_fulltext_search.text) AGAINST (:omeka_fulltext_search)'; + $conn = $this->getServiceLocator()->get('Omeka\Connection'); + $isSqlite = ConnectionFactory::isSqlite($conn); + + if ($isSqlite) { + $match = '(omeka_fulltext_search.title LIKE :omeka_fulltext_search OR omeka_fulltext_search.text LIKE :omeka_fulltext_search)'; + } else { + $match = 'MATCH(omeka_fulltext_search.title, omeka_fulltext_search.text) AGAINST (:omeka_fulltext_search IN BOOLEAN MODE)'; + } if ('api.search.query' === $event->getName()) { @@ -743,7 +751,11 @@ public function searchFulltext(ZendEvent $event) // during "api.search.query" because "api.search.query.finalize" // happens after we've already gotten the total count. - $qb->setParameter('omeka_fulltext_search', $query['fulltext_search']); + if ($isSqlite) { + $qb->setParameter('omeka_fulltext_search', '%' . $query['fulltext_search'] . '%'); + } else { + $qb->setParameter('omeka_fulltext_search', $query['fulltext_search']); + } $joinConditions = sprintf( 'omeka_fulltext_search.id = omeka_root.id AND omeka_fulltext_search.resource = %s', @@ -752,7 +764,7 @@ public function searchFulltext(ZendEvent $event) $qb->innerJoin('Omeka\Entity\FulltextSearch', 'omeka_fulltext_search', 'WITH', $joinConditions); // Filter out resources with no similarity. - $qb->andWhere(sprintf('%s > 0', $match)); + $qb->andWhere($match); // Set visibility constraints. $acl = $this->getServiceLocator()->get('Omeka\Acl'); diff --git a/application/asset/js/resource-form.js b/application/asset/js/resource-form.js index 335526ac61..0e5ae8bd49 100644 --- a/application/asset/js/resource-form.js +++ b/application/asset/js/resource-form.js @@ -435,7 +435,6 @@ const resourceTemplate = $('#item-stub-resource-template'); const resourceClass = $('#item-stub-resource-class'); const propertyValues = $('#item-stub-property-values'); - console.log(itemStubForm.data('resourceTemplateUrl')); const resourceTemplateUrl = itemStubForm.data('resourceTemplateUrl') + '/' + resourceTemplate.val(); $.get(resourceTemplateUrl, function(rtData) { const templateResourceClass = rtData['o:resource_class']; diff --git a/application/data/install/schema.sqlite.sql b/application/data/install/schema.sqlite.sql new file mode 100644 index 0000000000..9499c88e1b --- /dev/null +++ b/application/data/install/schema.sqlite.sql @@ -0,0 +1,389 @@ +PRAGMA foreign_keys = OFF; +CREATE TABLE `api_key` ( + `id` varchar(32) NOT NULL, + `owner_id` INTEGER NOT NULL, + `label` varchar(255) NOT NULL, + `credential_hash` varchar(60) NOT NULL, + `last_ip` BLOB DEFAULT NULL, + `last_accessed` datetime DEFAULT NULL, + `created` datetime NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `FK_C912ED9D7E3C61F9` FOREIGN KEY (`owner_id`) REFERENCES `user` (`id`) +); +CREATE INDEX `IDX_C912ED9D7E3C61F9` ON `api_key` (`owner_id`); +CREATE TABLE `asset` ( + `id` INTEGER NOT NULL, + `owner_id` INTEGER DEFAULT NULL, + `name` varchar(255) NOT NULL, + `media_type` varchar(255) NOT NULL, + `storage_id` varchar(190) NOT NULL, + `extension` varchar(255) DEFAULT NULL, + `alt_text` TEXT, + PRIMARY KEY (`id`), + UNIQUE (`storage_id`), + CONSTRAINT `FK_2AF5A5C7E3C61F9` FOREIGN KEY (`owner_id`) REFERENCES `user` (`id`) ON DELETE SET NULL +); +CREATE INDEX `IDX_2AF5A5C7E3C61F9` ON `asset` (`owner_id`); +CREATE TABLE `fulltext_search` ( + `id` INTEGER NOT NULL, + `resource` varchar(190) NOT NULL, + `owner_id` INTEGER DEFAULT NULL, + `is_public` INTEGER NOT NULL, + `title` TEXT, + `text` TEXT, + PRIMARY KEY (`id`,`resource`), + CONSTRAINT `FK_AA31FE4A7E3C61F9` FOREIGN KEY (`owner_id`) REFERENCES `user` (`id`) ON DELETE SET NULL +); +CREATE INDEX `IDX_AA31FE4A7E3C61F9` ON `fulltext_search` (`owner_id`); +CREATE TABLE `item` ( + `id` INTEGER NOT NULL, + `primary_media_id` INTEGER DEFAULT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `FK_1F1B251EBF396750` FOREIGN KEY (`id`) REFERENCES `resource` (`id`) ON DELETE CASCADE, + CONSTRAINT `FK_1F1B251ECBE0B084` FOREIGN KEY (`primary_media_id`) REFERENCES `media` (`id`) ON DELETE SET NULL +); +CREATE INDEX `IDX_1F1B251ECBE0B084` ON `item` (`primary_media_id`); +CREATE TABLE `item_item_set` ( + `item_id` INTEGER NOT NULL, + `item_set_id` INTEGER NOT NULL, + PRIMARY KEY (`item_id`,`item_set_id`), + CONSTRAINT `FK_6D0C9625126F525E` FOREIGN KEY (`item_id`) REFERENCES `item` (`id`) ON DELETE CASCADE, + CONSTRAINT `FK_6D0C9625960278D7` FOREIGN KEY (`item_set_id`) REFERENCES `item_set` (`id`) ON DELETE CASCADE +); +CREATE INDEX `IDX_6D0C9625126F525E` ON `item_item_set` (`item_id`); +CREATE INDEX `IDX_6D0C9625960278D7` ON `item_item_set` (`item_set_id`); +CREATE TABLE `item_set` ( + `id` INTEGER NOT NULL, + `is_open` INTEGER NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `FK_1015EEEBF396750` FOREIGN KEY (`id`) REFERENCES `resource` (`id`) ON DELETE CASCADE +); +CREATE TABLE `item_site` ( + `item_id` INTEGER NOT NULL, + `site_id` INTEGER NOT NULL, + PRIMARY KEY (`item_id`,`site_id`), + CONSTRAINT `FK_A1734D1F126F525E` FOREIGN KEY (`item_id`) REFERENCES `item` (`id`) ON DELETE CASCADE, + CONSTRAINT `FK_A1734D1FF6BD1646` FOREIGN KEY (`site_id`) REFERENCES `site` (`id`) ON DELETE CASCADE +); +CREATE INDEX `IDX_A1734D1F126F525E` ON `item_site` (`item_id`); +CREATE INDEX `IDX_A1734D1FF6BD1646` ON `item_site` (`site_id`); +CREATE TABLE `job` ( + `id` INTEGER NOT NULL, + `owner_id` INTEGER DEFAULT NULL, + `pid` varchar(255) DEFAULT NULL, + `status` varchar(255) DEFAULT NULL, + `class` varchar(255) NOT NULL, + `args` TEXT, + `log` TEXT, + `started` datetime NOT NULL, + `ended` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `FK_FBD8E0F87E3C61F9` FOREIGN KEY (`owner_id`) REFERENCES `user` (`id`) ON DELETE SET NULL +); +CREATE INDEX `IDX_FBD8E0F87E3C61F9` ON `job` (`owner_id`); +CREATE TABLE `media` ( + `id` INTEGER NOT NULL, + `item_id` INTEGER NOT NULL, + `ingester` varchar(255) NOT NULL, + `renderer` varchar(255) NOT NULL, + `data` TEXT, + `source` TEXT, + `media_type` varchar(190) DEFAULT NULL, + `storage_id` varchar(190) DEFAULT NULL, + `extension` varchar(255) DEFAULT NULL, + `sha256` char(64) DEFAULT NULL, + `size` INTEGER DEFAULT NULL, + `has_original` INTEGER NOT NULL, + `has_thumbnails` INTEGER NOT NULL, + `position` INTEGER DEFAULT NULL, + `lang` varchar(190) DEFAULT NULL, + `alt_text` TEXT, + PRIMARY KEY (`id`), + UNIQUE (`storage_id`), + CONSTRAINT `FK_6A2CA10C126F525E` FOREIGN KEY (`item_id`) REFERENCES `item` (`id`), + CONSTRAINT `FK_6A2CA10CBF396750` FOREIGN KEY (`id`) REFERENCES `resource` (`id`) ON DELETE CASCADE +); +CREATE INDEX `IDX_6A2CA10C126F525E` ON `media` (`item_id`); +CREATE INDEX `item_position` ON `media` (`item_id`, `position`); +CREATE INDEX `media_type` ON `media` (`media_type`); +CREATE TABLE `migration` ( + `version` varchar(16) NOT NULL, + PRIMARY KEY (`version`) +); +CREATE TABLE `module` ( + `id` varchar(190) NOT NULL, + `is_active` INTEGER NOT NULL, + `version` varchar(255) NOT NULL, + PRIMARY KEY (`id`) +); +CREATE TABLE `password_creation` ( + `id` varchar(32) NOT NULL, + `user_id` INTEGER NOT NULL, + `created` datetime NOT NULL, + `activate` INTEGER NOT NULL, + PRIMARY KEY (`id`), + UNIQUE (`user_id`), + CONSTRAINT `FK_C77917B4A76ED395` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE +); +CREATE TABLE `property` ( + `id` INTEGER NOT NULL, + `owner_id` INTEGER DEFAULT NULL, + `vocabulary_id` INTEGER NOT NULL, + `local_name` varchar(190) NOT NULL, + `label` varchar(255) NOT NULL, + `comment` TEXT, + PRIMARY KEY (`id`), + UNIQUE (`vocabulary_id`, `local_name`), + CONSTRAINT `FK_8BF21CDE7E3C61F9` FOREIGN KEY (`owner_id`) REFERENCES `user` (`id`) ON DELETE SET NULL, + CONSTRAINT `FK_8BF21CDEAD0E05F6` FOREIGN KEY (`vocabulary_id`) REFERENCES `vocabulary` (`id`) +); +CREATE INDEX `IDX_8BF21CDE7E3C61F9` ON `property` (`owner_id`); +CREATE INDEX `IDX_8BF21CDEAD0E05F6` ON `property` (`vocabulary_id`); +CREATE TABLE `resource` ( + `id` INTEGER NOT NULL, + `owner_id` INTEGER DEFAULT NULL, + `resource_class_id` INTEGER DEFAULT NULL, + `resource_template_id` INTEGER DEFAULT NULL, + `thumbnail_id` INTEGER DEFAULT NULL, + `title` TEXT, + `is_public` INTEGER NOT NULL, + `created` datetime NOT NULL, + `modified` datetime DEFAULT NULL, + `resource_type` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `FK_BC91F41616131EA` FOREIGN KEY (`resource_template_id`) REFERENCES `resource_template` (`id`) ON DELETE SET NULL, + CONSTRAINT `FK_BC91F416448CC1BD` FOREIGN KEY (`resource_class_id`) REFERENCES `resource_class` (`id`) ON DELETE SET NULL, + CONSTRAINT `FK_BC91F4167E3C61F9` FOREIGN KEY (`owner_id`) REFERENCES `user` (`id`) ON DELETE SET NULL, + CONSTRAINT `FK_BC91F416FDFF2E92` FOREIGN KEY (`thumbnail_id`) REFERENCES `asset` (`id`) ON DELETE SET NULL +); +CREATE INDEX `IDX_BC91F4167E3C61F9` ON `resource` (`owner_id`); +CREATE INDEX `IDX_BC91F416448CC1BD` ON `resource` (`resource_class_id`); +CREATE INDEX `IDX_BC91F41616131EA` ON `resource` (`resource_template_id`); +CREATE INDEX `IDX_BC91F416FDFF2E92` ON `resource` (`thumbnail_id`); +CREATE INDEX `title` ON `resource` (`title`); +CREATE INDEX `is_public_resource` ON `resource` (`is_public`); +CREATE TABLE `resource_class` ( + `id` INTEGER NOT NULL, + `owner_id` INTEGER DEFAULT NULL, + `vocabulary_id` INTEGER NOT NULL, + `local_name` varchar(190) NOT NULL, + `label` varchar(255) NOT NULL, + `comment` TEXT, + PRIMARY KEY (`id`), + UNIQUE (`vocabulary_id`, `local_name`), + CONSTRAINT `FK_C6F063AD7E3C61F9` FOREIGN KEY (`owner_id`) REFERENCES `user` (`id`) ON DELETE SET NULL, + CONSTRAINT `FK_C6F063ADAD0E05F6` FOREIGN KEY (`vocabulary_id`) REFERENCES `vocabulary` (`id`) +); +CREATE INDEX `IDX_C6F063AD7E3C61F9` ON `resource_class` (`owner_id`); +CREATE INDEX `IDX_C6F063ADAD0E05F6` ON `resource_class` (`vocabulary_id`); +CREATE TABLE `resource_template` ( + `id` INTEGER NOT NULL, + `owner_id` INTEGER DEFAULT NULL, + `resource_class_id` INTEGER DEFAULT NULL, + `title_property_id` INTEGER DEFAULT NULL, + `description_property_id` INTEGER DEFAULT NULL, + `label` varchar(190) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE (`label`), + CONSTRAINT `FK_39ECD52E448CC1BD` FOREIGN KEY (`resource_class_id`) REFERENCES `resource_class` (`id`) ON DELETE SET NULL, + CONSTRAINT `FK_39ECD52E724734A3` FOREIGN KEY (`title_property_id`) REFERENCES `property` (`id`) ON DELETE SET NULL, + CONSTRAINT `FK_39ECD52E7E3C61F9` FOREIGN KEY (`owner_id`) REFERENCES `user` (`id`) ON DELETE SET NULL, + CONSTRAINT `FK_39ECD52EB84E0D1D` FOREIGN KEY (`description_property_id`) REFERENCES `property` (`id`) ON DELETE SET NULL +); +CREATE INDEX `IDX_39ECD52E7E3C61F9` ON `resource_template` (`owner_id`); +CREATE INDEX `IDX_39ECD52E448CC1BD` ON `resource_template` (`resource_class_id`); +CREATE INDEX `IDX_39ECD52E724734A3` ON `resource_template` (`title_property_id`); +CREATE INDEX `IDX_39ECD52EB84E0D1D` ON `resource_template` (`description_property_id`); +CREATE TABLE `resource_template_property` ( + `id` INTEGER NOT NULL, + `resource_template_id` INTEGER NOT NULL, + `property_id` INTEGER NOT NULL, + `alternate_label` varchar(255) DEFAULT NULL, + `alternate_comment` TEXT, + `position` INTEGER DEFAULT NULL, + `data_type` TEXT, + `is_required` INTEGER NOT NULL, + `is_private` INTEGER NOT NULL, + `default_lang` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE (`resource_template_id`, `property_id`), + CONSTRAINT `FK_4689E2F116131EA` FOREIGN KEY (`resource_template_id`) REFERENCES `resource_template` (`id`), + CONSTRAINT `FK_4689E2F1549213EC` FOREIGN KEY (`property_id`) REFERENCES `property` (`id`) ON DELETE CASCADE +); +CREATE INDEX `IDX_4689E2F116131EA` ON `resource_template_property` (`resource_template_id`); +CREATE INDEX `IDX_4689E2F1549213EC` ON `resource_template_property` (`property_id`); +CREATE TABLE `session` ( + `id` varchar(190) NOT NULL, + `data` BLOB NOT NULL, + `modified` INTEGER NOT NULL, + PRIMARY KEY (`id`) +); +CREATE TABLE `setting` ( + `id` varchar(190) NOT NULL, + `value` TEXT NOT NULL, + PRIMARY KEY (`id`) +); +CREATE TABLE `site` ( + `id` INTEGER NOT NULL, + `thumbnail_id` INTEGER DEFAULT NULL, + `homepage_id` INTEGER DEFAULT NULL, + `owner_id` INTEGER DEFAULT NULL, + `slug` varchar(190) NOT NULL, + `theme` varchar(190) NOT NULL, + `title` varchar(190) NOT NULL, + `summary` TEXT, + `navigation` TEXT NOT NULL, + `item_pool` TEXT NOT NULL, + `created` datetime NOT NULL, + `modified` datetime DEFAULT NULL, + `is_public` INTEGER NOT NULL, + `assign_new_items` INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE (`slug`), + UNIQUE (`homepage_id`), + CONSTRAINT `FK_694309E4571EDDA` FOREIGN KEY (`homepage_id`) REFERENCES `site_page` (`id`) ON DELETE SET NULL, + CONSTRAINT `FK_694309E47E3C61F9` FOREIGN KEY (`owner_id`) REFERENCES `user` (`id`) ON DELETE SET NULL, + CONSTRAINT `FK_694309E4FDFF2E92` FOREIGN KEY (`thumbnail_id`) REFERENCES `asset` (`id`) ON DELETE SET NULL +); +CREATE INDEX `IDX_694309E4FDFF2E92` ON `site` (`thumbnail_id`); +CREATE INDEX `IDX_694309E47E3C61F9` ON `site` (`owner_id`); +CREATE TABLE `site_block_attachment` ( + `id` INTEGER NOT NULL, + `block_id` INTEGER NOT NULL, + `item_id` INTEGER DEFAULT NULL, + `media_id` INTEGER DEFAULT NULL, + `caption` TEXT NOT NULL, + `position` INTEGER NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `FK_236473FE126F525E` FOREIGN KEY (`item_id`) REFERENCES `item` (`id`) ON DELETE SET NULL, + CONSTRAINT `FK_236473FEE9ED820C` FOREIGN KEY (`block_id`) REFERENCES `site_page_block` (`id`), + CONSTRAINT `FK_236473FEEA9FDD75` FOREIGN KEY (`media_id`) REFERENCES `media` (`id`) ON DELETE SET NULL +); +CREATE INDEX `IDX_236473FEE9ED820C` ON `site_block_attachment` (`block_id`); +CREATE INDEX `IDX_236473FE126F525E` ON `site_block_attachment` (`item_id`); +CREATE INDEX `IDX_236473FEEA9FDD75` ON `site_block_attachment` (`media_id`); +CREATE INDEX `block_position` ON `site_block_attachment` (`block_id`, `position`); +CREATE TABLE `site_item_set` ( + `id` INTEGER NOT NULL, + `site_id` INTEGER NOT NULL, + `item_set_id` INTEGER NOT NULL, + `position` INTEGER DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE (`site_id`, `item_set_id`), + CONSTRAINT `FK_D4CE134960278D7` FOREIGN KEY (`item_set_id`) REFERENCES `item_set` (`id`) ON DELETE CASCADE, + CONSTRAINT `FK_D4CE134F6BD1646` FOREIGN KEY (`site_id`) REFERENCES `site` (`id`) ON DELETE CASCADE +); +CREATE INDEX `IDX_D4CE134F6BD1646` ON `site_item_set` (`site_id`); +CREATE INDEX `IDX_D4CE134960278D7` ON `site_item_set` (`item_set_id`); +CREATE INDEX `position` ON `site_item_set` (`position`); +CREATE TABLE `site_page` ( + `id` INTEGER NOT NULL, + `site_id` INTEGER NOT NULL, + `slug` varchar(190) NOT NULL, + `title` varchar(190) NOT NULL, + `is_public` INTEGER NOT NULL, + `layout` varchar(255) DEFAULT NULL, + `layout_data` TEXT, + `created` datetime NOT NULL, + `modified` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE (`site_id`, `slug`), + CONSTRAINT `FK_2F900BD9F6BD1646` FOREIGN KEY (`site_id`) REFERENCES `site` (`id`) +); +CREATE INDEX `is_public_site_page` ON `site_page` (`is_public`); +CREATE INDEX `IDX_2F900BD9F6BD1646` ON `site_page` (`site_id`); +CREATE TABLE `site_page_block` ( + `id` INTEGER NOT NULL, + `page_id` INTEGER NOT NULL, + `layout` varchar(80) NOT NULL, + `data` TEXT NOT NULL, + `layout_data` TEXT, + `position` INTEGER NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `FK_C593E731C4663E4` FOREIGN KEY (`page_id`) REFERENCES `site_page` (`id`) +); +CREATE INDEX `IDX_C593E731C4663E4` ON `site_page_block` (`page_id`); +CREATE INDEX `page_position` ON `site_page_block` (`page_id`, `position`); +CREATE TABLE `site_permission` ( + `id` INTEGER NOT NULL, + `site_id` INTEGER NOT NULL, + `user_id` INTEGER NOT NULL, + `role` varchar(80) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE (`site_id`, `user_id`), + CONSTRAINT `FK_C0401D6FA76ED395` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, + CONSTRAINT `FK_C0401D6FF6BD1646` FOREIGN KEY (`site_id`) REFERENCES `site` (`id`) ON DELETE CASCADE +); +CREATE INDEX `IDX_C0401D6FF6BD1646` ON `site_permission` (`site_id`); +CREATE INDEX `IDX_C0401D6FA76ED395` ON `site_permission` (`user_id`); +CREATE TABLE `site_setting` ( + `id` varchar(190) NOT NULL, + `site_id` INTEGER NOT NULL, + `value` TEXT NOT NULL, + PRIMARY KEY (`id`,`site_id`), + CONSTRAINT `FK_64D05A53F6BD1646` FOREIGN KEY (`site_id`) REFERENCES `site` (`id`) ON DELETE CASCADE +); +CREATE INDEX `IDX_64D05A53F6BD1646` ON `site_setting` (`site_id`); +CREATE TABLE `user` ( + `id` INTEGER NOT NULL, + `email` varchar(190) NOT NULL, + `name` varchar(190) NOT NULL, + `created` datetime NOT NULL, + `modified` datetime DEFAULT NULL, + `password_hash` varchar(60) DEFAULT NULL, + `role` varchar(190) NOT NULL, + `is_active` INTEGER NOT NULL, + PRIMARY KEY (`id`), + UNIQUE (`email`) +); +CREATE TABLE `user_setting` ( + `id` varchar(190) NOT NULL, + `user_id` INTEGER NOT NULL, + `value` TEXT NOT NULL, + PRIMARY KEY (`id`,`user_id`), + CONSTRAINT `FK_C779A692A76ED395` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE +); +CREATE INDEX `IDX_C779A692A76ED395` ON `user_setting` (`user_id`); +CREATE TABLE `value` ( + `id` INTEGER NOT NULL, + `resource_id` INTEGER NOT NULL, + `property_id` INTEGER NOT NULL, + `value_resource_id` INTEGER DEFAULT NULL, + `value_annotation_id` INTEGER DEFAULT NULL, + `type` varchar(255) NOT NULL, + `lang` varchar(255) DEFAULT NULL, + `value` TEXT, + `uri` TEXT, + `is_public` INTEGER NOT NULL, + PRIMARY KEY (`id`), + UNIQUE (`value_annotation_id`), + CONSTRAINT `FK_1D7758344BC72506` FOREIGN KEY (`value_resource_id`) REFERENCES `resource` (`id`) ON DELETE CASCADE, + CONSTRAINT `FK_1D775834549213EC` FOREIGN KEY (`property_id`) REFERENCES `property` (`id`) ON DELETE CASCADE, + CONSTRAINT `FK_1D77583489329D25` FOREIGN KEY (`resource_id`) REFERENCES `resource` (`id`), + CONSTRAINT `FK_1D7758349B66727E` FOREIGN KEY (`value_annotation_id`) REFERENCES `value_annotation` (`id`) ON DELETE SET NULL +); +CREATE INDEX `IDX_1D77583489329D25` ON `value` (`resource_id`); +CREATE INDEX `IDX_1D775834549213EC` ON `value` (`property_id`); +CREATE INDEX `IDX_1D7758344BC72506` ON `value` (`value_resource_id`); +CREATE INDEX `value_value` ON `value` (`value`); +CREATE INDEX `uri` ON `value` (`uri`); +CREATE INDEX `is_public_value` ON `value` (`is_public`); +CREATE TABLE `value_annotation` ( + `id` INTEGER NOT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `FK_C03BA4EBF396750` FOREIGN KEY (`id`) REFERENCES `resource` (`id`) ON DELETE CASCADE +); +CREATE TABLE `vocabulary` ( + `id` INTEGER NOT NULL, + `owner_id` INTEGER DEFAULT NULL, + `namespace_uri` varchar(190) NOT NULL, + `prefix` varchar(190) NOT NULL, + `label` varchar(255) NOT NULL, + `comment` TEXT, + PRIMARY KEY (`id`), + UNIQUE (`namespace_uri`), + UNIQUE (`prefix`), + CONSTRAINT `FK_9099C97B7E3C61F9` FOREIGN KEY (`owner_id`) REFERENCES `user` (`id`) ON DELETE SET NULL +); +CREATE INDEX `IDX_9099C97B7E3C61F9` ON `vocabulary` (`owner_id`); +PRAGMA foreign_keys = ON; diff --git a/application/src/Api/Adapter/AbstractResourceEntityAdapter.php b/application/src/Api/Adapter/AbstractResourceEntityAdapter.php index f3dc2215b9..b0323bbc36 100644 --- a/application/src/Api/Adapter/AbstractResourceEntityAdapter.php +++ b/application/src/Api/Adapter/AbstractResourceEntityAdapter.php @@ -139,10 +139,18 @@ public function sortQuery(QueryBuilder $qb, array $query) "omeka_root.values", $valuesAlias, 'WITH', $qb->expr()->eq("$valuesAlias.property", $property->getId()) ); - $qb->addOrderBy( - "GROUP_CONCAT($valuesAlias.value ORDER BY $valuesAlias.id)", - $query['sort_order'] - ); + $conn = $this->getServiceLocator()->get('Omeka\Connection'); + if (\Omeka\Service\ConnectionFactory::isSqlite($conn)) { + $qb->addOrderBy( + "GROUP_CONCAT($valuesAlias.value)", + $query['sort_order'] + ); + } else { + $qb->addOrderBy( + "GROUP_CONCAT($valuesAlias.value ORDER BY $valuesAlias.id)", + $query['sort_order'] + ); + } } elseif ('resource_class_label' == $query['sort_by']) { $resourceClassAlias = $qb->createAlias(); $qb->leftJoin("omeka_root.resourceClass", $resourceClassAlias) diff --git a/application/src/Controller/Admin/ItemController.php b/application/src/Controller/Admin/ItemController.php index 95187bcbb4..5814303f5a 100644 --- a/application/src/Controller/Admin/ItemController.php +++ b/application/src/Controller/Admin/ItemController.php @@ -243,14 +243,14 @@ public function addItemStubAction() $request = $this->getRequest(); $response = $this->getResponse(); if (!$request->isPost()) { - $response->setStatusCode(500); + $response->setStatusCode(405); return $response; } $itemData = $this->params()->fromPost(); $form = $this->getForm(ItemStubForm::class); $form->setData($itemData); if (!$form->isValid()) { - $response->setStatusCode(500); + $response->setStatusCode(422); $response->setContent(json_encode($form->getMessages())); return $response; } diff --git a/application/src/Controller/Admin/SystemInfoController.php b/application/src/Controller/Admin/SystemInfoController.php index 07b404b2ae..f87e02ffc6 100644 --- a/application/src/Controller/Admin/SystemInfoController.php +++ b/application/src/Controller/Admin/SystemInfoController.php @@ -4,6 +4,7 @@ use PDO; use Doctrine\DBAL\Connection; use Omeka\Module; +use Omeka\Service\ConnectionFactory; use Omeka\Module\Manager as Modules; use Omeka\Stdlib\Cli; use Laminas\Mvc\Controller\AbstractActionController; @@ -55,7 +56,7 @@ public function browseAction() protected function getSystemInfo() { $conn = $this->connection->getWrappedConnection(); - $mode = $this->connection->fetchColumn('SELECT @@sql_mode'); + $isSqlite = ConnectionFactory::isSqlite($this->connection); $extensions = get_loaded_extensions(); natcasesort($extensions); @@ -73,11 +74,23 @@ protected function getSystemInfo() 'Garbage Collection' => gc_enabled(), 'Extensions' => $extensions, ], - 'MySQL' => [ + ]; + + if ($isSqlite) { + $sqliteVersion = $this->connection->fetchColumn('SELECT sqlite_version()'); + $info['SQLite'] = [ + 'Version' => $sqliteVersion, + ]; + } else { + $mode = $this->connection->fetchColumn('SELECT @@sql_mode'); + $info['MySQL'] = [ 'Server Version' => $conn->getAttribute(PDO::ATTR_SERVER_VERSION), 'Client Version' => $conn->getAttribute(PDO::ATTR_CLIENT_VERSION), 'Mode' => explode(',', $mode), - ], + ]; + } + + $info += [ 'OS' => [ 'Version' => sprintf('%s %s %s', php_uname('s'), php_uname('r'), php_uname('m')), ], diff --git a/application/src/Db/Connection/SqliteCompatConnection.php b/application/src/Db/Connection/SqliteCompatConnection.php new file mode 100644 index 0000000000..0bd2a1b949 --- /dev/null +++ b/application/src/Db/Connection/SqliteCompatConnection.php @@ -0,0 +1,267 @@ +translateSql($sql); + if ($statements === null) { + return 0; + } + $result = 0; + foreach ($statements as $stmt) { + $result = parent::exec($stmt); + } + return $result; + } + + public function query(...$args) + { + if (isset($args[0]) && is_string($args[0])) { + $last = $this->execLeading($args[0]); + if ($last === null) { + return parent::query('SELECT 1 WHERE 0'); + } + $args[0] = $last; + } + return parent::query(...$args); + } + + public function executeQuery($sql, array $params = [], $types = [], ?QueryCacheProfile $qcp = null) + { + $last = $this->execLeading($sql); + if ($last === null) { + return parent::executeQuery('SELECT 1 WHERE 0', [], []); + } + return parent::executeQuery($last, $params, $types, $qcp); + } + + public function executeStatement($sql, array $params = [], array $types = []) + { + $last = $this->execLeading($sql); + if ($last === null) { + return 0; + } + return parent::executeStatement($last, $params, $types); + } + + /** + * Translate and execute all leading statements, returning the final one. + * + * @return string|null The last translated statement to execute, or null to skip. + */ + private function execLeading(string $sql): ?string + { + $statements = $this->translateSql($sql); + if ($statements === null) { + return null; + } + for ($i = 0, $last = count($statements) - 1; $i < $last; $i++) { + parent::exec($statements[$i]); + } + return $statements[$last]; + } + + /** + * Translate MySQL-specific SQL to SQLite equivalents. + * + * Returns null to suppress execution entirely (e.g. SET NAMES). + * Returns a single-element array for direct translations. + * Returns a multi-element array for compound statements (e.g. CREATE TABLE + * followed by CREATE INDEX statements). + * + * @return string[]|null + */ + protected function translateSql(string $sql): ?array + { + $trimmed = trim($sql, " \t\n\r\0\x0B;"); + + // Fast path: most queries don't need translation. + $firstSpace = strpos($trimmed, ' '); + $firstWord = $firstSpace !== false ? strtoupper(substr($trimmed, 0, $firstSpace)) : strtoupper($trimmed); + if (!in_array($firstWord, self::TRANSLATABLE_KEYWORDS, true)) { + return [$sql]; + } + + if (preg_match('/^SHOW\s+(FULL\s+)?TABLES/i', $trimmed)) { + return ["SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"]; + } + + if (preg_match('/^SET\s+FOREIGN_KEY_CHECKS\s*=\s*0/i', $trimmed)) { + return ['PRAGMA foreign_keys = OFF']; + } + if (preg_match('/^SET\s+FOREIGN_KEY_CHECKS\s*=\s*1/i', $trimmed)) { + return ['PRAGMA foreign_keys = ON']; + } + + // SET NAMES and other unsupported SET statements are no-ops for SQLite. + if (preg_match('/^SET\s+/i', $trimmed)) { + return null; + } + + if (preg_match('/^(?:SHOW\s+COLUMNS\s+FROM|DESCRIBE|DESC)\s+[`"\']?(\w+)[`"\']?/i', $trimmed, $m)) { + return ['PRAGMA table_info(' . $m[1] . ')']; + } + + if (preg_match('/^TRUNCATE\s+(?:TABLE\s+)?[`"\']?(\w+)[`"\']?/i', $trimmed, $m)) { + return ['DELETE FROM ' . $m[1]]; + } + + // Translate MySQL CREATE TABLE to SQLite-compatible DDL. + if (preg_match('/^CREATE\s+TABLE\s+/i', $trimmed)) { + return $this->translateCreateTable($trimmed); + } + + // SQLite doesn't support ALTER TABLE ADD CONSTRAINT FOREIGN KEY. + // Skip these since FKs are already defined inline in CREATE TABLE. + if (preg_match('/^ALTER\s+TABLE\s+.+\s+ADD\s+CONSTRAINT\s+.+\s+FOREIGN\s+KEY/i', $trimmed)) { + return null; + } + + // Compound statements like "SET FOREIGN_KEY_CHECKS=0; DROP TABLE x". + if (str_contains($trimmed, ';')) { + $parts = array_filter(array_map('trim', explode(';', $trimmed))); + if (count($parts) > 1) { + $result = []; + foreach ($parts as $part) { + $translated = $this->translateSql($part); + if ($translated !== null) { + array_push($result, ...$translated); + } + } + return $result ?: null; + } + } + + return [$sql]; + } + + /** + * Translate a MySQL CREATE TABLE statement to SQLite-compatible DDL. + * + * Extracts inline INDEX/KEY definitions into separate CREATE INDEX + * statements and strips MySQL-specific column options (AUTO_INCREMENT, + * COMMENT, COLLATE, ENGINE, CHARSET, etc.). + * + * @return string[] + */ + private function translateCreateTable(string $sql): array + { + // Extract table name. + if (!preg_match('/^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?\s*\(/i', $sql, $m)) { + return [$sql]; + } + $tableName = $m[1]; + + // Find the body between the outermost parentheses. + $openParen = strpos($sql, '('); + $closeParen = strrpos($sql, ')'); + if ($openParen === false || $closeParen === false) { + return [$sql]; + } + $body = substr($sql, $openParen + 1, $closeParen - $openParen - 1); + + // Split body into lines by comma, respecting parenthesized expressions. + $lines = $this->splitByComma($body); + + $columns = []; + $createIndexes = []; + + foreach ($lines as $line) { + $line = trim($line); + if ($line === '') { + continue; + } + + // Skip FULLTEXT KEY/INDEX (handled in application code). + if (preg_match('/^\s*FULLTEXT\s+(KEY|INDEX)\s+/i', $line)) { + continue; + } + + // Extract KEY/INDEX into separate CREATE INDEX statements. + if (preg_match('/^\s*(?:UNIQUE\s+)?(?:KEY|INDEX)\s+[`"\']?(\w+)[`"\']?\s*(\(.*\))/i', $line, $km)) { + $isUnique = preg_match('/^\s*UNIQUE/i', $line) ? 'UNIQUE ' : ''; + $indexName = $km[1]; + // Strip prefix lengths like col(191) → col (SQLite doesn't support them). + $indexCols = preg_replace('/(\w)`?\s*\(\d+\)/', '$1`', $km[2]); + $createIndexes[] = "CREATE {$isUnique}INDEX `{$indexName}` ON `{$tableName}` {$indexCols}"; + continue; + } + + // Keep CONSTRAINT, PRIMARY KEY, and column definitions. + // Apply column-level translations. + $line = $this->translateColumnDef($line); + $columns[] = $line; + } + + $ifNotExists = preg_match('/^CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS/i', $sql) ? 'IF NOT EXISTS ' : ''; + $columnsStr = implode(",\n ", $columns); + $result = ["CREATE TABLE {$ifNotExists}`{$tableName}` (\n {$columnsStr}\n)"]; + + // Append CREATE INDEX statements. + foreach ($createIndexes as $idx) { + $result[] = $idx; + } + + return $result; + } + + /** + * Translate a single column definition or constraint from MySQL to SQLite. + */ + private function translateColumnDef(string $line): string + { + $line = preg_replace('/\b(?:TINY|SMALL|MEDIUM|BIG)?INT\b(?:\s*\(\d+\))?/i', 'INTEGER', $line); + $line = preg_replace('/\b(?:LONG|MEDIUM|TINY)TEXT\b/i', 'TEXT', $line); + $line = preg_replace('/\b(?:VAR)?BINARY\b(?:\s*\(\d+\))?/i', 'BLOB', $line); + $line = preg_replace('/\b(?:LONG|MEDIUM|TINY)BLOB\b/i', 'BLOB', $line); + $line = preg_replace('/\s+AUTO_INCREMENT/i', '', $line); + $line = preg_replace('/\s+COLLATE\s+[`"\']?\w+[`"\']?/i', '', $line); + $line = preg_replace('/\s+COMMENT\s+(?:\'[^\']*\'|"[^"]*")/i', '', $line); + $line = preg_replace('/\s+CHARACTER\s+SET\s+\w+/i', '', $line); + $line = preg_replace('/^(\s*)UNIQUE\s+KEY\s+[`"\']?\w+[`"\']?\s*/i', '$1UNIQUE ', $line); + + return $line; + } + + /** + * Split a string by commas, respecting parenthesized expressions. + * + * @return string[] + */ + private function splitByComma(string $body): array + { + $parts = []; + $current = ''; + $depth = 0; + + for ($i = 0, $len = strlen($body); $i < $len; $i++) { + $char = $body[$i]; + if ($char === '(') { + $depth++; + } elseif ($char === ')') { + $depth--; + } elseif ($char === ',' && $depth === 0) { + $parts[] = $current; + $current = ''; + continue; + } + $current .= $char; + } + if (trim($current) !== '') { + $parts[] = $current; + } + return $parts; + } +} diff --git a/application/src/Installation/Task/InstallSchemaTask.php b/application/src/Installation/Task/InstallSchemaTask.php index b51cee1585..5c23e4bb60 100644 --- a/application/src/Installation/Task/InstallSchemaTask.php +++ b/application/src/Installation/Task/InstallSchemaTask.php @@ -3,6 +3,7 @@ use Doctrine\DBAL\DBALException; use Omeka\Installation\Installer; +use Omeka\Service\ConnectionFactory; /** * Install schema task. @@ -11,7 +12,11 @@ class InstallSchemaTask implements TaskInterface { public function perform(Installer $installer) { - $schemaPath = OMEKA_PATH . '/application/data/install/schema.sql'; + $connection = $installer->getServiceLocator()->get('Omeka\Connection'); + $isSqlite = ConnectionFactory::isSqlite($connection); + + $schemaFile = $isSqlite ? 'schema.sqlite.sql' : 'schema.sql'; + $schemaPath = OMEKA_PATH . '/application/data/install/' . $schemaFile; if (!is_readable($schemaPath)) { $installer->addError('Could not read the schema installation file.'); return; @@ -19,7 +24,6 @@ public function perform(Installer $installer) $schema = file_get_contents($schemaPath); $statements = explode(';', $schema); - $connection = $installer->getServiceLocator()->get('Omeka\Connection'); try { foreach ($statements as $statement) { $statement = trim($statement); diff --git a/application/src/Job/UpdateSiteItems.php b/application/src/Job/UpdateSiteItems.php index 863da866fe..ce397bcdeb 100644 --- a/application/src/Job/UpdateSiteItems.php +++ b/application/src/Job/UpdateSiteItems.php @@ -3,6 +3,7 @@ use Doctrine\DBAL\Connection; use Omeka\Job\Exception\InvalidArgumentException; +use Omeka\Service\ConnectionFactory; /** * Update item assignments for one or more site. @@ -94,7 +95,8 @@ protected function updateSiteItems(int $siteId, array $query, string $action): v $bindValues[] = $siteId; } // Note the use of IGNORE here to prevent duplicate-key errors. - $sql = sprintf('INSERT IGNORE INTO item_site (item_id, site_id) VALUES %s', implode(',', $values)); + $insertKeyword = ConnectionFactory::isSqlite($conn) ? 'INSERT OR IGNORE' : 'INSERT IGNORE'; + $sql = sprintf('%s INTO item_site (item_id, site_id) VALUES %s', $insertKeyword, implode(',', $values)); $stmt = $conn->prepare($sql); foreach ($bindValues as $position => $value) { $stmt->bindValue($position + 1, $value); diff --git a/application/src/Service/ConnectionFactory.php b/application/src/Service/ConnectionFactory.php index e17dfd1058..72857436ad 100644 --- a/application/src/Service/ConnectionFactory.php +++ b/application/src/Service/ConnectionFactory.php @@ -4,6 +4,8 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Platforms\MySqlPlatform; +use Doctrine\DBAL\Platforms\SqlitePlatform; +use Omeka\Db\Connection\SqliteCompatConnection; use Omeka\Db\Logging\FileSqlLogger; use Laminas\ServiceManager\Factory\FactoryInterface; use Interop\Container\ContainerInterface; @@ -13,7 +15,8 @@ */ class ConnectionFactory implements FactoryInterface { - const DRIVER = 'pdo_mysql'; + const DRIVER_MYSQL = 'pdo_mysql'; + const DRIVER_SQLITE = 'pdo_sqlite'; const CHARSET = 'utf8mb4'; /** @@ -30,17 +33,28 @@ public function __invoke(ContainerInterface $serviceLocator, $requestedName, ?ar throw new Exception\ConfigException('Missing database connection configuration'); } - // Force the "generic" MySQL platform to avoid autodetecting and using exclusive features - // of newer versions - $platform = new MySqlPlatform; + $driver = $config['connection']['driver'] ?? self::DRIVER_MYSQL; - $config['connection']['driver'] = self::DRIVER; - $config['connection']['charset'] = self::CHARSET; - $config['connection']['platform'] = $platform; - $connection = DriverManager::getConnection($config['connection']); - - // Manually-set platforms must have the event manager manually injected - $platform->setEventManager($connection->getEventManager()); + if ($driver === self::DRIVER_SQLITE) { + $platform = new SqlitePlatform; + $config['connection']['driver'] = self::DRIVER_SQLITE; + $config['connection']['platform'] = $platform; + // Remove MySQL-specific options that don't apply to SQLite + unset($config['connection']['charset']); + $config['connection']['wrapperClass'] = SqliteCompatConnection::class; + $connection = DriverManager::getConnection($config['connection']); + $platform->setEventManager($connection->getEventManager()); + // Enable foreign keys and WAL mode for better performance + $connection->exec('PRAGMA foreign_keys = ON'); + $connection->exec('PRAGMA journal_mode = WAL'); + } else { + $platform = new MySqlPlatform; + $config['connection']['driver'] = self::DRIVER_MYSQL; + $config['connection']['charset'] = self::CHARSET; + $config['connection']['platform'] = $platform; + $connection = DriverManager::getConnection($config['connection']); + $platform->setEventManager($connection->getEventManager()); + } if (isset($config['connection']['log_path']) && is_file($config['connection']['log_path']) @@ -52,4 +66,12 @@ public function __invoke(ContainerInterface $serviceLocator, $requestedName, ?ar return $connection; } + + /** + * Check if the given connection uses SQLite. + */ + public static function isSqlite(Connection $connection): bool + { + return $connection->getDatabasePlatform() instanceof SqlitePlatform; + } } diff --git a/application/src/Service/EntityManagerFactory.php b/application/src/Service/EntityManagerFactory.php index 962d2c229a..2f3ed0c129 100644 --- a/application/src/Service/EntityManagerFactory.php +++ b/application/src/Service/EntityManagerFactory.php @@ -88,10 +88,13 @@ public function __invoke(ContainerInterface $serviceLocator, $requestedName, ?ar } } - // Add custom functions. - $emConfig->setCustomNumericFunctions($config['entity_manager']['functions']['numeric']); - $emConfig->setCustomStringFunctions($config['entity_manager']['functions']['string']); - $emConfig->setCustomDatetimeFunctions($config['entity_manager']['functions']['datetime']); + // Add custom functions (MySQL-specific DQL functions; skip for SQLite). + $connection = $serviceLocator->get('Omeka\Connection'); + if (!ConnectionFactory::isSqlite($connection)) { + $emConfig->setCustomNumericFunctions($config['entity_manager']['functions']['numeric']); + $emConfig->setCustomStringFunctions($config['entity_manager']['functions']['string']); + $emConfig->setCustomDatetimeFunctions($config['entity_manager']['functions']['datetime']); + } // Load proxies from different directories // HACK: Doctrine takes an integer here and just happens to do nothing (which is diff --git a/application/src/Session/SaveHandler/Db.php b/application/src/Session/SaveHandler/Db.php index 232660bc49..4769a2ab0f 100644 --- a/application/src/Session/SaveHandler/Db.php +++ b/application/src/Session/SaveHandler/Db.php @@ -3,6 +3,7 @@ use Doctrine\DBAL\Connection; use Laminas\Session\SaveHandler\SaveHandlerInterface; +use Omeka\Service\ConnectionFactory; class Db implements SaveHandlerInterface { @@ -67,8 +68,13 @@ public function read($id) #[\ReturnTypeWillChange] public function write($id, $data) { - $sql = 'INSERT INTO session (id, modified, data) VALUES (:id, :modified, :data) ' - . 'ON DUPLICATE KEY UPDATE modified = :modified, data = :data'; + if (ConnectionFactory::isSqlite($this->conn)) { + $sql = 'INSERT INTO session (id, modified, data) VALUES (:id, :modified, :data) ' + . 'ON CONFLICT(id) DO UPDATE SET modified = :modified, data = :data'; + } else { + $sql = 'INSERT INTO session (id, modified, data) VALUES (:id, :modified, :data) ' + . 'ON DUPLICATE KEY UPDATE modified = :modified, data = :data'; + } $this->conn->executeStatement($sql, [ 'id' => $id, 'modified' => time(), 'data' => $data, ]); diff --git a/application/src/Site/BlockLayout/IiifImage.php b/application/src/Site/BlockLayout/IiifImage.php index bea25a4e23..2cc7856977 100644 --- a/application/src/Site/BlockLayout/IiifImage.php +++ b/application/src/Site/BlockLayout/IiifImage.php @@ -70,7 +70,7 @@ public function onHydrate(SitePageBlock $block, ErrorStore $errorStore) $blockData['title'] = is_string($blockData['title']) ? trim($blockData['title']) : $this->defaultBlockData['title']; - $blockData['show_title'] = $blockData['show_title']; + $blockData['show_title'] = (bool) $blockData['show_title']; $block->setData($blockData); } diff --git a/application/src/Site/BlockLayout/IiifPresentation.php b/application/src/Site/BlockLayout/IiifPresentation.php index 5846cb7f25..b8f9f929f6 100644 --- a/application/src/Site/BlockLayout/IiifPresentation.php +++ b/application/src/Site/BlockLayout/IiifPresentation.php @@ -70,7 +70,7 @@ public function onHydrate(SitePageBlock $block, ErrorStore $errorStore) $blockData['title'] = is_string($blockData['title']) ? trim($blockData['title']) : $this->defaultBlockData['title']; - $blockData['show_title'] = $blockData['show_title']; + $blockData['show_title'] = (bool) $blockData['show_title']; $block->setData($blockData); } @@ -81,7 +81,7 @@ public function render(PhpRenderer $view, SitePageBlockRepresentation $block, $t return; } - return $view->partial($templateViewScript, [ + return $view->render($templateViewScript, [ 'block' => $block, 'manifestUrl' => $manifestUrl, 'title' => $block->dataValue('title'), diff --git a/application/src/Stdlib/Environment.php b/application/src/Stdlib/Environment.php index 086b2e2081..2b315fd00a 100644 --- a/application/src/Stdlib/Environment.php +++ b/application/src/Stdlib/Environment.php @@ -2,6 +2,7 @@ namespace Omeka\Stdlib; use Omeka\Module; +use Omeka\Service\ConnectionFactory; use Omeka\Settings\Settings; use Doctrine\DBAL\Connection; @@ -25,11 +26,11 @@ class Environment const MARIADB_MINIMUM_VERSION = '10.2.6'; /** - * The required PHP extensions + * The required PHP extensions (base, without DB-specific) * * (Note: the json extension is also required but must be checked separately) */ - const PHP_REQUIRED_EXTENSIONS = ['fileinfo', 'mbstring', 'PDO', 'pdo_mysql', 'xml']; + const PHP_REQUIRED_EXTENSIONS = ['fileinfo', 'mbstring', 'PDO', 'xml']; /** * @var array Environment error messages @@ -44,6 +45,7 @@ public function __construct(Connection $connection, Settings $settings) { $codeVersion = Module::VERSION; $dbVersion = $settings->get('version'); + $isSqlite = ConnectionFactory::isSqlite($connection); // The Message class used in other error messages requires // \JsonSerializable, so we have to check for it before anything else @@ -79,32 +81,41 @@ public function __construct(Connection $connection, Settings $settings) ); } } + // Check for the appropriate DB extension + $requiredDbExtension = $isSqlite ? 'pdo_sqlite' : 'pdo_mysql'; + if (!extension_loaded($requiredDbExtension)) { + $this->errorMessages[] = new Message( + 'Omeka requires the PHP extension %s, but it is not loaded.', // @translate + $requiredDbExtension + ); + } try { $connection->connect(); } catch (\Exception $e) { $this->errorMessages[] = new Message($e->getMessage()); - // Error establishing a connection, no need to check MySQL version. return; } - // MariaDB includes a fake 5.5.5- leading version in many cases to the - // client handshake, which is what you get if you ask PDO for the server - // version. The VERSION() function doesn't include that junk. - $mysqlVersion = $connection->fetchColumn('SELECT VERSION()'); - if (strpos($mysqlVersion, 'MariaDB') === false) { - if (!version_compare($mysqlVersion, self::MYSQL_MINIMUM_VERSION, '>=')) { - $this->errorMessages[] = new Message( - 'The installed MySQL version (%1$s) is too low. Omeka requires at least version %2$s.', // @translate - $mysqlVersion, - self::MYSQL_MINIMUM_VERSION - ); - } - } else { - if (!version_compare($mysqlVersion, self::MARIADB_MINIMUM_VERSION, '>=')) { - $this->errorMessages[] = new Message( - 'The installed MariaDB version (%1$s) is too low. Omeka requires at least version %2$s.', // @translate - $mysqlVersion, - self::MARIADB_MINIMUM_VERSION - ); + if (!$isSqlite) { + // MariaDB includes a fake 5.5.5- leading version in many cases to the + // client handshake, which is what you get if you ask PDO for the server + // version. The VERSION() function doesn't include that junk. + $mysqlVersion = $connection->fetchColumn('SELECT VERSION()'); + if (strpos($mysqlVersion, 'MariaDB') === false) { + if (!version_compare($mysqlVersion, self::MYSQL_MINIMUM_VERSION, '>=')) { + $this->errorMessages[] = new Message( + 'The installed MySQL version (%1$s) is too low. Omeka requires at least version %2$s.', // @translate + $mysqlVersion, + self::MYSQL_MINIMUM_VERSION + ); + } + } else { + if (!version_compare($mysqlVersion, self::MARIADB_MINIMUM_VERSION, '>=')) { + $this->errorMessages[] = new Message( + 'The installed MariaDB version (%1$s) is too low. Omeka requires at least version %2$s.', // @translate + $mysqlVersion, + self::MARIADB_MINIMUM_VERSION + ); + } } } } diff --git a/application/src/Stdlib/FulltextSearch.php b/application/src/Stdlib/FulltextSearch.php index 8ec27a56f0..3c2568b2a7 100644 --- a/application/src/Stdlib/FulltextSearch.php +++ b/application/src/Stdlib/FulltextSearch.php @@ -4,6 +4,7 @@ use Omeka\Api\Adapter\AdapterInterface; use Omeka\Api\Adapter\FulltextSearchableInterface; use Omeka\Api\ResourceInterface; +use Omeka\Service\ConnectionFactory; use PDO; class FulltextSearch @@ -31,12 +32,21 @@ public function save(ResourceInterface $resource, AdapterInterface $adapter) $owner = $adapter->getFulltextOwner($resource); $ownerId = $owner ? $owner->getId() : null; - $sql = 'INSERT INTO `fulltext_search` ( - `id`, `resource`, `owner_id`, `is_public`, `title`, `text` - ) VALUES ( - :id, :resource, :owner_id, :is_public, :title, :text - ) ON DUPLICATE KEY UPDATE - `owner_id` = :owner_id, `is_public` = :is_public, `title` = :title, `text` = :text'; + if (ConnectionFactory::isSqlite($this->conn)) { + $sql = 'INSERT INTO `fulltext_search` ( + `id`, `resource`, `owner_id`, `is_public`, `title`, `text` + ) VALUES ( + :id, :resource, :owner_id, :is_public, :title, :text + ) ON CONFLICT(`id`, `resource`) DO UPDATE SET + `owner_id` = :owner_id, `is_public` = :is_public, `title` = :title, `text` = :text'; + } else { + $sql = 'INSERT INTO `fulltext_search` ( + `id`, `resource`, `owner_id`, `is_public`, `title`, `text` + ) VALUES ( + :id, :resource, :owner_id, :is_public, :title, :text + ) ON DUPLICATE KEY UPDATE + `owner_id` = :owner_id, `is_public` = :is_public, `title` = :title, `text` = :text'; + } $stmt = $this->conn->prepare($sql); $stmt->bindValue('id', $resourceId, PDO::PARAM_INT); diff --git a/application/src/Test/DbTestCase.php b/application/src/Test/DbTestCase.php index d93864a587..c23077a42c 100644 --- a/application/src/Test/DbTestCase.php +++ b/application/src/Test/DbTestCase.php @@ -2,6 +2,7 @@ namespace Omeka\Test; use Laminas\Mvc\Application; +use Omeka\Service\ConnectionFactory; /** * Database test case. @@ -68,14 +69,22 @@ public static function dropSchema() { $connection = self::getApplication()->getServiceManager() ->get('Omeka\EntityManager')->getConnection(); - $connection->query('SET FOREIGN_KEY_CHECKS=0'); + if (ConnectionFactory::isSqlite($connection)) { + $connection->exec('PRAGMA foreign_keys = OFF'); + } else { + $connection->query('SET FOREIGN_KEY_CHECKS=0'); + } foreach ($connection->getSchemaManager()->listTableNames() as $table) { $connection->executeUpdate( $connection->getDatabasePlatform() ->getDropTableSQL($table) ); } - $connection->query('SET FOREIGN_KEY_CHECKS=1'); + if (ConnectionFactory::isSqlite($connection)) { + $connection->exec('PRAGMA foreign_keys = ON'); + } else { + $connection->query('SET FOREIGN_KEY_CHECKS=1'); + } } /** diff --git a/composer.json b/composer.json index 16e0811a69..f7c1b2f240 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "php": ">=8.1", "ext-fileinfo": "*", "ext-mbstring": "*", - "ext-pdo_mysql": "*", + "ext-PDO": "*", "ext-xml": "*", "doctrine/orm": "^2.11.2", "doctrine/annotations": "^1.14.1", @@ -50,6 +50,10 @@ "beberlei/doctrineextensions": "^1.0", "fileeye/pel": "^0.12.0" }, + "suggest": { + "ext-pdo_mysql": "Required for MySQL/MariaDB database support", + "ext-pdo_sqlite": "Required for SQLite database support" + }, "require-dev": { "phpunit/phpunit": "^9", "friendsofphp/php-cs-fixer": "^3.8.0", diff --git a/config/database.ini.dist b/config/database.ini.dist index c3cbe5c36d..8d16f0e4c9 100644 --- a/config/database.ini.dist +++ b/config/database.ini.dist @@ -1,7 +1,13 @@ +; MySQL configuration (default) +;driver = "pdo_mysql" user = "" password = "" dbname = "" host = "" -;port = -;unix_socket = -;log_path = +;port = +;unix_socket = +;log_path = + +; SQLite configuration (uncomment below and comment out MySQL settings above) +;driver = "pdo_sqlite" +;path = "OMEKA_PATH/db/omeka.db" diff --git a/gulpfile.js b/gulpfile.js index bb20c79add..1cac9c67ef 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -31,7 +31,7 @@ var langDir = __dirname + '/application/language'; var pot = langDir + '/template.pot'; var cliOptions = minimist(process.argv.slice(2), { - string: ['php-path', 'module-name'], + string: ['php-path', 'module-name', 'port', 'host', 'db-path'], boolean: 'dev', alias: {'module-name': 'module'}, default: {'php-path': 'php', 'dev': true, 'module-name': null} @@ -502,6 +502,48 @@ var taskInit = gulp.series('dedist', 'deps'); taskInit.description = 'Run first-time setup for a source checkout'; gulp.task('init', taskInit); +function taskServe() { + var port = cliOptions['port'] || 8080; + var host = cliOptions['host'] || 'localhost'; + console.log('Starting PHP development server on http://' + host + ':' + port); + return runCommand(cliOptions['php-path'], [ + '-S', host + ':' + port, + '-t', __dirname + ], {stdio: 'inherit'}); +} +taskServe.description = 'Start PHP development server'; +taskServe.flags = {'--port': 'Port number (default: 8080)', '--host': 'Host address (default: localhost)'}; +gulp.task('serve', taskServe); + +function taskServeSqlite() { + var port = cliOptions['port'] || 8080; + var host = cliOptions['host'] || 'localhost'; + var dbPath = path.resolve(cliOptions['db-path'] || path.join(__dirname, 'db', 'omeka.db')); + var dbDir = path.dirname(dbPath); + var configPath = path.join(__dirname, 'config', 'database.ini'); + + // Ensure db directory exists + fs.mkdirSync(dbDir, {recursive: true}); + + // Write SQLite database.ini config + var configContent = 'driver = "pdo_sqlite"\npath = "' + dbPath + '"\n'; + fs.writeFileSync(configPath, configContent); + console.log('Configured SQLite database at: ' + dbPath); + console.log('Starting PHP development server on http://' + host + ':' + port); + + return runCommand(cliOptions['php-path'], [ + '-S', host + ':' + port, + '-t', __dirname + ], {stdio: 'inherit'}); +} +taskServeSqlite.description = 'Configure SQLite and start PHP development server'; +taskServeSqlite.flags = { + '--port': 'Port number (default: 8080)', + '--host': 'Host address (default: localhost)', + '--db-path': 'SQLite database file path (default: ./db/omeka.db)' +}; +gulp.task('serve:sqlite', taskServeSqlite); + function taskClean() { return rimraf(buildDir).then(function () { rimraf(__dirname + '/vendor');