2015-10-27 21:10:05 +00:00
< ? php
2020-09-23 22:22:39 +00:00
/* This Source Code Form is subject to the terms of the Mozilla Public
* License , v . 2.0 . If a copy of the MPL was not distributed with this
* file , You can obtain one at https :// mozilla . org / MPL / 2.0 /. */
2015-10-27 21:10:05 +00:00
register_module ([
" name " => " Search " ,
2021-09-02 23:00:49 +00:00
" version " => " 0.13.3 " ,
2015-10-27 21:10:05 +00:00
" author " => " Starbeamrainbowlabs " ,
2016-03-12 18:52:26 +00:00
" description " => " Adds proper search functionality to Pepperminty Wiki using an inverted index to provide a full text search engine. If pages don't show up, then you might have hit a stop word. If not, try requesting the `invindex-rebuild` action to rebuild the inverted index from scratch. " ,
2015-10-27 21:10:05 +00:00
" id " => " feature-search " ,
2020-03-11 23:51:49 +00:00
// After refactoring, we'll need to specify dependencies like this
2020-03-14 17:18:51 +00:00
" depends " => [ " lib-search-engine " ],
2015-10-27 21:10:05 +00:00
" code " => function () {
2019-08-17 19:47:51 +00:00
global $settings , $paths ;
2016-06-12 20:15:43 +00:00
/**
* @ api { get } ? action = index & page = { pageName } Get an index of words for a given page
* @ apiName SearchIndex
* @ apiGroup Search
* @ apiPermission Anonymous
2018-02-14 23:08:28 +00:00
* @ apiDescription For debugging purposes . Be warned - the format could change at any time !
2016-06-12 20:15:43 +00:00
*
* @ apiParam { string } page The page to generate a word index page .
*/
2015-12-26 12:55:19 +00:00
/*
* ██ ███ ██ ██████ ███████ ██ ██
* ██ ████ ██ ██ ██ ██ ██ ██
* ██ ██ ██ ██ ██ ██ █████ ███
* ██ ██ ██ ██ ██ ██ ██ ██ ██
* ██ ██ ████ ██████ ███████ ██ ██
*/
2015-10-27 21:10:05 +00:00
add_action ( " index " , function () {
global $settings , $env ;
$breakable_chars = " \r \n \t ., \\ /! \" £ $ %^&*[]()+`_~# " ;
header ( " content-type: text/plain " );
2015-11-08 21:15:08 +00:00
$source = file_get_contents ( " $env->storage_prefix $env->page .md " );
2015-10-27 21:10:05 +00:00
2019-08-18 17:52:29 +00:00
$index = search :: index_generate ( $source );
2015-10-27 21:10:05 +00:00
2018-06-29 11:08:38 +00:00
echo ( " Page name: $env->page\n " );
echo ( " --------------- Source --------------- \n " );
echo ( $source ); echo ( " \n " );
echo ( " -------------------------------------- \n \n " );
echo ( " ---------------- Index --------------- \n " );
foreach ( $index as $term => $entry ) {
echo ( " $term : { $entry [ " freq " ] } matches | " . implode ( " , " , $entry [ " offsets " ]) . " \n " );
}
echo ( " -------------------------------------- \n " );
2015-10-27 21:10:05 +00:00
});
2015-10-28 20:56:10 +00:00
2016-06-12 20:15:43 +00:00
/**
* @ api { get } ? action = invindex - rebuild Rebuild the inverted search index from scratch
* @ apiDescription Causes the inverted search index to be completely rebuilt from scratch . Can take a while for large wikis !
* @ apiName SearchInvindexRebuild
* @ apiGroup Search
2017-07-10 21:10:18 +00:00
* @ apiPermission Admin
*
* @ apiParam { string } secret Optional . Specify the secret from peppermint . json here in order to rebuild the search index without logging in .
2016-06-12 20:15:43 +00:00
*/
2015-12-26 12:55:19 +00:00
/*
* ██ ███ ██ ██ ██ ██ ███ ██ ██████ ███████ ██ ██
* ██ ████ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ █████ ███ █████
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
* ██ ██ ████ ████ ██ ██ ████ ██████ ███████ ██ ██
*
* ██████ ███████ ██████ ██ ██ ██ ██ ██████
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
* ██████ █████ ██████ ██ ██ ██ ██ ██ ██
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
* ██ ██ ███████ ██████ ██████ ██ ███████ ██████
*/
2015-10-28 20:56:10 +00:00
add_action ( " invindex-rebuild " , function () {
2017-07-10 21:06:41 +00:00
global $env , $settings ;
if ( $env -> is_admin ||
(
! empty ( $_POST [ " secret " ]) &&
$_POST [ " secret " ] === $settings -> secret
)
)
2019-08-18 17:52:29 +00:00
search :: invindex_rebuild ();
2017-07-10 21:06:41 +00:00
else
{
http_response_code ( 401 );
exit ( page_renderer :: render_main ( " Error - Search index regenerator - $settings->sitename " , " <p>Error: You aren't allowed to regenerate the search index. Try logging in as an admin, or setting the <code>secret</code> POST parameter to $settings->sitename 's secret - which can be found in $settings->sitename 's <code>peppermint.json</code> file.</p> " ));
}
2015-10-28 20:56:10 +00:00
});
2016-08-26 16:47:46 +00:00
/**
2016-08-26 16:55:50 +00:00
* @ api { get } ? action = idindex - show Show the id index
2018-02-14 23:10:20 +00:00
* @ apiDescription Outputs the id index . Useful if you need to verify that it ' s working as expected . Output is a json object .
2016-08-26 16:47:46 +00:00
* @ apiName SearchShowIdIndex
* @ apiGroup Search
* @ apiPermission Anonymous
*/
add_action ( " idindex-show " , function () {
global $idindex ;
header ( " content-type: application/json; charset=UTF-8 " );
exit ( json_encode ( $idindex , JSON_PRETTY_PRINT ));
});
2016-06-12 20:15:43 +00:00
/**
2018-02-14 23:08:28 +00:00
* @ api { get } ? action = search & query = { text }[ & format = { format }] Search the wiki for a given query string
2016-06-12 20:15:43 +00:00
* @ apiName Search
* @ apiGroup Search
* @ apiPermission Anonymous
*
* @ apiParam { string } query The query string to search for .
2018-02-14 23:08:28 +00:00
* @ apiParam { string } format Optional . Valid values : html , json . In json mode an object is returned with page names as keys , values as search result information - sorted in ranking order .
2016-06-12 20:15:43 +00:00
*/
2015-12-26 12:55:19 +00:00
/*
* ███████ ███████ █████ ██████ ██████ ██ ██
* ██ ██ ██ ██ ██ ██ ██ ██ ██
* ███████ █████ ███████ ██████ ██ ███████
* ██ ██ ██ ██ ██ ██ ██ ██ ██
* ███████ ███████ ██ ██ ██ ██ ██████ ██ ██
*/
2015-10-28 20:56:10 +00:00
add_action ( " search " , function () {
2015-11-09 07:25:28 +00:00
global $settings , $env , $pageindex , $paths ;
2015-10-28 20:56:10 +00:00
2015-12-05 17:27:01 +00:00
// Create the inverted index if it doesn't exist.
if ( ! file_exists ( $paths -> searchindex ))
2019-08-18 17:52:29 +00:00
search :: invindex_rebuild ( false );
2020-03-15 17:54:27 +00:00
// Create the didyoumean index if it doesn't exist.
if ( module_exists ( " feature-search-didyoumean " ) && ! file_exists ( $paths -> didyoumeanindex ))
search :: didyoumean_rebuild ( false );
2015-12-05 17:27:01 +00:00
2015-10-28 20:56:10 +00:00
if ( ! isset ( $_GET [ " query " ]))
2015-10-29 11:21:04 +00:00
exit ( page_renderer :: render ( " No Search Terms - Error - $settings->sitename " , " <p>You didn't specify any search terms. Try typing some into the box above.</p> " ));
2015-10-28 20:56:10 +00:00
2015-10-29 11:21:04 +00:00
$search_start = microtime ( true );
2015-10-28 20:56:10 +00:00
2018-06-25 21:53:53 +00:00
$time_start = microtime ( true );
2019-08-22 16:43:14 +00:00
search :: invindex_load ( $paths -> searchindex );
2018-06-25 21:53:53 +00:00
$env -> perfdata -> invindex_decode_time = round (( microtime ( true ) - $time_start ) * 1000 , 3 );
2019-08-15 22:46:23 +00:00
$time_start = microtime ( true );
2020-04-21 20:01:00 +00:00
$query_parsed = null ;
$results = search :: invindex_query ( $_GET [ " query " ], $query_parsed );
2016-08-20 10:35:04 +00:00
$resultCount = count ( $results );
2018-06-25 21:53:53 +00:00
$env -> perfdata -> invindex_query_time = round (( microtime ( true ) - $time_start ) * 1000 , 3 );
2019-08-22 16:43:14 +00:00
header ( " x-invindex-load-time: { $env -> perfdata -> invindex_decode_time } ms " );
2018-06-25 21:53:53 +00:00
header ( " x-invindex-query-time: { $env -> perfdata -> invindex_query_time } ms " );
2018-02-14 23:08:28 +00:00
2018-06-26 13:15:19 +00:00
$start = microtime ( true );
2020-07-10 23:39:05 +00:00
// FUTURE: When we implement $_GET["offset"] and $_GET["count"] or something we can optimise here
foreach ( $results as $key => & $result ) {
2020-07-10 22:22:30 +00:00
$filepath = $env -> storage_prefix . $result [ " pagename " ] . " .md " ;
if ( ! file_exists ( $filepath )) {
2020-07-28 18:42:41 +00:00
error_log ( " [PeppermintyWiki/ $settings->sitename /search] Search engine returned { $result [ " pagename " ] } as a result (maps to $filepath ), but it doesn't exist on disk (try rebuilding the search index). " );
2020-07-10 22:22:30 +00:00
continue ; // Something strange is happening
}
2018-02-14 23:08:28 +00:00
$result [ " context " ] = search :: extract_context (
2019-08-22 20:38:17 +00:00
$result [ " pagename " ],
2020-04-21 20:01:00 +00:00
$query_parsed ,
2020-07-10 22:23:32 +00:00
file_get_contents ( $filepath )
2018-02-14 23:08:28 +00:00
);
}
2020-07-10 23:39:05 +00:00
// This is absolutely *essential*, because otherwise we hit a very strange bug whereby PHP duplicates the value of the last iterated search result. Ref https://bugs.php.net/bug.php?id=70387 - apparently "documented behaviour"
unset ( $result );
2018-06-26 13:15:19 +00:00
$env -> perfdata -> context_generation_time = round (( microtime ( true ) - $start ) * 1000 , 3 );
header ( " x-context-generation-time: { $env -> perfdata -> context_generation_time } ms " );
2018-02-14 23:08:28 +00:00
2018-06-25 23:11:01 +00:00
$env -> perfdata -> search_time = round (( microtime ( true ) - $search_start ) * 1000 , 3 );
header ( " x-search-time: { $env -> perfdata -> search_time } ms " );
2018-02-14 23:08:28 +00:00
if ( ! empty ( $_GET [ " format " ]) && $_GET [ " format " ] == " json " ) {
header ( " content-type: application/json " );
$json_results = new stdClass ();
2020-07-11 00:00:16 +00:00
foreach ( $results as $key => $result )
2020-07-10 23:39:05 +00:00
$json_results -> { $result [ " pagename " ]} = $result ;
2018-02-14 23:08:28 +00:00
exit ( json_encode ( $json_results ));
}
2015-11-08 21:15:08 +00:00
2015-10-29 11:21:04 +00:00
$title = $_GET [ " query " ] . " - Search results - $settings->sitename " ;
$content = " <section> \n " ;
$content .= " <h1>Search Results</h1> " ;
2015-10-31 14:05:00 +00:00
/// Search Box ///
$content .= " <form method='get' action=''> \n " ;
2017-07-29 09:44:55 +00:00
$content .= " <input type='search' id='search-box' name='query' placeholder='Type your query here and then press enter.' value=' " . htmlentities ( $_GET [ " query " ], ENT_HTML5 | ENT_QUOTES ) . " ' /> \n " ;
2015-10-31 14:05:00 +00:00
$content .= " <input type='hidden' name='action' value='search' /> \n " ;
$content .= " </form> " ;
2015-10-29 11:21:04 +00:00
2016-08-20 10:35:04 +00:00
$content .= " <p>Found $resultCount " . ( $resultCount === 1 ? " result " : " results " ) . " in " . $env -> perfdata -> search_time . " ms. " ;
2015-11-01 10:13:35 +00:00
$query = $_GET [ " query " ];
2019-08-22 16:43:14 +00:00
if ( isset ( $pageindex -> $query )) {
2021-09-02 23:00:49 +00:00
$content .= " There's a page on $settings->sitename called <a href='?page= " . rawurlencode ( $query ) . " '> " . htmlentities ( $query ) . " </a>. " ;
2015-11-01 10:13:35 +00:00
}
else
{
2021-09-02 23:00:49 +00:00
$content .= " There isn't a page called " . htmlentities ( $query ) . " on $settings->sitename , but you " ;
2019-08-22 16:43:14 +00:00
if (( ! $settings -> anonedits && ! $env -> is_logged_in ) || ! $settings -> editing ) {
2016-03-12 19:02:36 +00:00
$content .= " do not have permission to create it. " ;
2019-08-22 16:43:14 +00:00
if ( ! $env -> is_logged_in ) {
2016-03-12 19:02:36 +00:00
$content .= " You could try <a href='?action=login&returnto= " . rawurlencode ( $_SERVER [ " REQUEST_URI " ]) . " '>logging in</a>. " ;
}
}
2019-08-22 16:43:14 +00:00
else {
2016-08-20 10:35:04 +00:00
$content .= " can <a href='?action=edit&page= " . rawurlencode ( $query ) . " '>create it</a>. " ;
2016-03-12 19:02:36 +00:00
}
2015-11-01 10:13:35 +00:00
}
2019-08-24 19:47:41 +00:00
$content .= " <br /><small><em>Pssst! Power users can make use of $settings->sitename 's advanced query syntax. Learn about it <a href='?action=help#27-search'>here</a>!</em></small></p> " ;
2015-11-01 10:13:35 +00:00
2017-09-19 16:32:52 +00:00
if ( module_exists ( " page-list " )) {
2020-04-21 20:01:00 +00:00
// TODO: Refactor this to use STAS
2017-09-19 16:32:52 +00:00
$nterms = search :: tokenize ( $query );
$nterms_regex = implode ( " | " , array_map ( function ( $nterm ) {
return preg_quote ( strtolower ( trim ( $nterm )));
}, $nterms ));
$all_tags = get_all_tags ();
$matching_tags = [];
foreach ( $all_tags as $tag ) {
if ( preg_match ( " / $nterms_regex /i " , trim ( $tag )) > 0 )
$matching_tags [] = $tag ;
}
if ( count ( $matching_tags ) > 0 ) {
2018-06-30 10:46:07 +00:00
$content .= " <p class='matching-tags-display'><label>Matching tags</label><span class='tags'> " ;
2017-09-19 16:32:52 +00:00
foreach ( $matching_tags as $tag ) {
$content .= " \t <a href='?action=list-tags&tag= " . rawurlencode ( $tag ) . " ' class='mini-tag'> " . htmlentities ( $tag ) . " </a> \n " ;
}
$content .= " </span></p> " ;
}
}
2015-10-31 14:16:19 +00:00
$i = 0 ; // todo use $_GET["offset"] and $_GET["result-count"] or something
2015-10-29 11:21:04 +00:00
foreach ( $results as $result )
{
2021-09-02 23:00:49 +00:00
$pagename_display = htmlentities ( $result [ " pagename " ]);
2015-10-29 11:21:04 +00:00
$link = " ?page= " . rawurlencode ( $result [ " pagename " ]);
2015-11-08 21:15:08 +00:00
$pagesource = file_get_contents ( $env -> storage_prefix . $result [ " pagename " ] . " .md " );
2016-08-19 12:02:42 +00:00
//echo("Extracting context for result " . $result["pagename"] . ".\n");
2018-02-14 23:08:28 +00:00
$context = $result [ " context " ];
2018-03-18 16:52:55 +00:00
if ( mb_strlen ( $context ) === 0 )
$context = mb_substr ( $pagesource , 0 , $settings -> search_characters_context * 2 );
2016-08-19 12:02:42 +00:00
//echo("'Generated search context for " . $result["pagename"] . ": $context'\n");
2018-03-18 16:52:55 +00:00
$context = search :: highlight_context (
2020-04-21 20:01:00 +00:00
$query_parsed ,
2018-03-18 16:52:55 +00:00
preg_replace ( '/</u' , '<' , $context )
);
2015-11-02 14:42:38 +00:00
/* if ( strlen ( $context ) == 0 )
2015-11-01 15:05:54 +00:00
{
$context = search :: strip_markup ( file_get_contents ( " $env->page .md " , null , null , null , $settings -> search_characters_context * 2 ));
if ( $pageindex -> { $env -> page } -> size > $settings -> search_characters_context * 2 )
$context .= " ... " ;
2015-11-02 14:42:38 +00:00
} */
2015-11-01 15:05:54 +00:00
2017-10-14 21:48:58 +00:00
$tag_list = " <span class='tags'> " ;
2021-09-02 23:00:49 +00:00
foreach ( $pageindex -> { $result [ " pagename " ]} -> tags ? ? [] as $tag ) $tag_list .= " <a href='?action=list-tags&tag= " . rawurlencode ( $tag ) . " ' class='mini-tag'> " . htmlentities ( $tag ) . " </a> " ;
2017-10-14 21:48:58 +00:00
$tag_list .= " </span> \n " ;
2017-03-20 20:21:25 +00:00
// Make redirect pages italics
if ( ! empty ( $pageindex -> { $result [ " pagename " ]} -> redirect ))
2021-09-02 23:00:49 +00:00
$pagename_display = " <em> $pagename_display </em> " ;
2015-10-29 11:21:04 +00:00
2015-10-31 14:16:19 +00:00
// We add 1 to $i here to convert it from an index to a result
// number as people expect it to start from 1
$content .= " <div class='search-result' data-result-number=' " . ( $i + 1 ) . " ' data-rank=' " . $result [ " rank " ] . " '> \n " ;
2021-09-02 23:00:49 +00:00
$content .= " <h2><a href=' $link '> $pagename_display </a> <span class='search-result-badges'> $tag_list </span></h2> \n " ;
2016-08-19 12:47:56 +00:00
$content .= " <p class='search-context'> $context </p> \n " ;
2015-10-29 11:21:04 +00:00
$content .= " </div> \n " ;
2015-10-31 14:16:19 +00:00
$i ++ ;
2015-10-29 11:21:04 +00:00
}
$content .= " </section> \n " ;
2016-08-21 20:02:36 +00:00
header ( " content-type: text/html; charset=UTF-8 " );
2015-10-29 11:21:04 +00:00
exit ( page_renderer :: render ( $title , $content ));
//header("content-type: text/plain");
//var_dump($results);
2015-10-28 20:56:10 +00:00
});
2016-10-01 10:32:38 +00:00
2017-03-23 21:13:20 +00:00
/*
* ██████ ██ ██ ███████ ██████ ██ ██
* ██ ██ ██ ██ ██ ██ ██ ██ ██
* ██ ██ ██ ██ █████ ██████ ████ █████
* ██ ▄▄ ██ ██ ██ ██ ██ ██ ██
* ██████ ██████ ███████ ██ ██ ██
* ▀▀
* ███████ ███████ █████ ██████ ██████ ██ ██ ██ ███ ██ ██████ ███████ ██ ██
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██
* ███████ █████ ███████ ██████ ██ ███████ ██ ██ ██ ██ ██ ██ █████ ███
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
* ███████ ███████ ██ ██ ██ ██ ██████ ██ ██ ██ ██ ████ ██████ ███████ ██ ██
*/
/**
* @ api { get } ? action = query - searchindex & query = { text } Inspect the internals of the search results for a query
* @ apiName Search
* @ apiGroup Search
* @ apiPermission Anonymous
*
* @ apiParam { string } query The query string to search for .
*/
add_action ( " query-searchindex " , function () {
global $env , $paths ;
if ( empty ( $_GET [ " query " ])) {
http_response_code ( 400 );
header ( " content-type: text/plain " );
exit ( " Error: No query specified. Specify it with the 'query' GET parameter. " );
}
$env -> perfdata -> searchindex_decode_start = microtime ( true );
2019-08-22 23:51:39 +00:00
search :: invindex_load ( $paths -> searchindex );
2017-03-23 21:13:20 +00:00
$env -> perfdata -> searchindex_decode_time = ( microtime ( true ) - $env -> perfdata -> searchindex_decode_start ) * 1000 ;
$env -> perfdata -> searchindex_query_start = microtime ( true );
2020-03-15 21:28:56 +00:00
$query_stas = null ;
$searchResults = search :: invindex_query ( $_GET [ " query " ], $query_stas );
2017-03-23 21:13:20 +00:00
$env -> perfdata -> searchindex_query_time = ( microtime ( true ) - $env -> perfdata -> searchindex_query_start ) * 1000 ;
header ( " content-type: application/json " );
$result = new stdClass ();
$result -> time_format = " ms " ;
$result -> decode_time = $env -> perfdata -> searchindex_decode_time ;
$result -> query_time = $env -> perfdata -> searchindex_query_time ;
2020-03-15 18:10:23 +00:00
if ( isset ( $env -> perfdata -> didyoumean_correction ))
$result -> didyoumean_correction_time = $env -> perfdata -> didyoumean_correction ;
2017-03-23 21:13:20 +00:00
$result -> total_time = $result -> decode_time + $result -> query_time ;
2020-03-15 21:28:56 +00:00
// $result->stas = search::stas_parse(search::stas_split($_GET["query"]));
$result -> stas = $query_stas ;
2017-03-23 21:13:20 +00:00
$result -> search_results = $searchResults ;
exit ( json_encode ( $result , JSON_PRETTY_PRINT ));
});
2019-08-23 00:24:17 +00:00
2019-08-23 00:27:35 +00:00
/**
* @ api { get } ? action = stas - parse & query = { text } Debug search queries
* @ apiDescription Debug Pepperminty Wiki ' s understanding of search queries .
* If you want something machine - readable , check out the new stas property on the object returned by query - searchindex .
* @ apiName SearchSTASParse
* @ apiGroup Search
* @ apiPermission Anonymous
*
* @ apiParam { string } query The query string to parse .
*/
2019-08-23 00:24:17 +00:00
add_action ( " stas-parse " , function () {
global $settings ;
2020-03-15 17:54:27 +00:00
if ( ! isset ( $_GET [ " query " ])) {
http_response_code ( 400 );
header ( " x-status: failed " );
header ( " x-problem: no-query-specified " );
exit ( page_renderer :: render_main ( " Error - STAS Query Analysis - $settings->sitename " , " <p>No query was present in the <code>query</code> GET parameter.</p> " ));
}
2019-08-23 00:24:17 +00:00
$tokens = search :: stas_split ( $_GET [ " query " ]);
$stas_query = search :: stas_parse ( $tokens );
$result = " " ;
foreach ( $tokens as $token ) {
if ( in_array ( substr ( $token , 1 ), $stas_query [ " exclude " ])) {
2021-09-02 23:00:49 +00:00
$result .= " <span title='explicit exclude' style='color: red; text-decoration: dotted line-through;'> " . htmlentities ( substr ( $token , 1 )) . " </span> " ;
2019-08-23 00:24:17 +00:00
continue ;
}
$term = null ;
$token_part = $token ;
2019-12-15 20:03:04 +00:00
if ( $token_part [ 0 ] == " + " ) $token_part = substr ( $token_part , 1 );
2019-08-23 00:24:17 +00:00
if ( strpos ( $token_part , " : " ) !== false ) $token_part = explode ( " : " , $token_part , 2 )[ 1 ];
foreach ( $stas_query [ " terms " ] as $c_term ) {
2019-12-15 20:03:04 +00:00
// echo(var_export($token_part, true) . " / {$c_term["term"]}\n");
2019-08-23 00:24:17 +00:00
if ( $c_term [ " term " ] == $token_part ) {
$term = $c_term ;
break ;
}
}
if ( $term == null ) {
2021-09-02 23:00:49 +00:00
$result .= " <span title='unknown' style='color: black; text-decoration: wavy underline;'> " . htmlentities ( $token ) . " </span> " ;
2019-08-23 00:24:17 +00:00
continue ;
}
$title = " ? " ;
$style = " " ;
switch ( $term [ " weight " ]) {
case - 1 : $style .= " color: grey; text-decoration: wavy line-through; " ; $title = " stop word " ; break ;
case 1 : $style .= " color: blue; " ; $title = " normal word " ; break ;
}
2019-12-15 20:03:04 +00:00
if ( $term [ " weight " ] > 1 ) {
$style .= " color: darkblue; font-weight: bold; " ;
$title = " weighted word " ;
}
2019-12-15 17:56:56 +00:00
if ( $term [ " weight " ] !== - 1 ) {
switch ( $term [ " location " ]) {
case " body " : $style = " color: cyan " ; $title = " body only " ; break ;
case " title " : $style .= " font-weight: bolder; font-size: 1.2em; color: orange; " ; $title = " searching title only " ; $token = $token_part ; break ;
case " tags " : $style .= " font-weight: bolder; color: purple; " ; $title = " searching tags only " ; $token = $token_part ; break ;
case " all " : $title .= " , searching everywhere " ;
}
2019-08-23 00:24:17 +00:00
}
2019-12-15 20:03:04 +00:00
$title .= " , weight: { $term [ " weight " ] } " ;
2019-08-23 00:24:17 +00:00
2021-09-02 23:00:49 +00:00
$result .= " <span title=' $title ' style=' $style '> " . htmlentities ( $token ) . " </span> " ;
2019-08-23 00:24:17 +00:00
}
exit ( page_renderer :: render_main ( " STAS Query Analysis - $settings->sitename " , " <p> $settings->sitename understood your query to mean the following:</p>
< blockquote > $result </ blockquote > " ));
});
2017-06-28 08:45:13 +00:00
/*
* ██████ ██████ ███████ ███ ██ ███████ ███████ █████ ██████ ██████ ██ ██
* ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
* ██ ██ ██████ █████ ██ ██ ██ ███████ █████ ███████ ██████ ██ ███████
* ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
* ██████ ██ ███████ ██ ████ ███████ ███████ ██ ██ ██ ██ ██████ ██ ██
*/
/**
* @ api { get } ? action = opensearch - description Get the opensearch description file
* @ apiName OpenSearchDescription
* @ apiGroup Search
* @ apiPermission Anonymous
2016-10-01 10:32:38 +00:00
*/
add_action ( " opensearch-description " , function () {
global $settings ;
2021-09-02 23:00:49 +00:00
$siteRoot = htmlentities ( full_url () . " /index.php " , ENT_XML1 );
2016-10-01 10:32:38 +00:00
if ( ! isset ( $_GET [ " debug " ]))
header ( " content-type: application/opensearchdescription+xml " );
else
header ( " content-type: text/plain " );
2017-06-06 20:33:11 +00:00
exit ( '<?xml version="1.0" encoding="UTF-8"?' . '>' . // hack The build system strips it otherwise O.o I should really fix that.
" \ n<OpenSearchDescription xmlns= \" http://a9.com/-/spec/opensearch/1.1/ \" >
2016-10-01 10:32:38 +00:00
< ShortName > Search $settings -> sitename </ ShortName >
< Description > Search $settings -> sitename , which is powered by Pepperminty Wiki .</ Description >
< Tags > $settings -> sitename Wiki </ Tags >
< Image type = \ " image/png \" > $settings->favicon </Image>
< Attribution > Search content available under the license linked to at the bottom of the search results page .</ Attribution >
< Developer > Starbeamrainbowlabs ( https :// github . com / sbrl / Pepperminty - Wiki / graphs / contributors ) </ Developer >
< InputEncoding > UTF - 8 </ InputEncoding >
< OutputEncoding > UTF - 8 </ OutputEncoding >
2017-07-16 09:26:22 +00:00
< Url type = \ " text/html \" method= \" get \" template= \" $siteRoot ?action=view&search-redirect=yes&page= { searchTerms}&offset= { startIndex?}&count= { count} \" />
2017-06-28 10:21:42 +00:00
< Url type = \ " application/x-suggestions+json \" template= \" $siteRoot ?action=suggest-pages&query= { searchTerms}&type=opensearch \" />
2017-06-06 20:33:11 +00:00
</ OpenSearchDescription > " );
2016-10-01 10:32:38 +00:00
});
2016-11-20 13:24:35 +00:00
2017-06-28 08:45:13 +00:00
/**
2018-02-14 23:08:28 +00:00
* @ api { get } ? action = suggest - pages [ & type = { type }] Get page name suggestions for a query
2017-06-28 08:45:13 +00:00
* @ apiName OpenSearchDescription
* @ apiGroup Search
* @ apiPermission Anonymous
*
* @ apiParam { string } text The search query string to get search suggestions for .
2017-06-28 09:44:44 +00:00
* @ apiParam { string } type The type of result to return . Default value : json . Available values : json , opensearch
2017-06-28 08:45:13 +00:00
*/
2016-11-20 13:24:35 +00:00
add_action ( " suggest-pages " , function () {
global $settings , $pageindex ;
2019-08-22 21:11:09 +00:00
if ( $settings -> dynamic_page_suggestion_count === 0 ) {
2016-11-20 13:24:35 +00:00
header ( " content-type: application/json " );
2017-06-28 10:21:42 +00:00
header ( " content-length: 3 " );
exit ( " [] \n " );
2016-11-20 13:24:35 +00:00
}
if ( empty ( $_GET [ " query " ])) {
http_response_code ( 400 );
header ( " content-type: text/plain " );
exit ( " Error: You didn't specify the 'query' GET parameter. " );
}
2017-06-28 09:44:44 +00:00
$type = $_GET [ " type " ] ? ? " json " ;
if ( ! in_array ( $type , [ " json " , " opensearch " ])) {
http_response_code ( 406 );
2021-09-02 23:00:49 +00:00
header ( " content-type: text/plain " );
2017-06-28 09:44:44 +00:00
exit ( " Error: The type ' $type ' is not one of the supported output types. Available values: json, opensearch. Default: json " );
}
2019-12-08 21:04:59 +00:00
$query = search :: $literator -> transliterate ( $_GET [ " query " ]);
2018-06-25 22:03:00 +00:00
2016-11-20 13:24:35 +00:00
// Rank each page name
$results = [];
foreach ( $pageindex as $pageName => $entry ) {
$results [] = [
" pagename " => $pageName ,
// Costs: Insert: 1, Replace: 8, Delete: 6
2019-12-08 21:04:59 +00:00
" distance " => levenshtein ( $query , search :: $literator -> transliterate ( $pageName ), 1 , 8 , 6 )
2016-11-20 13:24:35 +00:00
];
}
2017-06-28 09:44:44 +00:00
// Sort the page names by distance from the original query
2016-11-20 13:24:35 +00:00
usort ( $results , function ( $a , $b ) {
if ( $a [ " distance " ] == $b [ " distance " ])
return strcmp ( $a [ " pagename " ], $b [ " pagename " ]);
return $a [ " distance " ] < $b [ " distance " ] ? - 1 : 1 ;
});
// Send the results to the user
2017-06-28 09:44:44 +00:00
$suggestions = array_slice ( $results , 0 , $settings -> dynamic_page_suggestion_count );
switch ( $type )
{
case " json " :
header ( " content-type: application/json " );
exit ( json_encode ( $suggestions ));
case " opensearch " :
$opensearch_output = [
$_GET [ " query " ],
array_map ( function ( $suggestion ) { return $suggestion [ " pagename " ]; }, $suggestions )
];
header ( " content-type: application/x-suggestions+json " );
exit ( json_encode ( $opensearch_output ));
}
2016-11-20 13:24:35 +00:00
});
if ( $settings -> dynamic_page_suggestion_count > 0 )
{
2019-01-27 22:56:51 +00:00
page_renderer :: add_js_snippet ( ' /// Dynamic page suggestion system
2016-11-20 13:24:35 +00:00
// Micro snippet 8 - Promisified GET (fetched 20th Nov 2016)
function get ( u ){ return new Promise ( function ( r , t , a ){ a = new XMLHttpRequest (); a . onload = function ( b , c ){ b = a . status ; c = a . response ; if ( b > 199 && b < 300 ){ r ( c )} else { t ( c )}}; a . open ( " GET " , u , true ); a . send ( null )})}
window . addEventListener ( " load " , function ( event ) {
var searchBox = document . querySelector ( " input[type=search] " );
searchBox . dataset . lastValue = " " ;
searchBox . addEventListener ( " keyup " , function ( event ) {
// Make sure that we don\'t keep sending requests to the server if nothing has changed
if ( searchBox . dataset . lastValue == event . target . value )
return ;
searchBox . dataset . lastValue = event . target . value ;
// Fetch the suggestions from the server
get ( " ?action=suggest-pages&query= " + encodeURIComponent ( event . target . value )) . then ( function ( response ) {
var suggestions = JSON . parse ( response ),
dataList = document . getElementById ( " allpages " );
// If the server sent no suggestions, then we shouldn\'t replace the contents of the datalist
if ( suggestions . length == 0 )
return ;
console . info ( `Fetched suggestions for ${event.target.value}:` , suggestions . map ( s => s . pagename ));
// Remove all the existing suggestions
while ( dataList . firstChild ) {
dataList . removeChild ( dataList . firstChild );
}
// Add the new suggestions to the datalist
2016-11-28 13:05:23 +00:00
var optionsFrag = document . createDocumentFragment ();
2016-11-20 13:24:35 +00:00
suggestions . forEach ( function ( suggestion ) {
var suggestionElement = document . createElement ( " option " );
suggestionElement . value = suggestion . pagename ;
suggestionElement . dataset . distance = suggestion . distance ;
2016-11-28 13:05:23 +00:00
optionsFrag . appendChild ( suggestionElement );
2016-11-20 13:24:35 +00:00
});
2016-11-28 13:05:23 +00:00
dataList . appendChild ( optionsFrag );
2016-11-20 13:24:35 +00:00
});
});
});
' );
}
2019-08-24 18:56:14 +00:00
2020-03-11 23:07:38 +00:00
if ( module_exists ( " feature-cli " )) {
cli_register ( " search " , " Query and manipulate the search index " , function ( array $args ) : int {
if ( count ( $args ) < 1 ) {
echo ( " search: query and manipulate the search index
Usage :
search { subcommand }
Subcommands :
rebuild Rebuilds the search index
" );
return 0 ;
}
switch ( $args [ 0 ]) {
case " rebuild " :
search :: invindex_rebuild ();
break ;
}
return 0 ;
});
}
2019-08-24 18:56:14 +00:00
add_help_section ( " 27-search " , " Searching " , " <p> $settings->sitename has an integrated full-text search engine, allowing you to search all of the pages on $settings->sitename and their content. To use it, simply enter your query into the page name box and press enter. If a page isn't found with the exact name of your query terms, a search will be performed instead.</p>
< p > Additionally , advanced users can take advantage of some extra query syntax that $settings -> sitename supports , which is inspired by popular search engines :</ p >
< table >
< tr >< th style = 'width: 33%;' > Example </ th >< th style = 'width: 66%;' > Meaning </ th ></ tr >
< tr >< td >< code > cat - dog </ code ></ td >< td > Search for pages containing \ " cat \" , but not \" dog \" . This syntax does not make sense on it's own - other words must be present for it to take effect.</td>
< tr >< td >< code >+ glass marble </ code ></ td >< td > Double the weighting of the word \ " glass \" .</td>
< tr >< td >< code > intitle : rocket </ code ></ td >< td > Search only page titles for \ " rocket \" .</td>
< tr >< td >< code > intags : bill </ code ></ td >< td > Search only tags for \ " bill \" .</td>
< tr >< td >< code > inbody : satellite </ code ></ td >< td > Search only the page body for \ " satellite \" .</td>
</ table >
< p > More query syntax will be added in the future , so keep an eye on < a href = 'https://github.com/sbrl/Pepperminty-Wiki/releases/' > the latest releases </ a > of < em > Pepperminty Wiki </ em > to stay up - to - date ( < a href = 'https://github.com/sbrl/Pepperminty-Wiki/releases.atom' > Atom / RSS feed available here </ a > ) .</ p > " );
2015-10-27 21:10:05 +00:00
}
]);
?>