diff --git a/README.md b/README.md
index e506df9..53ef71a 100755
--- a/README.md
+++ b/README.md
@@ -10,8 +10,18 @@ The built-in SilverStripe search form is a very simple search engine. This plugi
# Usage
* Create a `SearchPage` instance (typically at the root of your website). This page only is used to display results, so please refrain from creating multiple instances.
-* Configure your website's `_config/config.yml` to define search parameters.
-* Run `dev/build` to instansiate your new configuration
+* Configure your website's `_config/config.yml` (or add `_config/search.yml`) to define search parameters.
+* Run `dev/build` to instansiate your new configuration (this will also automatically create an instance of `SearchPage` if one does not exist).
+* To overwrite the default `SearchPage` tmeplate, add a template file to your application: `templates/PlasticStudio/Search/Layout/SearchPage.ss`
+
+
+# Elemental
+
+* Elemental search is included
+* On page or Element save, all content from all Elements is saved to a field called `ElementalSearchContent` on sitetree.
+* Simply include `'SiteTree_Live.ElementalSearchContent'` to the list of page columns
+* Currently there is no way to exclude individual elements from being included.
+* Run IndexPageContentForSearchTask to index element content
# Configuration
@@ -23,17 +33,20 @@ The built-in SilverStripe search form is a very simple search engine. This plugi
* `Filters`: a list of filters to apply pre-search (maps to `DataList->Filter(key => value)`)
* `Columns`: columns to search for query string matches (format `Table.Column`)
* `filters`: associative list of filter options
- * `Structure`: defines the filter's relational structure (must be one of `db`, `has_one` or `has_many`)
+ * `Structure`: defines the filter's relational structure (must be one of `db`, `has_one` or `many_many`)
* `Label`: front-end field label
* `Table`: relational subject's table
* `Column`: column to filter on
- * `Operator`: SQL filter operator (ie `>`, `=`)
+ * `Operator`: SQL filter operator (ie `>`, `<`, `=`)
* `JoinTables`: associative list of relationship mappings (use the `key` from the `types` array)
* `Table`: relational join table
* `Column`: column to join by
- * `sorts`: associative list of sort options
+ * `sorts`: associative list of sort options. These are used to popoulate a "Sort by" dropdown field in the Advanced Search Form. Sort order of search results will default to the top item in this list.
* `Label`: front-end field label
* `Sort`: SQL sort string
+* `submit_button_text`: Text to use on search form submit button (defaults to "Search")
+
+TODO: `defaults`: Default attributes or settings, as opposed to those submitted through the search form.
# Example configuration
@@ -44,7 +57,7 @@ Name: search
Before:
- '#site'
---
-Jaedb\Search\SearchPageController:
+PlasticStudio\Search\SearchPageController:
types:
docs:
Label: 'Documents'
@@ -53,6 +66,7 @@ Jaedb\Search\SearchPageController:
ClassNameShort: 'File'
Filters:
File_Live.ShowInSearch: '1'
+ File_Live.ClassName: '''Silverstripe\\Assets\\File''' # You need to TRIPLE-ESCAPE in order to pass this as a string to the query
Columns: ['File_Live.Title','File_Live.Description','File_Live.Name']
pages:
Label: 'Pages'
@@ -62,7 +76,7 @@ Jaedb\Search\SearchPageController:
Filters:
SiteTree_Live.ShowInSearch: '1'
JoinTables: ['SiteTree_Live']
- Columns: ['SiteTree_Live.Title','SiteTree_Live.MenuTitle','SiteTree_Live.Content']
+ Columns: ['SiteTree_Live.Title','SiteTree_Live.MenuTitle','SiteTree_Live.Content', 'SiteTree_Live.ElementalSearchContent']
filters:
updated_before:
Structure: 'db'
@@ -86,6 +100,15 @@ Jaedb\Search\SearchPageController:
pages:
Table: 'Page_Tags'
Column: 'PageID'
+ authors:
+ Structure: 'many_many'
+ Label: 'Authors'
+ ClassName: 'Member'
+ Table: 'Member'
+ JoinTables:
+ pages:
+ Table: 'Page_Authors'
+ Column: 'PageID'
sorts:
title_asc:
Label: 'Title (A-Z)'
@@ -99,4 +122,8 @@ Jaedb\Search\SearchPageController:
published_desc:
Label: 'Publish date (oldest first)'
Sort: 'DatePublished ASC'
+ submit_button_text: 'Go'
+ ## TODO:
+ ## defaults:
+ ## sort: 'Title ASC'
```
diff --git a/_config/config.yml b/_config/config.yml
index e3ed9d8..641ad16 100755
--- a/_config/config.yml
+++ b/_config/config.yml
@@ -1,12 +1,15 @@
---
-Name: search
+Name: ps-search
Before:
- '#site'
---
SilverStripe\Control\Controller:
extensions:
- - Jaedb\Search\SearchControllerExtension
-Jaedb\Search\SearchPageController:
+ - PlasticStudio\Search\SearchControllerExtension
+SilverStripe\CMS\Model\SiteTree:
+ extensions:
+ - PlasticStudio\Search\SiteTreeSearchExtension
+# PlasticStudio\Search\SearchPageController:
# types:
# docs:
# Label: 'Documents'
diff --git a/_config/elemental.yml b/_config/elemental.yml
new file mode 100644
index 0000000..d44fa5d
--- /dev/null
+++ b/_config/elemental.yml
@@ -0,0 +1,9 @@
+---
+Name: 'ps-search-elemental'
+After: '#ps-search'
+Only:
+ moduleexists: 'dnadesign/silverstripe-elemental'
+---
+DNADesign\Elemental\Models\BaseElement:
+ extensions:
+ - PlasticStudio\Search\ElementalSearchExtension
\ No newline at end of file
diff --git a/composer.json b/composer.json
index 4cf5edc..d7904db 100755
--- a/composer.json
+++ b/composer.json
@@ -1,19 +1,24 @@
{
- "name": "jaedb/search",
+ "name": "plasticstudio/search",
"type": "silverstripe-vendormodule",
- "description": "SilverStripe search engine",
- "homepage": "http://jamesbarnsley.co.nz",
- "keywords": ["silverstripe"],
+ "description": "Search engine for Silverstripe websites - forked from jaedb/search",
+ "homepage": "https://psdigital.co.nz",
+ "keywords": ["silverstripe","silverstripesearch"],
"license": "BSD-3-Clause",
"authors": [
{
"name": "James Barnsley",
- "homepage": "http://jamesbarnsley.co.nz",
+ "homepage": "https://jamesbarnsley.co.nz",
"email": "james@barnsley.co.nz"
+ },
+ {
+ "name": "Jeremy Cole",
+ "homepage": "https://psdigital.co.nz",
+ "email": "jeremy@psdigital.co.nz"
}
],
"support": {
- "issues": "http://github.com/jaedb/search/issues"
+ "issues": "http://github.com/plasticstudio/search/issues"
},
"extra": {
"expose": [
diff --git a/package.json b/package.json
index a3647d9..231671f 100755
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"version": "1.1.0",
"author": "James Barnsley",
"description": "SilverStripe Search Engine",
- "repository": "https://github.com/jaedb/search",
+ "repository": "https://github.com/plasticstudio/search",
"licenses": {
"type": "Apache License",
"url": "http://www.apache.org/licenses/LICENSE-2.0.txt"
diff --git a/src/ElementalSearchExtension.php b/src/ElementalSearchExtension.php
new file mode 100644
index 0000000..9a78457
--- /dev/null
+++ b/src/ElementalSearchExtension.php
@@ -0,0 +1,46 @@
+updateSearchContent();
+ }
+
+ /**
+ * Force a re-index of the parent page on archive of element
+ * @param Versioned $original
+ */
+ public function onAfterDelete(&$original)
+ {
+ $this->updateSearchContent();
+ }
+ /**
+ * Force a re-index of the parent page on un-publish of element
+ */
+ public function onAfterUnpublish()
+ {
+ $this->updateSearchContent();
+ }
+
+ public function updateSearchContent()
+ {
+ $parent = $this->getOwner()->getPage();
+ //Even though we have the parent page. Lets always get the "live" version. This is so when we update the search content we are not indexing draft/unpublished content
+ $liveParentPage = Versioned::get_by_stage($parent->ClassName, Versioned::LIVE)->byID($parent->ID);
+ if ($liveParentPage && $liveParentPage->hasExtension(SiteTreeSearchExtension::class)) {
+ $liveParentPage->updateSearchContent();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/IndexPageContentForSearchTask.php b/src/IndexPageContentForSearchTask.php
new file mode 100644
index 0000000..fde4af4
--- /dev/null
+++ b/src/IndexPageContentForSearchTask.php
@@ -0,0 +1,132 @@
+getVar('reindex');
+ $offset = $request->getVar('offset') ? $request->getVar('offset') : NULL;
+ $limit = $request->getVar('limit') ? $request->getVar('limit') : 10;
+
+ // select all sitetree items
+ $items = SiteTree::get()->limit($limit, $offset);
+ echo 'Running...
';
+ echo 'limit: ' . $limit . '
';
+ echo 'offset: ' . $offset . '
';
+ // echo 'count ' . $items->Count(). '
';
+
+ if(!$reindex) {
+ $items = $items->filter(['ElementalSearchContent' => null]);
+ echo 'Running - generating first index...
';
+ }
+
+ if(!$items->count()) {
+ echo 'No items to update.
';
+ } else {
+
+ foreach ($items as $item) {
+
+ // get the page content as plain content string
+ $content = $this->collateSearchContent($item);
+
+ // Update this item in db
+ $update = SQLUpdate::create();
+ $update->setTable('"SiteTree"');
+ $update->addWhere(['ID' => $item->ID]);
+ $update->addAssignments([
+ '"ElementalSearchContent"' => $content
+ ]);
+ $update->execute();
+
+ // IF page is published, update the live table
+ if ($item->isPublished()) {
+ $update = SQLUpdate::create();
+ $update->setTable('"SiteTree_Live"');
+ $update->addWhere(['ID' => $item->ID]);
+ $update->addAssignments([
+ '"ElementalSearchContent"' => $content
+ ]);
+ $update->execute();
+ }
+
+ echo '
Page ' . $item->Title . ' indexed.
' . PHP_EOL; + } + } + } + + /** + * Generate the search content to use for the searchable object + * + * We just retrieve it from the templates. + */ + private function collateSearchContent($page): string + { + // Get the page + /** @var SiteTree $page */ + // $page = $this->getOwner(); + + $content = ''; + + if (self::isElementalPage($page)) { + // Get the page's elemental content + $content .= $this->collateSearchContentFromElements($page); + } + + return $content; + } + + + /** + * @param SiteTree $page + * @return bool + */ + private static function isElementalPage($page) + { + return $page::has_extension("DNADesign\Elemental\Extensions\ElementalPageExtension"); + } + + /** + * @return string|string[]|null + */ + private function collateSearchContentFromElements($page) + { + // Get the original theme + $originalThemes = SSViewer::get_themes(); + + // Init content + $content = ''; + + try { + // Enable frontend themes in order to correctly render the elements as they would be for the frontend + Config::nest(); + SSViewer::set_themes(SSViewer::config()->get('themes')); + + // Get the elements content + $content .= $page->getOwner()->getElementsForSearch(); + + // Clean up the content + $content = preg_replace('/\s+/', ' ', $content); + + // Return themes back for the CMS + Config::unnest(); + } finally { + // Restore themes + SSViewer::set_themes($originalThemes); + } + + return $content; + } + +} \ No newline at end of file diff --git a/src/SearchControllerExtension.php b/src/SearchControllerExtension.php index 31c5f3f..a24be45 100755 --- a/src/SearchControllerExtension.php +++ b/src/SearchControllerExtension.php @@ -1,6 +1,6 @@ push( TextField::create('query','',SearchPageController::get_query())->addExtraClass('query')->setAttribute('placeholder', 'Keywords') ); + $placeholder_text = 'Keywords'; + if (Config::inst()->get('PlasticStudio\Search\SearchPageController', 'search_form_placeholder_text')) { + $placeholder_text = Config::inst()->get('PlasticStudio\Search\SearchPageController', 'search_form_placeholder_text'); + } + $fields->push( TextField::create('query','',SearchPageController::get_query())->addExtraClass('query')->setAttribute('placeholder', $placeholder_text) ); // create the form actions (we only need a submit button) + $submit_button_text = 'Search'; + if (Config::inst()->get('PlasticStudio\Search\SearchPageController', 'submit_button_text')) { + $submit_button_text = Config::inst()->get('PlasticStudio\Search\SearchPageController', 'submit_button_text'); + } + // don't do action here, set below for 404 error page fix + // fix breaks pagination, reinstating $actions = FieldList::create( - FormAction::create("doSearchForm")->setTitle("Search") + FormAction::create("doSearchForm")->setTitle($submit_button_text) ); // now build the actual form object @@ -46,7 +57,11 @@ public function SearchForm(){ $name = 'SearchForm', $fields = $fields, $actions = $actions - )->addExtraClass('search-form'); + )->addExtraClass('search-form') + ->disableSecurityToken(); + + // $page = SearchPage::get()->first(); + // $form->setFormAction($page->Link()); return $form; } @@ -147,7 +162,12 @@ public function AdvancedSearchForm(){ $source = $source->filter($filter['Filters']); } - $fields->push(ListboxField::create($key, $filter['Label'], $source->map('ID','Title','All'), explode(',',$value))->addExtraClass('chosen-select')); + if ($value == null) { + $default = ''; + } else { + $default = explode(',', $value); + } + $fields->push(CheckboxSetField::create($key, $filter['Label'], $source->map('ID','Title','All'), $default)->addExtraClass('chosen-select')); break; } @@ -169,8 +189,12 @@ public function AdvancedSearchForm(){ } // create the form actions (we only need a submit button) + $submit_button_text = 'Search'; + if (Config::inst()->get('PlasticStudio\Search\SearchPageController', 'submit_button_text')) { + $submit_button_text = Config::inst()->get('PlasticStudio\Search\SearchPageController', 'submit_button_text'); + } $actions = FieldList::create( - FormAction::create("doSearchForm")->setTitle("Search") + FormAction::create("doSearchForm")->setTitle($submit_button_text) ); // now build the actual form object @@ -179,7 +203,8 @@ public function AdvancedSearchForm(){ $name = 'AdvancedSearchForm', $fields = $fields, $actions = $actions - )->addExtraClass('search-form advanced-search-form'); + )->addExtraClass('search-form advanced-search-form') + ->disableSecurityToken(); return $form; } diff --git a/src/SearchPage.php b/src/SearchPage.php index 027ecfe..e402e10 100755 --- a/src/SearchPage.php +++ b/src/SearchPage.php @@ -1,24 +1,36 @@ 0, + 'ShowInSearch' => 0 + ]; + /** * We need to have a SearchPage to use it */ - public function requireDefaultRecords() { + public function requireDefaultRecords() + { parent::requireDefaultRecords(); if (static::class == self::class && $this->config()->create_default_pages) { - if (!SearchPage::get()){ + if (count(SearchPage::get()) < 1) { $page = SearchPage::create(); $page->Title = 'Search'; $page->Content = ''; + $page->ShowInMenus = false; + $page->ShowInSearch = false; $page->write(); + $page->copyVersionToStage(Versioned::DRAFT, Versioned::LIVE); $page->flushCache(); DB::alteration_message('Search page created', 'created'); } diff --git a/src/SearchPageController.php b/src/SearchPageController.php index 71afb00..c7491c4 100755 --- a/src/SearchPageController.php +++ b/src/SearchPageController.php @@ -1,6 +1,6 @@ get('Jaedb\Search\SearchPageController', 'types'); + $types = Config::inst()->get('PlasticStudio\Search\SearchPageController', 'types'); $array = []; if ($types){ @@ -73,7 +74,7 @@ public static function get_types_available(){ } public static function get_filters_available(){ - $filters = Config::inst()->get('Jaedb\Search\SearchPageController', 'filters'); + $filters = Config::inst()->get('PlasticStudio\Search\SearchPageController', 'filters'); $array = []; if ($filters){ @@ -87,7 +88,7 @@ public static function get_filters_available(){ } public static function get_sorts_available(){ - $sorts = Config::inst()->get('Jaedb\Search\SearchPageController', 'sorts'); + $sorts = Config::inst()->get('PlasticStudio\Search\SearchPageController', 'sorts'); $array = []; if ($sorts){ @@ -99,6 +100,19 @@ public static function get_sorts_available(){ return $array; } + + public static function get_defaults_available(){ + $defaults = Config::inst()->get('PlasticStudio\Search\SearchPageController', 'defaults'); + $array = []; + + if ($defaults){ + foreach ($defaults as $key => $value){ + $array[$key] = $value; + } + } + + return $array; + } public static function get_types(){ return self::$types; @@ -146,11 +160,13 @@ public static function get_mapped_filters(){ } public static function get_query($mysqlSafe = false){ - $query = self::$query; - if( $mysqlSafe ){ - $query = str_replace("'", "\'", $query); - $query = str_replace('"', '\"', $query); - $query = str_replace('`', '\`', $query); + $query = self::$query ?? ''; + if ($query) { + if ($mysqlSafe) { + $query = str_replace("'", "\'", $query); + $query = str_replace('"', '\"', $query); + $query = str_replace('`', '\`', $query); + } } return $query; } @@ -163,6 +179,10 @@ public static function get_sort(){ return self::$sort; } + public static function set_sort($sort){ + self::$sort = $sort; + } + public static function get_mapped_sort(){ $sorts_available = self::get_sorts_available(); $sort = self::get_sort(); @@ -175,8 +195,16 @@ public static function get_mapped_sort(){ } } - public static function set_sort($sort){ - self::$sort = $sort; + public static function get_defaults(){ + return self::$defaults; + } + + public static function set_defaults($defaults){ + self::$defaults = $defaults; + } + + public static function get_mapped_defaults(){ + return self::get_defaults_available(); } public static function get_results(){ @@ -327,7 +355,7 @@ public function PerformSearch(){ $tables_to_check[] = $type['Table']; foreach ($tables_to_check as $table_to_check){ - $column_exists_query = DB::query( "SHOW COLUMNS FROM \"".$table_to_check."\" LIKE '".$filter['Column']."'" ); + $column_exists_query = DB::query( "SHOW COLUMNS FROM \"".$table_to_check."\" LIKE '".$filter['Column']."'" ); foreach ($column_exists_query as $column){ $table_with_column = $table_to_check; @@ -401,18 +429,20 @@ public function PerformSearch(){ // join the relationship table to our record(s) $joins.= "LEFT JOIN \"".$filter['Table']."\" ON \"".$filter['Table']."\".\"ID\" = \"".$table_with_column."\".\"".$filter['Column']."\""; - if (is_array($filter['Value'])){ - $ids = ''; - foreach ($filter['Value'] as $id){ - if ($ids != ''){ - $ids.= ','; + if(!empty($filter['Value'])){ + if (is_array($filter['Value'])){ + $ids = ''; + foreach ($filter['Value'] as $id){ + if ($ids != ''){ + $ids.= ','; + } + $ids.= "'".$id."'"; } - $ids.= "'".$id."'"; + } else { + $ids = $filter['Value']; } - } else { - $ids = $filter['Value']; + $where.= ' AND ('."\"".$table_with_column."\".\"".$filter['Column']."\" IN (". $ids .")".')'; } - $where.= ' AND ('."\"".$table_with_column."\".\"".$filter['Column']."\" IN (". $ids .")".')'; break; @@ -426,21 +456,26 @@ public function PerformSearch(){ $filter_join = $filter['JoinTables'][$type['Key']]; - $joins.= "LEFT JOIN \"".$filter_join['Table']."\" ON \"".$type['Table']."\".\"ID\" = \"".$filter_join['Column']."\""; - - if (is_array($filter['Value'])){ - $ids = ''; - foreach ($filter['Value'] as $id){ - if ($ids != ''){ - $ids.= ','; + $joins.= "LEFT JOIN \"".$filter_join['Table']."\" ON \"".$type['Table']."\".\"ID\" = \"".$filter_join['Table']."\".\"".$filter_join['Column']."\""; + + if(!empty($filter['Value'])){ + if (is_array($filter['Value'])){ + $ids = ''; + foreach ($filter['Value'] as $id){ + if ($ids != ''){ + $ids.= ','; + } + $ids.= "'".$id."'"; } - $ids.= "'".$id."'"; + } else { + $ids = $filter['Value']; } - } else { - $ids = $filter['Value']; - } - $relations_sql.= "\"".$filter_join['Table']."\".\"".$filter['Table']."ID\" IN (". $ids .")"; + if ($relations_sql !== ''){ + $relations_sql.= " AND "; + } + $relations_sql.= "\"".$filter_join['Table']."\".\"".$filter['Table']."ID\" IN (". $ids .")"; + } } break; @@ -459,7 +494,7 @@ public function PerformSearch(){ $sql.= $where; // Debugging - //echo '