2 // PukiWiki - Yet another WikiWikiWeb clone.
4 // Copyright 2017-2021 PukiWiki Development Team
5 // License: GPL v2 or (at your option) any later version
7 // Search2 plugin - Show detail result using JavaScript
9 define('PLUGIN_SEARCH2_MAX_BASE', 16); // #search(1,2,3,...,15,16)
11 define('PLUGIN_SEARCH2_RESULT_RECORD_LIMIT', 1000);
12 define('PLUGIN_SEARCH2_RESULT_RECORD_LIMIT_START', 100);
13 define('PLUGIN_SEARCH2_SEARCH_WAIT_MILLISECONDS', 1000);
14 define('PLUGIN_SEARCH2_SEARCH_MAX_RESULTS', 500);
16 // Show a search box on a page
17 function plugin_search2_convert()
19 return 'Usage: Please use #search()';
22 function plugin_search2_action()
24 global $vars, $_title_search, $_title_result, $_msg_searching;
26 $action = isset($vars['action']) ? $vars['action'] : '';
27 $base = isset($vars['base']) ? $vars['base'] : '';
28 $start_s = isset($vars['start']) ? $vars['start'] : '';
29 $start_index = pkwk_ctype_digit($start_s) ? intval($start_s) : 0;
35 $q = trim(isset($vars['q']) ? $vars['q'] : '');
36 $offset_s = isset($vars['offset']) ? $vars['offset'] : '';
37 $offset = pkwk_ctype_digit($offset_s) ? intval($offset_s) : 0;
38 $prev_offset_s = isset($vars['prev_offset']) ? $vars['prev_offset'] : '';
40 return array('msg' => $_title_search,
41 'body' => "<br>" . $_msg_searching . "\n" .
42 plugin_search2_search_form($q, $bases, $offset));
44 $msg = str_replace('$1', htmlsc($q), $_title_result);
45 return array('msg' => $msg,
46 'body' => plugin_search2_search_form($q, $bases, $offset, $prev_offset_s));
48 } else if ($action === 'query') {
49 $q = isset($vars['q']) ? $vars['q'] : '';
50 $search_start_time = isset($vars['search_start_time']) ?
51 $vars['search_start_time'] : null;
52 $modified_since = (int)(isset($vars['modified_since']) ?
53 $vars['modified_since'] : '0');
54 header('Content-Type: application/json; charset=UTF-8');
55 plugin_search2_do_search($q, $base, $start_index,
56 $search_start_time, $modified_since);
61 function plugin_search2_get_base_url($search_text)
65 if (!defined('PKWK_UTF8_ENABLE')) {
66 $params[] = 'encode_hint=' . rawurlencode($vars['encode_hint']);
68 $params[] = 'cmd=search2';
69 if (isset($vars['encode_hint']) && $vars['encode_hint']) {
70 $params[] = 'encode_hint=' . rawurlencode($vars['encode_hint']);
73 $params[] = 'q=' . plugin_search2_urlencode_searchtext($search_text);
75 if (isset($vars['base']) && $vars['base']) {
76 $params[] = 'base=' . rawurlencode($vars['base']);
78 $url = get_base_uri() . '?' . join('&', $params);
82 function plugin_search2_urlencode_searchtext($search_text)
84 $s2 = preg_replace('#^\s+|\s+$#', '', $search_text);
86 $sp = preg_split('#\s+#', $s2);
88 for ($i = 0; $i < count($sp); $i++) {
89 $list[] = rawurlencode($sp[$i]);
91 return join('+', $list);
94 function plugin_search2_do_search($query_text, $base, $start_index,
95 $search_start_time, $modified_since)
97 global $whatsnew, $non_list, $search_non_list;
98 global $_msg_andresult, $_msg_orresult;
99 global $search_auth, $auth_user;
101 $result_record_limit = $start_index === 0 ?
102 PLUGIN_SEARCH2_RESULT_RECORD_LIMIT_START : PLUGIN_SEARCH2_RESULT_RECORD_LIMIT;
105 $b_type_and = true; // AND:TRUE OR:FALSE
106 $key_candidates = preg_split('/\s+/', $query_text, -1, PREG_SPLIT_NO_EMPTY);
107 for ($i = count($key_candidates) - 2; $i >= 1; $i--) {
108 if ($key_candidates[$i] === 'OR') {
110 unset($key_candidates[$i]);
113 $key_candidates = array_merge($key_candidates);
114 $keys = get_search_words($key_candidates);
115 foreach ($keys as $key=>$value)
116 $keys[$key] = '/' . $value . '/S';
118 if ($modified_since > 0) {
120 $recent_files = get_recent_files();
121 $modified_loc = $modified_since - LOCALZONE;
123 foreach ($recent_files as $p => $time) {
124 if ($time >= $modified_loc) {
129 $pages = preg_grep('/^' . preg_quote($base, '/') . '/S', $pages);
131 $page_names = $pages;
134 $pages = get_existpages();
138 $pages = preg_grep('/^' . preg_quote($base, '/') . '/S', $pages);
140 if (! $search_non_list) {
141 $pages = array_diff($pages, preg_grep('/' . $non_list . '/S', $pages));
143 $pages = array_flip($pages);
144 unset($pages[$whatsnew]);
145 $page_names = array_keys($pages);
147 natsort($page_names);
149 if (is_null($search_start_time)) {
150 // Don't use client cache
151 $search_start_time = UTIME + LOCALZONE;
153 $found_pages = array();
154 $readable_page_index = -1;
155 $scan_page_index = -1;
156 $saved_scan_start_index = -1;
157 $last_read_page_name = null;
158 foreach ($page_names as $page) {
160 $pagename_only = false;
162 if (! is_page_readable($page)) {
164 // $search_auth - 1: User can know page names that contain search text if the page is readable
167 // $search_auth - 0: All users can know page names that conntain search text
168 $pagename_only = true;
170 $readable_page_index++;
171 if ($readable_page_index < $start_index) {
172 // Skip: It's not time to read
175 if ($saved_scan_start_index === -1) {
176 $saved_scan_start_index = $scan_page_index;
178 if (count($keys) > 0) {
179 // Search for page name and contents
180 $body = get_source($page, TRUE, TRUE, TRUE);
181 $target = $page . "\n" . remove_author_header($body);
182 foreach ($keys as $key) {
183 $b_match = preg_match($key, $target);
184 if ($b_type_and xor $b_match) break; // OR
187 // No search target. get_source($page) is meaningless.
188 // $b_match is always false.
192 $author_info = get_author_info($body);
194 $updated_at = get_update_datetime_from_author($author_info);
195 $updated_time = strtotime($updated_at);
197 $updated_time = filemtime(get_filename($page));
198 $updated_at = get_date_atom($updated_time);
200 if ($pagename_only) {
201 // The user cannot read this page body
202 $found_pages[] = array('name' => (string)$page,
203 'url' => get_page_uri($page), 'updated_at' => $updated_at,
204 'updated_time' => $updated_time,
205 'body' => '', 'pagename_only' => 1);
207 $found_pages[] = array('name' => (string)$page,
208 'url' => get_page_uri($page), 'updated_at' => $updated_at,
209 'updated_time' => $updated_time,
210 'body' => (string)$body);
213 $last_read_page_name = $page;
214 if ($start_index + $result_record_limit <= $readable_page_index + 1) {
219 $message = str_replace('$1', htmlsc($query_text), str_replace('$2', count($found_pages),
220 str_replace('$3', count($page_names), $b_type_and ? $_msg_andresult : $_msg_orresult)));
221 $search_done = (boolean)($scan_page_index + 1 === count($page_names));
223 'message' => $message,
225 'start_index' => $start_index,
226 'limit' => $result_record_limit,
227 'read_page_count' => $readable_page_index - $start_index + 1,
228 'scan_page_count' => $scan_page_index - $saved_scan_start_index + 1,
229 'page_count' => count($page_names),
230 'last_read_page_name' => $last_read_page_name,
231 'next_start_index' => $readable_page_index + 1,
232 'search_done' => $search_done,
233 'search_start_time' => $search_start_time,
234 'auth_user' => $auth_user,
235 'results' => $found_pages);
237 if (!defined('PKWK_UTF8_ENABLE')) {
238 if (SOURCE_ENCODING === 'EUC-JP') {
239 mb_convert_variables('UTF-8', 'CP51932', $obj);
241 mb_convert_variables('UTF-8', SOURCE_ENCODING, $obj);
244 print(json_encode($obj, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
247 function plugin_search2_search_form($search_text = '', $bases = array(),
248 $offset, $prev_offset_s = null)
251 global $_search_pages, $_search_all;
252 global $_msg_andresult, $_msg_orresult, $_msg_notfoundresult;
253 global $_search_detail, $_search_searching, $_search_showing_result;
254 global $_msg_unsupported_webbrowser, $_msg_use_alternative_link;
255 global $_msg_more_results, $_msg_prev_results, $_msg_general_error;
258 static $search2_form_total_count = 0;
259 $search2_form_total_count++;
260 $script = get_base_uri();
261 $h_search_text = htmlsc($search_text);
264 if (!empty($bases)) {
268 foreach($bases as $base) {
270 if (PLUGIN_SEARCH2_MAX_BASE < $_num) break;
271 $s_base = htmlsc($base);
272 $base_str = '<strong>' . $s_base . '</strong>';
273 $base_label = str_replace('$1', $base_str, $_search_pages);
277 <input type="radio" name="base" value="$s_base" $check> $base_label
284 <label><input type="radio" name="base" value=""> $_search_all</label>
286 $base_option = '<div class="small">' . $base_msg . '</div>';
288 $_search2_result_notfound = htmlsc($_msg_notfoundresult);
289 $_search2_result_found = htmlsc($_msg_andresult);
290 $_search2_search_wait_milliseconds = PLUGIN_SEARCH2_SEARCH_WAIT_MILLISECONDS;
291 $result_page_panel =<<<EOD
292 <input type="checkbox" id="_plugin_search2_detail" checked><label for="_plugin_search2_detail">$_search_detail</label>
293 <ul id="_plugin_search2_result-list">
296 if ($h_search_text == '' || $search2_form_total_count > 1) {
297 $result_page_panel = '';
300 $plain_search_link = '<a href="' . $script . '?cmd=search' . '">' . htmlsc($_btn_search) . '</a>';
301 $alt_msg = str_replace('$1', $plain_search_link, $_msg_use_alternative_link);
302 $status_span_text = '<span class="_plugin_search2_search_status_text1"></span>' .
303 '<span class="_plugin_search2_search_status_text2"></span>';
305 <form action="$script" method="GET" class="_plugin_search2_form">
307 <input type="hidden" name="cmd" value="search2">
308 <input type="search" name="q" value="$h_search_text" data-original-q="$h_search_text" size="40">
309 <input type="submit" value="$_btn_search">
315 <div class="_plugin_search2_second_form" style="display:none;">
316 <div class="_plugin_search2_search_status">$status_span_text</span></div>
317 <div class="_plugin_search2_message"></div>
322 $h_auth_user = htmlsc($auth_user);
323 $h_base_url = htmlsc(plugin_search2_get_base_url($search_text));
324 $h_msg_more_results = htmlsc($_msg_more_results);
325 $h_msg_prev_results = htmlsc($_msg_prev_results);
326 $max_results = PLUGIN_SEARCH2_SEARCH_MAX_RESULTS;
327 $prev_offset = pkwk_ctype_digit($prev_offset_s) ? $prev_offset_s : '';
328 $search_props =<<<EOD
329 <div style="display:none;">
330 <input type="hidden" id="_plugin_search2_auth_user" value="$h_auth_user">
331 <input type="hidden" id="_plugin_search2_base_url" value="$h_base_url">
332 <input type="hidden" id="_plugin_search2_msg_searching" value="$_search_searching">
333 <input type="hidden" id="_plugin_search2_msg_showing_result" value="$_search_showing_result">
334 <input type="hidden" id="_plugin_search2_msg_result_notfound" value="$_search2_result_notfound">
335 <input type="hidden" id="_plugin_search2_msg_result_found" value="$_search2_result_found">
336 <input type="hidden" id="_plugin_search2_msg_more_results" value="$h_msg_more_results">
337 <input type="hidden" id="_plugin_search2_msg_prev_results" value="$h_msg_prev_results">
338 <input type="hidden" id="_plugin_search2_search_wait_milliseconds" value="$_search2_search_wait_milliseconds">
339 <input type="hidden" id="_plugin_search2_max_results" value="$max_results">
340 <input type="hidden" id="_plugin_search2_offset" value="$offset">
341 <input type="hidden" id="_plugin_search2_prev_offset" value="$prev_offset">
342 <input type="hidden" id="_plugin_search2_msg_error" value="$_msg_general_error">
345 if ($search2_form_total_count > 1) {
351 <p>$_msg_unsupported_webbrowser $alt_msg</p>
354 input#_plugin_search2_detail:checked ~ ul > li > div.search-result-detail {
357 input#_plugin_search2_detail ~ ul > li > div.search-result-detail {
360 ._plugin_search2_search_status {
363 @keyframes plugin-search2-searching {
369 span.plugin-search2-progress {
370 animation: plugin-search2-searching 1.5s infinite ease-out;
372 span.plugin-search2-progress1 {
373 animation-delay: -1s;
375 span.plugin-search2-progress2 {
376 animation-delay: -0.8s;
378 span.plugin-search2-progress3 {
379 animation-delay: -0.6s;
382 <p class="_plugin_search2_nosupport_message" style="display:none;">
383 $_msg_unsupported_webbrowser $alt_msg
387 <div class="_plugin_search2_search_status">$status_span_text</div>
388 <div class="_plugin_search2_message"></div>