OSDN Git Service

177a134a7ce8cd79145ac60da518ca6fd22a0fdd
[pukiwiki/pukiwiki.git] / plugin / search2.inc.php
1 <?php
2 // PukiWiki - Yet another WikiWikiWeb clone.
3 // search2.inc.php
4 // Copyright 2017-2022 PukiWiki Development Team
5 // License: GPL v2 or (at your option) any later version
6 //
7 // Search2 plugin - Show detail result using JavaScript
8
9 define('PLUGIN_SEARCH2_MAX_BASE', 16); // #search(1,2,3,...,15,16)
10
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);
15
16 // Show a search box on a page
17 function plugin_search2_convert()
18 {
19         return 'Usage: Please use #search()';
20 }
21
22 function plugin_search2_action()
23 {
24         global $vars, $_title_search, $_title_result, $_msg_searching;
25
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;
30         $bases = array();
31         if ($base !== '') {
32                 $bases[] = $base;
33         }
34         if ($action === '') {
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'] : '';
39                 if ($q === '') {
40                         return array('msg' => $_title_search,
41                                 'body' => "<br>" . $_msg_searching . "\n" .
42                                 plugin_search2_search_form($q, $bases, $offset));
43                 } else {
44                         $q2 = $q;
45                         if (defined('PKWK_UTF8_ENABLE')) {
46                                 $zen_space = "\xE3\x80\x80"; // IDEOGRAPHIC SPACE in UTF-8 - &#x3000;
47                                 $q2 = str_replace($zen_space, ' ', $q);
48                         }
49                         $msg  = str_replace('$1', htmlsc($q2), $_title_result);
50                         return array('msg' => $msg,
51                                 'body' => plugin_search2_search_form($q2, $bases, $offset, $prev_offset_s));
52                 }
53         } else if ($action === 'query') {
54                 $q = isset($vars['q']) ? $vars['q'] : '';
55                 $search_start_time = isset($vars['search_start_time']) ?
56                         $vars['search_start_time'] : null;
57                 $modified_since = (int)(isset($vars['modified_since']) ?
58                         $vars['modified_since'] : '0');
59                 header('Content-Type: application/json; charset=UTF-8');
60                 plugin_search2_do_search($q, $base, $start_index,
61                         $search_start_time, $modified_since);
62                 exit;
63         }
64 }
65
66 function plugin_search2_get_base_url($search_text)
67 {
68         global $vars;
69         $params = array();
70         if (!defined('PKWK_UTF8_ENABLE')) {
71                 $params[] = 'encode_hint=' . rawurlencode($vars['encode_hint']);
72         }
73         $params[] = 'cmd=search2';
74         if (isset($vars['encode_hint']) && $vars['encode_hint']) {
75                 $params[] = 'encode_hint=' . rawurlencode($vars['encode_hint']);
76         }
77         if ($search_text) {
78                 $params[] = 'q=' . plugin_search2_urlencode_searchtext($search_text);
79         }
80         if (isset($vars['base']) && $vars['base']) {
81                 $params[] = 'base=' . rawurlencode($vars['base']);
82         }
83         $url = get_base_uri() . '?' . join('&', $params);
84         return $url;
85 }
86
87 function plugin_search2_urlencode_searchtext($search_text)
88 {
89         $s2 = preg_replace('#^\s+|\s+$#', '', $search_text);
90         if (!$s2) return '';
91         $sp = preg_split('#\s+#', $s2);
92         $list = array();
93         for ($i = 0; $i < count($sp); $i++) {
94                 $list[] = rawurlencode($sp[$i]);
95         }
96         return join('+', $list);
97 }
98
99 function plugin_search2_do_search($query_text, $base, $start_index,
100         $search_start_time, $modified_since)
101 {
102         global $whatsnew, $non_list, $search_non_list;
103         global $_msg_andresult, $_msg_orresult;
104         global $search_auth, $auth_user;
105
106         $result_record_limit = $start_index === 0 ?
107                 PLUGIN_SEARCH2_RESULT_RECORD_LIMIT_START : PLUGIN_SEARCH2_RESULT_RECORD_LIMIT;
108         $retval = array();
109
110         $b_type_and = true; // AND:TRUE OR:FALSE
111         $key_candidates = preg_split('/\s+/', $query_text, -1, PREG_SPLIT_NO_EMPTY);
112         for ($i = count($key_candidates) - 2; $i >= 1; $i--) {
113                 if ($key_candidates[$i] === 'OR') {
114                         $b_type_and = false;
115                         unset($key_candidates[$i]);
116                 }
117         }
118         $key_candidates = array_merge($key_candidates);
119         $keys = get_search_words($key_candidates);
120         foreach ($keys as $key=>$value)
121                 $keys[$key] = '/' . $value . '/S';
122
123         if ($modified_since > 0) {
124                 // Recent search
125                 $recent_files = get_recent_files();
126                 $modified_loc = $modified_since - LOCALZONE;
127                 $pages = array();
128                 foreach ($recent_files as $p => $time) {
129                         if ($time >= $modified_loc) {
130                                 $pages[] = $p;
131                         }
132                 }
133                 if ($base != '') {
134                         $pages = preg_grep('/^' . preg_quote($base, '/') . '/S', $pages);
135                 }
136                 $page_names = $pages;
137         } else {
138                 // Normal search
139                 $pages = get_existpages();
140
141                 // Avoid
142                 if ($base != '') {
143                         $pages = preg_grep('/^' . preg_quote($base, '/') . '/S', $pages);
144                 }
145                 if (! $search_non_list) {
146                         $pages = array_diff($pages, preg_grep('/' . $non_list . '/S', $pages));
147                 }
148                 $pages = array_flip($pages);
149                 unset($pages[$whatsnew]);
150                 $page_names = array_keys($pages);
151         }
152         natsort($page_names);
153         // Cache collabolate
154         if (is_null($search_start_time)) {
155                 // Don't use client cache
156                 $search_start_time = UTIME + LOCALZONE;
157         }
158         $found_pages = array();
159         $readable_page_index = -1;
160         $scan_page_index = -1;
161         $saved_scan_start_index = -1;
162         $last_read_page_name = null;
163         foreach ($page_names as $page) {
164                 $b_match = FALSE;
165                 $pagename_only = false;
166                 $scan_page_index++;
167                 if (! is_page_readable($page)) {
168                         if ($search_auth) {
169                                 // $search_auth - 1: User can know page names that contain search text if the page is readable
170                                 continue;
171                         }
172                         // $search_auth - 0: All users can know page names that conntain search text
173                         $pagename_only = true;
174                 }
175                 $readable_page_index++;
176                 if ($readable_page_index < $start_index) {
177                         // Skip: It's not time to read
178                         continue;
179                 }
180                 if ($saved_scan_start_index === -1) {
181                         $saved_scan_start_index = $scan_page_index;
182                 }
183                 if (count($keys) > 0) {
184                         // Search for page name and contents
185                         $body = get_source($page, TRUE, TRUE, TRUE);
186                         $target = $page . "\n" . remove_author_header($body);
187                         foreach ($keys as $key) {
188                                 $b_match = preg_match($key, $target);
189                                 if ($b_type_and xor $b_match) break; // OR
190                         }
191                 } else {
192                         // No search target. get_source($page) is meaningless.
193                         // $b_match is always false.
194                 }
195                 if ($b_match) {
196                         // Found!
197                         $author_info = get_author_info($body);
198                         if ($author_info) {
199                                 $updated_at = get_update_datetime_from_author($author_info);
200                                 $updated_time = strtotime($updated_at);
201                         } else {
202                                 $updated_time = filemtime(get_filename($page));
203                                 $updated_at = get_date_atom($updated_time);
204                         }
205                         if ($pagename_only) {
206                                 // The user cannot read this page body
207                                 $found_pages[] = array('name' => (string)$page,
208                                         'url' => get_page_uri($page), 'updated_at' => $updated_at,
209                                         'updated_time' => $updated_time,
210                                         'body' => '', 'pagename_only' => 1);
211                         } else {
212                                 $found_pages[] = array('name' => (string)$page,
213                                         'url' => get_page_uri($page), 'updated_at' => $updated_at,
214                                         'updated_time' => $updated_time,
215                                         'body' => (string)$body);
216                         }
217                 }
218                 $last_read_page_name = $page;
219                 if ($start_index + $result_record_limit <= $readable_page_index + 1) {
220                         // Read page limit
221                         break;
222                 }
223         }
224         $message = str_replace('$1', htmlsc($query_text), str_replace('$2', count($found_pages),
225                 str_replace('$3', count($page_names), $b_type_and ? $_msg_andresult : $_msg_orresult)));
226         $search_done = (boolean)($scan_page_index + 1 === count($page_names));
227         $result_obj = array(
228                 'message' => $message,
229                 'q' => $query_text,
230                 'start_index' => $start_index,
231                 'limit' => $result_record_limit,
232                 'read_page_count' => $readable_page_index - $start_index + 1,
233                 'scan_page_count' => $scan_page_index - $saved_scan_start_index + 1,
234                 'page_count' => count($page_names),
235                 'last_read_page_name' => $last_read_page_name,
236                 'next_start_index' => $readable_page_index + 1,
237                 'search_done' => $search_done,
238                 'search_start_time' => $search_start_time,
239                 'auth_user' => $auth_user,
240                 'results' => $found_pages);
241         $obj = $result_obj;
242         if (!defined('PKWK_UTF8_ENABLE')) {
243                 if (SOURCE_ENCODING === 'EUC-JP') {
244                         mb_convert_variables('UTF-8', 'CP51932', $obj);
245                 } else {
246                         mb_convert_variables('UTF-8', SOURCE_ENCODING, $obj);
247                 }
248         }
249         print(json_encode($obj, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
250 }
251
252 function plugin_search2_search_form($search_text = '', $bases = array(),
253         $offset, $prev_offset_s = null)
254 {
255         global $_btn_search;
256         global $_search_pages, $_search_all;
257         global $_msg_andresult, $_msg_orresult, $_msg_notfoundresult;
258         global $_search_detail, $_search_searching, $_search_showing_result;
259         global $_msg_unsupported_webbrowser, $_msg_use_alternative_link;
260         global $_msg_more_results, $_msg_prev_results, $_msg_general_error;
261         global $auth_user;
262
263         static $search2_form_total_count = 0;
264         $search2_form_total_count++;
265         $script = get_base_uri();
266         $h_search_text = htmlsc($search_text);
267
268         $base_option = '';
269         if (!empty($bases)) {
270                 $base_msg = '';
271                 $_num = 0;
272                 $check = ' checked';
273                 foreach($bases as $base) {
274                         ++$_num;
275                         if (PLUGIN_SEARCH2_MAX_BASE < $_num) break;
276                         $s_base   = htmlsc($base);
277                         $base_str = '<strong>' . $s_base . '</strong>';
278                         $base_label = str_replace('$1', $base_str, $_search_pages);
279                         $base_msg  .=<<<EOD
280  <div>
281   <label>
282    <input type="radio" name="base" value="$s_base" $check> $base_label
283   </label>
284  </div>
285 EOD;
286                         $check = '';
287                 }
288                 $base_msg .=<<<EOD
289 <label><input type="radio" name="base" value=""> $_search_all</label>
290 EOD;
291                 $base_option = '<div class="small">' . $base_msg . '</div>';
292         }
293         $_search2_result_notfound = htmlsc($_msg_notfoundresult);
294         $_search2_result_found = htmlsc($_msg_andresult);
295         $_search2_search_wait_milliseconds = PLUGIN_SEARCH2_SEARCH_WAIT_MILLISECONDS;
296         $result_page_panel =<<<EOD
297 <input type="checkbox" id="_plugin_search2_detail" checked><label for="_plugin_search2_detail">$_search_detail</label>
298 <ul id="_plugin_search2_result-list">
299 </ul>
300 EOD;
301         if ($h_search_text == '' || $search2_form_total_count > 1) {
302                 $result_page_panel = '';
303         }
304
305         $plain_search_link = '<a href="' . $script . '?cmd=search' . '">' . htmlsc($_btn_search) . '</a>';
306         $alt_msg = str_replace('$1', $plain_search_link, $_msg_use_alternative_link);
307         $status_span_text = '<span class="_plugin_search2_search_status_text1"></span>' .
308                 '<span class="_plugin_search2_search_status_text2"></span>';
309         $form =<<<EOD
310 <form action="$script" method="GET" class="_plugin_search2_form">
311  <div>
312   <input type="hidden" name="cmd" value="search2">
313   <input type="search" name="q" value="$h_search_text" data-original-q="$h_search_text" size="40">
314   <input type="submit" value="$_btn_search">
315  </div>
316 $base_option
317 </form>
318 EOD;
319         $second_form =<<<EOD
320 <div class="_plugin_search2_second_form" style="display:none;">
321 <div class="_plugin_search2_search_status">$status_span_text</span></div>
322 <div class="_plugin_search2_message"></div>
323 $form
324 </div>
325 EOD;
326
327         $h_auth_user = htmlsc($auth_user);
328         $h_base_url = htmlsc(plugin_search2_get_base_url($search_text));
329         $h_msg_more_results = htmlsc($_msg_more_results);
330         $h_msg_prev_results = htmlsc($_msg_prev_results);
331         $max_results = PLUGIN_SEARCH2_SEARCH_MAX_RESULTS;
332         $prev_offset = pkwk_ctype_digit($prev_offset_s) ? $prev_offset_s : '';
333         $search_props =<<<EOD
334 <div style="display:none;">
335   <input type="hidden" id="_plugin_search2_auth_user" value="$h_auth_user">
336   <input type="hidden" id="_plugin_search2_base_url" value="$h_base_url">
337   <input type="hidden" id="_plugin_search2_msg_searching" value="$_search_searching">
338   <input type="hidden" id="_plugin_search2_msg_showing_result" value="$_search_showing_result">
339   <input type="hidden" id="_plugin_search2_msg_result_notfound" value="$_search2_result_notfound">
340   <input type="hidden" id="_plugin_search2_msg_result_found" value="$_search2_result_found">
341   <input type="hidden" id="_plugin_search2_msg_more_results" value="$h_msg_more_results">
342   <input type="hidden" id="_plugin_search2_msg_prev_results" value="$h_msg_prev_results">
343   <input type="hidden" id="_plugin_search2_search_wait_milliseconds" value="$_search2_search_wait_milliseconds">
344   <input type="hidden" id="_plugin_search2_max_results" value="$max_results">
345   <input type="hidden" id="_plugin_search2_offset" value="$offset">
346   <input type="hidden" id="_plugin_search2_prev_offset" value="$prev_offset">
347   <input type="hidden" id="_plugin_search2_msg_error" value="$_msg_general_error">
348 </div>
349 EOD;
350         if ($search2_form_total_count > 1) {
351                 $search_props = '';
352         }
353
354         return <<<EOD
355 <noscript>
356  <p>$_msg_unsupported_webbrowser $alt_msg</p>
357 </noscript>
358 <style>
359 input#_plugin_search2_detail:checked ~ ul > li > div.search-result-detail {
360   display:block;
361 }
362 input#_plugin_search2_detail ~ ul > li > div.search-result-detail {
363   display:none;
364 }
365 ._plugin_search2_search_status {
366   min-height:1.5em;
367 }
368 @keyframes plugin-search2-searching {
369   10% { opacity: 1; }
370   40% { opacity: 0; }
371   70% { opacity: 0; }
372   90% { opacity: 1; }
373 }
374 span.plugin-search2-progress {
375   animation: plugin-search2-searching 1.5s infinite ease-out;
376 }
377 span.plugin-search2-progress1 {
378   animation-delay: -1s;
379 }
380 span.plugin-search2-progress2 {
381   animation-delay: -0.8s;
382 }
383 span.plugin-search2-progress3 {
384   animation-delay: -0.6s;
385 }
386 </style>
387 <p class="_plugin_search2_nosupport_message" style="display:none;">
388   $_msg_unsupported_webbrowser $alt_msg
389 </p>
390 $search_props
391 $form
392 <div class="_plugin_search2_search_status">$status_span_text</div>
393 <div class="_plugin_search2_message"></div>
394 $result_page_panel
395 $second_form
396 EOD;
397 }